Skip to content

fix(blockindex): revert pending changes when indexing a block fails - #5027

Open
guo wants to merge 4 commits into
masterfrom
guo/todo-cleanup
Open

guo wants to merge 4 commits into
masterfrom
guo/todo-cleanup

Conversation

@guo

@guo guo commented Sep 29, 2026

Copy link
Copy Markdown
Member

Summary

  • fix(blockindex): PutBlocks / PutBlock left a partially indexed block behind when putBlock or commit failed. The entries stayed in the shared batch and the total block/action counting indexes kept their bumped in-memory sizes, so:

    • Height() reported a height that was never written,
    • retrying the same block failed with wrong block height until restart,
    • the next successful commit flushed the stale entries.

    On failure the indexer now clears the batch, drops the dirty per-address indexes and reloads the counting indexes from the DB, i.e. it returns to its last committed state (the same thing a restart does today). Resolves the TODO: Revert changes in PutBlocks.

  • chore: drop 9 stale TODOs referring to the explorer removed in Remove JSON RPC related code #1057 #1085 (long superseded by the api package) plus one note about sub-chain block forwarding, which no longer exists. Comment-only; gofmt realigned two struct field groups.

Non-consensus: only the block/action index used by API queries is affected.

Test plan

  • New TestIndexerPutBlocksRevertsOnFailure: fails on master (Height() returns 1 after a failed PutBlocks), passes with the fix; also checks the same blocks re-index to the same counts as a clean run
  • go test ./blockindex/ ./consensus/... ./dispatcher/ ./chainservice/
  • go build ./..., go vet on touched packages, gofmt

🤖 Generated with Claude Code

guo added 2 commits September 26, 2026 01:09
PutBlocks and PutBlock left the partially indexed block behind when
putBlock or commit returned an error: the entries stayed in the shared
batch and the total block/action counting indexes kept their bumped
in-memory sizes. Height() then reported a height that was never
written, retrying the same block failed with "wrong block height"
forever, and the next successful commit flushed the stale entries.

Clear the batch, drop the dirty address indexes and reload the counting
indexes from the DB on failure, so the indexer is back at its last
committed state.
The explorer was replaced by the api package long ago and sub-chain
block forwarding no longer exists, so these notes point at nothing.
@guo
guo requested a review from a team as a code owner September 29, 2026 08:00

@envestcc envestcc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing blockindex suite passes, but two fault-injection checks expose new rollback failures: nil index references and overwritten committed counters. Both checks pass against the PR base.

Comment thread blockindex/indexer.go Outdated
return errors.Wrapf(cause, "failed to reload total block index: %v", err)
}
if x.tac, err = db.NewCountingIndexNX(x.kvStore, _totalActionsBucket); err != nil {
return errors.Wrapf(cause, "failed to reload total action index: %v", err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve index references when reloading fails

If storage reads and writes fail during rollback, NewCountingIndexNX returns (nil, error), and this assignment replaces x.tbk with nil before returning. Subsequent Height(), GetBlockIndex(), or indexing calls then panic, even after storage recovers. The same problem applies to x.tac. Load into temporary variables and handle failed recovery without leaving methods able to dereference nil. A fault-injection test reproduces the panic on this patch and passes on the base revision.

Comment thread blockindex/indexer.go Outdated
delete(x.dirtyAddr, k)
}
var err error
if x.tbk, err = db.NewCountingIndexNX(x.kvStore, _totalBlocksBucket); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid recreating committed counters on reload read errors

If a counter read fails but writes remain available, NewCountingIndexNX treats the nil read result as a missing index and persists a zero counter. Using it here therefore lets rollback erase committed block/action counts: after committing block 1, injecting read failures during a rejected block resets the persisted block count from 2 to 0, making Height() underflow. Use a reload path that propagates read errors without initializing or overwriting existing counters. This fault-injection test passes on the base revision and fails on the patch.

envestcc and others added 2 commits October 15, 2026 12:39
… write

Address review on the rollback approach: reloading the counting indexes in
revert() could leave nil indexes when the reload failed, and could reset the
committed counters when a read failed, since NewCountingIndexNX took any read
error as a missing index.

Instead of reverting, PutBlocks now builds a local batch and collects the
entries of the counting indexes, puts them with AddToBatch, and updates the
in-memory sizes of the total block and action indexes only after WriteBatch
succeeds. A failure leaves the indexer and the DB unchanged, so revert(), the
shared batch and the dirty address indexes are gone.

- db: AddToBatch takes multiple values, so it can be used once per batch
- db: NewCountingIndexNX returns read errors instead of resetting the index
- db: add LoadCountingIndex, which does not write for a missing index, so new
  address indexes are only created when their batch is committed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants