A resume cursor is a transaction-log key, and resuming replays everything after it (auditStore.getRange({ start: cursor, exclusiveStart: true })). But on RocksDB a transaction's key is assigned when the transaction is created, not when it commits. Transaction.getTimestamp() in @harperfast/rocksdb-js defaults to "a process-wide monotonic value assigned when the transaction was created", and some write paths set it from JS time instead (DatabaseTransaction calls transaction.setTimestamp(txnTime)). So a transaction can commit after a cursor has already moved past its key, and then it is never replayed:
- Transaction A gets key 100 and stays open.
- Transaction B gets key 200, commits, and is delivered and acknowledged. The consumer persists 200.
- A commits. The live broadcast, which dispatches in log order, delivers it, but the consumer disconnects before acknowledging it.
- The consumer resumes from 200. A's key is below the cursor, so the replay never includes it, and nothing reports the loss.
Who is affected:
Candidate fixes:
- Per-log continuation positions. Resume each transaction log from an anchor transaction in append order, using the machinery replication already has (
getRange({ startByLog, exactStart, resumeAfterExactStart }) and the endTxn markers). A resume position becomes a per-log map instead of a scalar. Single-record history walks, which follow version links by key, need their own answer.
- Cap cursors at the oldest open local write transaction. Scalar cursors stay, but a persisted cursor never passes the key of a local write transaction that is still open. This is only as safe as the tracking's coverage of write paths, and it does not help replicated writes.
Found in the planning review of #2448's MQTT durable-session change, which keeps scalar cursors and documents this gap.
A resume cursor is a transaction-log key, and resuming replays everything after it (
auditStore.getRange({ start: cursor, exclusiveStart: true })). But on RocksDB a transaction's key is assigned when the transaction is created, not when it commits.Transaction.getTimestamp()in@harperfast/rocksdb-jsdefaults to "a process-wide monotonic value assigned when the transaction was created", and some write paths set it from JS time instead (DatabaseTransactioncallstransaction.setTimestamp(txnTime)). So a transaction can commit after a cursor has already moved past its key, and then it is never replayed:Who is affected:
DurableSubscriptionsSession.acknowledge()persists the acknowledged event's key.startTimeas a resume position, including the opt-in resume check from Let subscribe check a resume position against the database generation and retained history #2927. ItsresumeVerifiedcovers retention and database generation, not this ordering gap.Candidate fixes:
getRange({ startByLog, exactStart, resumeAfterExactStart })and theendTxnmarkers). A resume position becomes a per-log map instead of a scalar. Single-record history walks, which follow version links by key, need their own answer.Found in the planning review of #2448's MQTT durable-session change, which keeps scalar cursors and documents this gap.