feat: persistent applied-action ledger — replay-safe reindex#52
Merged
Conversation
Every data incident this year traced to the same structural gap: the indexer has no durable record of which chain actions it already applied. The in-memory seenGlobalSeqs Set dies on restart/seek, so any reindex/seek re-invokes every updater over existing rows, and safety depends entirely on hand-written per-updater guards (miss one — as with reward/verifyClaim — and a reindex silently corrupts). Add a _processed_actions ledger keyed on global_action_seq (unique across contracts; account_action_seq is per contract and collides): - GetActionsReader forwards global_action_seq as payload.globalSequence. - app.js creates the table at boot (indexer infra, owned here like demux's _index_state / _block_number_txid). - updaters.js wraps every updater (including future ones, via a map at export): INSERT .. ON CONFLICT DO NOTHING RETURNING atomically claims the seq inside the same block-level transaction demux opened for the updater's writes, so ledger row and domain writes commit or roll back together; an already-claimed seq skips the updater entirely. Per-updater guards stay on as the second layer — they also cover history processed before this ledger existed. With this, a block-range reindex becomes a sanctioned, safe operation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (the structural one)
Every data incident this year — duplicate claims/orders, the notification spam, the stale claim statuses, the phantom action ids — traced back to one gap: the indexer has no durable record of which chain actions it already applied. The in-memory
seenGlobalSeqsSet dies on restart/seekToBlock, so any reindex re-invokes every updater over existing rows, and correctness depends entirely on hand-written per-updater guards. Miss one (as happened withreward/verifyClaim) and a reindex silently corrupts prod.Fix
A
_processed_actionsledger keyed onglobal_action_seq(globally unique;account_action_seqis per contract and collides betweencambiatus.cmandcambiatus.tk):GetActionsReaderforwardsglobal_action_seqaspayload.globalSequence(it already fetched it for in-memory dedup).app.jscreates the table at boot (CREATE TABLE IF NOT EXISTS) — indexer infra, owned here like demux's_index_state/_block_number_txid, no backend migration needed.updaters.jswraps every updater (incl. future ones, via a map at export):INSERT … ON CONFLICT DO NOTHING RETURNINGatomically claims the seq inside the same block-level transaction demux opened for the updater's writes — ledger row and domain writes commit or roll back together. Already-claimed seq → skip, logged.Per-updater guards stay as the second layer: they cover history processed before the ledger existed (which has no rows), and the pre-existing window where an updater's inner
db.withTransactioncommits on a separate connection.Verified
node --checkclean.null→ updater skipped.After this merges + deploys, block-range reindexes become a sanctioned operation (runbook to follow in the workspace docs).
🤖 Generated with Claude Code