Skip to content

fix(explorer): stop the API truncating the indexer's tables; upsert the cursor - #10

Merged
MehranMazhar merged 1 commit into
mainfrom
fix/indexer-cursor-truncated-by-the-api
Sep 14, 2026
Merged

MehranMazhar merged 1 commit into
mainfrom
fix/indexer-cursor-truncated-by-the-api

Conversation

@MehranMazhar

Copy link
Copy Markdown
Member

Symptom

On stage: indexer_cursor had zero rows while the indexer logged a cursor walking 295 → 303, and blocks held 7 rows on a chain at height 303.

Two defects

The API truncates tables it does not own. run_api called cleanup_database, which is TRUNCATE ... indexer_cursor, blocks, ... CASCADE. The API is a reader; the indexer is a separate container against the same database. A truncate from the API races a live indexer.

Moved to run_indexer, and now after the migrations — the API ran it before them, where truncating tables that do not exist yet fails on a fresh database, and the error was logged and swallowed.

set_cursor was an UPDATE. An UPDATE whose row is missing affects zero rows and returns Ok. So once the row was gone the indexer kept its position only in memory and persisted nothing, silently, forever. Now an upsert — whatever removes the row, the next poll puts it back.

Why nothing noticed

reconcile_head compares only the head. Blocks written after the truncate matched the tip, so reconciliation was content while 1..295 stayed missing permanently. The hole is invisible from the one height that gets checked.

Stage remediation

None needed. The cursor row is currently absent, so the next indexer start takes ensure_cursor's start_height of 0 and walks 1..303; index_height upserts blocks ON CONFLICT (height), so the existing rows are rewritten rather than colliding.

Not tested

CI runs cargo test --all-targets with no Postgres service, and both changes are SQL against a live database. The upsert is one statement; the move is a relocated call site.

🤖 Generated with Claude Code

…he cursor

Stage showed an `indexer_cursor` table with zero rows while the indexer logged
a cursor walking 295 to 303, and 7 blocks on a chain at 303.

Two defects compounding:

- `run_api` called `cleanup_database`, which TRUNCATEs `indexer_cursor`,
  `blocks` and everything else. The API is a READER of tables the indexer
  writes, and the two run as separate containers against one database, so that
  truncate races a live indexer. Wiping now belongs to `run_indexer`, which
  owns the data, and happens after the migrations rather than before them --
  the API ran it first, where a truncate of tables that do not exist yet fails
  on a fresh database, and swallowed the error.

- `set_cursor` was an `UPDATE`. An `UPDATE` whose row is missing affects zero
  rows and returns Ok, so once the row was gone the indexer went on counting
  in memory and persisted nothing, with no error anywhere. It is an upsert
  now: whatever removes the row, the next poll puts it back.

The wipe was invisible from the tip, too. `reconcile_head` only compares the
head, so blocks written after a truncate matched and nothing looked wrong
while 1..295 stayed missing for good.

Also drops the developer-mode shutdown wipe from the API for the same reason;
the indexer keeps its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MehranMazhar
MehranMazhar merged commit a0f4f0f into main Sep 14, 2026
4 checks passed
@MehranMazhar
MehranMazhar deleted the fix/indexer-cursor-truncated-by-the-api branch September 14, 2026 17:50
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.

1 participant