feat(monitor/collectors/coinbase): per-product sequence-gap detector
This commit is contained in:
parent
10862314f0
commit
3e398da3fb
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue