marketbyorder-bot associates SnapshotOrder records with an instrument by snapshot_id alone, but Snapshot ID is scoped per instrument, not per channel.
Spec (market-by-order, 0x20): "Monotonically increasing per (channel_id, instrument_id) within the current Reset Count era." The association rule is "discard any SnapshotOrder whose Snapshot ID does not match the currently-open SnapshotBegin" — snapshot_id validates the match, it isn't the key.
Two places assume channel-wide uniqueness:
shard.go:164-176 — applySnapshotOrder scans s.instruments and inserts into the first instrument whose OpenSnapshot.SnapshotID matches. Go map iteration is randomized, so with two matching instruments the target is arbitrary.
shard.go:24-27,420 — snapCtx is keyed {channel_id, snapshot_id}, so two instruments sharing an id collide and the later snapshot_begin overwrites the earlier context. wire_snapshots rows can then be written with the wrong instrument_id/symbol.
Publishers must not interleave snapshot groups, which normally leaves one group open at a time. The gap is when a SnapshotEnd is lost: that instrument's OpenSnapshot is never cleared and lingers. Because each instrument's counter advances once per cycle, instruments in the same cycle typically sit at the same snapshot_id — so a stale open snapshot is likely to collide with the next instrument's rather than unlikely. #20 measured ~3.7% of snapshots arriving short on the Tokyo feed, so lingering open snapshots are not hypothetical.
Effects are bounded but real: misattributed orders land in a shadow that fails its count check and is discarded (snapshot_discarded_total), so live books are not corrupted — but recovery for the affected instrument is delayed a cycle, and mis-keyed wire_snapshots rows are persisted.
Fix: key the association on the currently-open snapshot for a known (channel_id, instrument_id) and use snapshot_id only to validate. snapCtx needs the instrument in its key.
Found while writing marketbyprice-parser (#29) — the market-by-price spec scopes Snapshot ID identically. Not verified against a live feed.
marketbyorder-botassociatesSnapshotOrderrecords with an instrument bysnapshot_idalone, butSnapshot IDis scoped per instrument, not per channel.Spec (market-by-order,
0x20): "Monotonically increasing per(channel_id, instrument_id)within the currentReset Countera." The association rule is "discard anySnapshotOrderwhoseSnapshot IDdoes not match the currently-openSnapshotBegin" —snapshot_idvalidates the match, it isn't the key.Two places assume channel-wide uniqueness:
shard.go:164-176—applySnapshotOrderscanss.instrumentsand inserts into the first instrument whoseOpenSnapshot.SnapshotIDmatches. Go map iteration is randomized, so with two matching instruments the target is arbitrary.shard.go:24-27,420—snapCtxis keyed{channel_id, snapshot_id}, so two instruments sharing an id collide and the latersnapshot_beginoverwrites the earlier context.wire_snapshotsrows can then be written with the wronginstrument_id/symbol.Publishers must not interleave snapshot groups, which normally leaves one group open at a time. The gap is when a
SnapshotEndis lost: that instrument'sOpenSnapshotis never cleared and lingers. Because each instrument's counter advances once per cycle, instruments in the same cycle typically sit at the samesnapshot_id— so a stale open snapshot is likely to collide with the next instrument's rather than unlikely. #20 measured ~3.7% of snapshots arriving short on the Tokyo feed, so lingering open snapshots are not hypothetical.Effects are bounded but real: misattributed orders land in a shadow that fails its count check and is discarded (
snapshot_discarded_total), so live books are not corrupted — but recovery for the affected instrument is delayed a cycle, and mis-keyedwire_snapshotsrows are persisted.Fix: key the association on the currently-open snapshot for a known
(channel_id, instrument_id)and usesnapshot_idonly to validate.snapCtxneeds the instrument in its key.Found while writing
marketbyprice-parser(#29) — the market-by-price spec scopesSnapshot IDidentically. Not verified against a live feed.