Repository navigation
Conversation
|
Warning Review limit reachedOnly developers with an assigned seat can use this organization's usage-based review budget, and seats here are assigned manually. Ask an admin to assign a seat, or change the review continuation mode in Billing. Next included review available in 40 minutes. View limit details
Comment |
|
@kevincodex1 this is the indexer durability change for #112. The app, contracts, deps and guard jobs pass. |
Make cursor_block and head_block GREATEST updates so overlapping sync machines cannot move them backwards. Share one full poll pass across concurrent callers. Replace the newest-100 trader heal with a keyset cursor that advances past every visited tx group, add the matching partial index, and prune retry maps instead of clearing them.
5d0179c to
6e72822
Compare
Fixes #112
What changed
run()now writescursor_block = GREATEST(cursor_block, $to)andhead_block = GREATEST(COALESCE(head_block, 0), $head). Two sync machines that read the same cursor can finish out of order without the slower one pulling the cursor (and every later read) back to its ownto.last_erroris cleared only when the run reached the cursor already stored, so a lagging successful machine cannot hide a real newer failure. The error path keeps the highest observed head instead of replacing it with a lower one.pollAll()is now single-flight. The per-chainrun()guard already deduplicatedrun()itself, but two callers could still run the trader heal, quote-token heal, attribution backlog and holder backfill at the same time. The 15s loop and a webPOST /api/launch/syncnow share one pass.healSwapTraders()usedORDER BY block_number DESC LIMIT 100every tick. Rows whose transaction could not be read are parked inhealRestUntiland skipped, but they still occupied those 100 slots, so the rows behind them were never selected. It now uses a keyset on(block_number, log_index), groups the page by transaction, and advances a per-chain cursor past every visited group — parked or not. At the end it wraps, so parked rows and rows added behind the cursor get another look. The pure planner is intraderHeal.ts; the DB page is 500 rows and at most 20 live transactions are read per tick, preserving the old RPC budget.healRestUntilandattributionTriesnow drop expired entries first and then evict the oldest insertion when they reach 5,000 / 20,000 keys. The oldclear()threw away every backoff at once, which let a burst of junk keys immediately retry rows that were supposed to rest.bb_launch_swaps_trader_null_idxon(chain_id, block_number, log_index) WHERE trader IS NULLmatches the new filter and order, and stays small once the backlog is drained.Why
The cursor write was last-writer-wins: with more than one app instance, a machine that finished block 190 after another finished 200 set the cursor back to 190. Every machine then re-read the overlap and could keep regressing. The trader heal had the opposite starvation problem: permanently unreadable transactions at the head of the newest-100 window kept every older missing trader out of the batch forever. Both are durability bugs that get worse exactly when the RPC is unhealthy.
Tests
npm test: 1382 passed, 0 failed, 11 skipped (including 17 targeted indexer/schema tests).npm run typecheck: passed.npm run lint: passed.npm run build: passed (same three pre-existing filesystem-tracing warnings).traderHeal.test.ts: groups keep row order; parked groups spend no chain read; an all-parked page still advances the cursor;maxReadsstops before the next live group; an empty page has no cursor.boundedMap.test.ts: expired retry entries are dropped before active ones; a full map with nothing expired evicts the oldest insertion; refreshing a key moves it out of the eviction lane; a burst of one-shot keys stays at the bound.indexer-durability.test.ts: pins theGREATESTcursor/head SQL, the conditionallast_errorclear, the single-flightpollAllwrapper, the keyset order, the bounded-map calls, the absence of the old newest-100 query and bothclear()calls, and the partial index.schema.test.tsvalidates the new index againstschema.sqlcolumns.Risk
No table or column changes. The new index is built by the existing
CONCURRENTLYmigration path. Per-tick RPC work is unchanged (20 transactions), and the 500-row DB page is covered by the partial index. The cursor is monotonic, so a reorg below the stored cursor is handled by the existing overlap window and the next successful pass, not by moving the cursor back.Checks
pr-guardreports theapp/db/concurrent-indexes.sqlchange as a review-tier finding, so that job stays red until an owner approves this exact head commit (the app, contracts, deps and guard jobs pass). The change is the partial index the issue asks for.