feat(monitor/collectors/coinbase): per-product sequence-gap detector

This commit is contained in:
CarterPerez-dev 2026-05-02 03:27:32 -04:00
parent 10862314f0
commit 3e398da3fb
2 changed files with 95 additions and 0 deletions

View File

@ -0,0 +1,32 @@
// ©AngelaMos | 2026
// sequencer.go
package coinbase
import "sync"
type Sequencer struct {
mu sync.Mutex
last map[string]int64
}
func NewSequencer() *Sequencer {
return &Sequencer{last: make(map[string]int64)}
}
func (s *Sequencer) Observe(productID string, seq int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
prev, ok := s.last[productID]
s.last[productID] = seq
if !ok {
return false
}
return seq != prev+1
}
func (s *Sequencer) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
s.last = make(map[string]int64)
}

View File

@ -0,0 +1,63 @@
// ©AngelaMos | 2026
// sequencer_test.go
package coinbase_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/coinbase"
)
func TestSequencer_FirstTickIsAlwaysInOrder(t *testing.T) {
s := coinbase.NewSequencer()
gap := s.Observe("BTC-USD", 100)
require.False(t, gap, "first sequence number must not be a gap")
}
func TestSequencer_ConsecutiveSequencesAreInOrder(t *testing.T) {
s := coinbase.NewSequencer()
for i := int64(50); i < 60; i++ {
gap := s.Observe("BTC-USD", i)
require.False(t, gap, "i=%d", i)
}
}
func TestSequencer_GapTriggersReportSignal(t *testing.T) {
s := coinbase.NewSequencer()
require.False(t, s.Observe("BTC-USD", 50))
require.False(t, s.Observe("BTC-USD", 51))
require.True(t, s.Observe("BTC-USD", 60), "skipping 52-59 must report gap")
}
func TestSequencer_GapsArePerProduct(t *testing.T) {
s := coinbase.NewSequencer()
require.False(t, s.Observe("BTC-USD", 100))
require.False(t, s.Observe("ETH-USD", 200))
require.False(t, s.Observe("BTC-USD", 101))
require.True(t, s.Observe("ETH-USD", 250))
require.False(t, s.Observe("BTC-USD", 102))
}
func TestSequencer_ResetClearsAllProducts(t *testing.T) {
s := coinbase.NewSequencer()
s.Observe("BTC-USD", 100)
s.Observe("ETH-USD", 200)
s.Reset()
require.False(t, s.Observe("BTC-USD", 9999))
require.False(t, s.Observe("ETH-USD", 8888))
}
func TestSequencer_DuplicateSequenceTreatedAsGap(t *testing.T) {
s := coinbase.NewSequencer()
require.False(t, s.Observe("BTC-USD", 100))
require.True(t, s.Observe("BTC-USD", 100), "replaying the same seq is a gap")
}
func TestSequencer_BackwardSequenceTreatedAsGap(t *testing.T) {
s := coinbase.NewSequencer()
require.False(t, s.Observe("BTC-USD", 100))
require.True(t, s.Observe("BTC-USD", 90))
}