You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A snapshot subscription can drop a write made during its initial scan #2933
When a collection subscription starts with a current-state snapshot (Table.subscribe without omitCurrent: WebSocket and SSE connect, and an MQTT subscribe that wants retained values), live events that arrive during the scan are buffered in pendingRealTimeQueue. After the scan, the buffer is filtered against cursorMaxTime, the newest record time the scan saw:
subscription.startTime is then set to cursorMaxTime, so the broadcaster also skips any record at or below it that it has not yet dispatched.
The filter exists to drop pre-subscribe events whose effect the scan already delivered. But cursorMaxTime is a maximum over every scanned row, so it also drops events the scan never covered:
The scan visits record R and sends its value as of time 90.
R is updated at 110. The event is buffered.
Record S, later in key order, is updated at 120, and the scan then visits it. cursorMaxTime becomes 120.
The filter drops R's event (110 ≤ 120). The subscriber keeps R as of 90 until R is written again.
The same happens to a message published to a record during the scan: messages do not change the stored value, so the scan cannot have delivered one.
This is silent. The subscriber sees no error, and a rarely-written record can stay stale indefinitely. The trigger is ordinary: any write to an already-scanned record while later rows have newer writes, which is common for a large table under write load.
Suggested fix. Drop a buffered event only when the scan delivered the same record at a version at or above the event's. Or drop nothing: the comment above the scan already accepts duplicate delivery ("Retained-message semantics: subscriber may legitimately receive a record twice ... We don't dedupe"). Either way, subscription.startTime should not advance to cursorMaxTime over records the scan did not cover.
Found while designing harper#2448's MQTT change, whose opt-in progress tracking keeps every buffered event rather than rely on this filter.
A second trigger for the same gate, which needs no write during the scan. On RocksDB a transaction's timestamp is assigned when the transaction is created, so a transaction with an older timestamp can commit after the scan has already seen a newer record:
Transaction A stages a write to record X (key 100), then awaits something.
B writes Y (key 101) and commits.
The current-state scan sees Y, so cursorMaxTime becomes 101 and subscription.startTime is 101.
The Harper PR that bumps rocksdb-js for #902 will fix this along with the scenario above. It gates by record version instead of by time: after the scan, a live or buffered event at or before cursorMaxTime is kept while it is still the record's current version. A version-gated subscription is exempt from the broadcaster's time skip. Duplicates of the current state are already accepted by the retained-message semantics this branch documents.
🤖 Generated by Claude Code (Claude Opus 5.5); posted via @kriszyp.
Activity
kriszyp commentedon Oct 5, 2026
A second trigger for the same gate, which needs no write during the scan. On RocksDB a transaction's timestamp is assigned when the transaction is created, so a transaction with an older timestamp can commit after the scan has already seen a newer record:
cursorMaxTimebecomes 101 andsubscription.startTimeis 101.startTime >= timestamp,resources/transactionBroadcast.ts:296), and the subscriber never receives X.This reproduces on current
mainwith@harperfast/rocksdb-js2.10.0. Run a database's async commits on up to four concurrent commit threads (HarperFast/rocksdb-js#902) makes it routine, because any two overlapping commits to one database can then finish out of dispatch order.The Harper PR that bumps rocksdb-js for #902 will fix this along with the scenario above. It gates by record version instead of by time: after the scan, a live or buffered event at or before
cursorMaxTimeis kept while it is still the record's current version. A version-gated subscription is exempt from the broadcaster's time skip. Duplicates of the current state are already accepted by the retained-message semantics this branch documents.🤖 Generated by Claude Code (Claude Opus 5.5); posted via @kriszyp.
Deliver commits that finish out of timestamp order, and bound deliver…
Deliver commits that finish out of timestamp order, and bound deliver…
Deliver commits that finish out of timestamp order, and bound deliver…
Deliver commits that finish out of timestamp order, and bound deliver…
Deliver commits that finish out of timestamp order, and bound deliver…
Deliver subscription events for commits that finish out of timestamp …
Deliver subscription events for commits that finish out of timestamp …