From 5feeaf96151d5cc0cc996cb461acf56d14cde938 Mon Sep 17 00:00:00 2001 From: Julien Lucca Date: Sun, 27 Sep 2026 22:22:07 +0200 Subject: [PATCH 1/2] fix(indexer): always write the resume point to _index_state row 1 demux loads the resume point from _index_state id=1 only. Our updateIndexState did findOne({id: 1}) || {} and save()d the result, so on a database without that row every action block inserted a new serial row, id=1 never appeared, and every restart replayed from block 1. Upsert row 1 explicitly. Prod already has row 1, so its behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 --- src/GetActionsHandler.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/GetActionsHandler.js b/src/GetActionsHandler.js index d0e550d..911f378 100644 --- a/src/GetActionsHandler.js +++ b/src/GetActionsHandler.js @@ -45,13 +45,16 @@ class GetActionsHandler extends MassiveActionHandler { // crash with a duplicate key violation since that table has no upsert support. async updateIndexState (state, block, isReplay, context) { const { blockInfo } = block - const fromDb = (await state._index_state.findOne({ id: 1 })) || {} - const toSave = Object.assign({}, fromDb, { - block_number: blockInfo.blockNumber, - block_hash: blockInfo.blockHash, - is_replay: isReplay - }) - await state._index_state.save(toSave) + // demux reads the resume point from row id=1 only (loadIndexState). Upsert + // that row explicitly: a save() without an id — what an empty table gave — + // inserted a fresh serial row per block, so id=1 never existed and every + // restart replayed from block 1. + await state.instance.none( + `INSERT INTO _index_state (id, block_number, block_hash, is_replay) VALUES (1, $1, $2, $3) + ON CONFLICT (id) DO UPDATE SET block_number = EXCLUDED.block_number, + block_hash = EXCLUDED.block_hash, is_replay = EXCLUDED.is_replay`, + [blockInfo.blockNumber, blockInfo.blockHash, isReplay] + ) await state.instance.none( 'INSERT INTO _block_number_txid (block_number, txid) VALUES ($1, $2) ON CONFLICT DO NOTHING', [blockInfo.blockNumber, context.txid] From 6793c394644308eb0eece508689e70436e71f61a Mon Sep 17 00:00:00 2001 From: Julien Lucca Date: Sun, 27 Sep 2026 22:22:42 +0200 Subject: [PATCH 2/2] docs(indexer): one comment for both updateIndexState overrides Co-Authored-By: Claude Opus 5.5 --- src/GetActionsHandler.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/GetActionsHandler.js b/src/GetActionsHandler.js index 911f378..db0891a 100644 --- a/src/GetActionsHandler.js +++ b/src/GetActionsHandler.js @@ -40,15 +40,14 @@ class GetActionsHandler extends MassiveActionHandler { return [false, 0] } - // Override to use ON CONFLICT DO NOTHING for _block_number_txid. - // Re-processed blocks (e.g. after switching from NodeosActionReader) would otherwise - // crash with a duplicate key violation since that table has no upsert support. + // Overridden for two reasons: + // - demux reads the resume point from _index_state row id=1 only (loadIndexState). + // A save() without an id inserted a fresh serial row per block, so id=1 never + // existed and every restart replayed from block 1. Upsert row 1 explicitly. + // - Re-processed blocks (e.g. after switching from NodeosActionReader) would crash + // on _block_number_txid's duplicate key, since that table has no upsert support. async updateIndexState (state, block, isReplay, context) { const { blockInfo } = block - // demux reads the resume point from row id=1 only (loadIndexState). Upsert - // that row explicitly: a save() without an id — what an empty table gave — - // inserted a fresh serial row per block, so id=1 never existed and every - // restart replayed from block 1. await state.instance.none( `INSERT INTO _index_state (id, block_number, block_hash, is_replay) VALUES (1, $1, $2, $3) ON CONFLICT (id) DO UPDATE SET block_number = EXCLUDED.block_number,