fix: poller uses store max IDs to prevent replaying entire DB

When GetMaxTransmissionID() fails silently (e.g., corrupted DB returns 0
from COALESCE), the poller starts from ID 0 and replays the entire
database over WebSocket — broadcasting thousands of old packets per second.

Fix: after querying the DB, use the in-memory store's MaxTransmissionID
and MaxObservationID as a floor. Since Load() already read the full DB
successfully, the store has the correct max IDs.

Root cause discovered on staging: DB corruption caused MAX(id) query to
fail, returning 0. Poller log showed 'starting from transmission ID 0'
followed by 1000-2000 broadcasts per tick walking through 76K rows.

Also adds MaxObservationID() to PacketStore for observation cursor safety.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-31 23:28:56 -07:00
co-authored by Copilot
parent 8e6fc9602f
commit 738d5fef39
2 changed files with 25 additions and 0 deletions
+14
View File
@@ -1344,6 +1344,20 @@ func (s *PacketStore) MaxTransmissionID() int {
return maxID
}
// MaxObservationID returns the highest observation ID in the store.
func (s *PacketStore) MaxObservationID() int {
s.mu.RLock()
defer s.mu.RUnlock()
maxID := 0
for id := range s.byObsID {
if id > maxID {
maxID = id
}
}
return maxID
}
// --- Internal filter/query helpers ---
// filterPackets applies PacketQuery filters to the in-memory packet list.
+11
View File
@@ -166,6 +166,17 @@ func NewPoller(db *DB, hub *Hub, interval time.Duration) *Poller {
func (p *Poller) Start() {
lastID := p.db.GetMaxTransmissionID()
lastObsID := p.db.GetMaxObservationID()
// If the store already loaded data, use its max IDs as a floor.
// This prevents replaying the entire DB when the DB query fails
// (e.g., corrupted DB returns 0 from COALESCE).
if p.store != nil {
if storeMax := p.store.MaxTransmissionID(); storeMax > lastID {
lastID = storeMax
}
if storeMaxObs := p.store.MaxObservationID(); storeMaxObs > lastObsID {
lastObsID = storeMaxObs
}
}
log.Printf("[poller] starting from transmission ID %d, obs ID %d, interval %v", lastID, lastObsID, p.interval)
ticker := time.NewTicker(p.interval)