fix(databases): retry transient index-backfill errors in-process instead of parking#1371
Draft
heskew wants to merge 1 commit into
Draft
fix(databases): retry transient index-backfill errors in-process instead of parking#1371heskew wants to merge 1 commit into
heskew wants to merge 1 commit into
Conversation
…ead of parking A secondary-index backfill pass that failed with only transient RocksDB errors (ERR_BUSY/ERR_TRY_AGAIN under bulk-ingest load) would park the index — leaving isIndexing=true + indexingFailed=true and waiting for an operator restart to retry. runIndexing now retries the whole pass in-process from the pass's starting checkpoint with bounded exponential backoff, re-reading every row (indexing is idempotent), so a transient error self-heals without a restart. A permanent error, or exhausting the retry budget, still parks exactly as before, so the change is strictly an improvement. The retry resets to the pass start rather than the intra-pass checkpoint: that checkpoint advances every 100 rows, even past a put that later rejects asynchronously, so it is not a safe resume point. Re-reading from the pass start re-covers any row that errored mid-pass and avoids a silent index gap. Resolves #1356 (part of #1354). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Contributor
|
Reviewed; no blockers found. |
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
This was referenced Jun 18, 2026
Member
|
The retry logic here is sound, but #410 so radically reworks this area that I think this PR would be better stacked on top of that one rather than merged independently. Given the scope of overlap, this feels like 5.2 material — worth waiting for #410 to land first so this doesn't need a rebase or conflict resolution after the fact. Happy to revisit once #410 is in! 🙂 — Kris (via Claude Sonnet 4.6) |
Member
Author
|
@kriszyp - Sounds good. I'll pull this back to a draft for now. |
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.
Summary
A secondary-index backfill pass (
runIndexing) that failed with only transient RocksDB errors (ERR_BUSY/ERR_TRY_AGAIN— e.g. the write buffer filling under bulk-ingest load) would park the index: leavingisIndexing=true+indexingFailed=trueand waiting for an operator restart to retry. This change retries the whole pass in-process from the pass's starting checkpoint with bounded exponential backoff (≤10 retries, capped 1s), re-reading every row. A transient error now self-heals without a restart.A permanent (non-transient) error, or exhausting the retry budget, still parks exactly as before — so this is strictly an improvement over the prior behavior.
Resolves #1356 (part of #1354).
Where to look
resources/databases.ts—runIndexing. Retry is implemented via bounded recursion: on a transient-only failed pass it resets eachattribute.lastIndexedKeyto the pass start, persists it, backs off, and re-enters with an emptyindicesToRemove(drops already done) andretryAttempt + 1.lastIndexedKey = key, written every 100 rows) can advance past a put that later rejects asynchronously, so resuming from it could skip a failed row and leave a silent index gap. Re-reading from the pass start re-covers it (indexing is idempotent). A regression test asserts exactly this.isTransientIndexingErrormirrors theERR_BUSY/ERR_TRY_AGAINclassification already used inDatabaseTransaction's commit retry.Open item for the reviewer
Cross-model review (Codex + Gemini) surfaced one pre-existing, out-of-scope point I did not change: for multi-value attributes only the last
put()per record is tracked bywhen(), so a non-last put that rejects is an unhandled rejection. This predates this PR (the inline comment at the post-loopawait lastResolutionalready notes it). Flagging in case we want a follow-up issue; left alone here to keep scope tight.Docs
Operator-facing surface is a new
[warn]log line when a backfill retries (…hit transient errors; retrying pass N/10…) plus the self-healing behavior. No API/config/schema change — I don't think this needs a docs PR, but happy to add a sentence to the indexing/migration operational notes if wanted.Tests
unitTests/resources/schemaMigrationFragility.test.js(RocksDB-only): transient errors retry to clean completion (exercising both the synchronous-throw and async-reject branches), permanent errors still park + recover on a simulated restart, and — the important one — a row that failed before an intra-pass checkpoint is re-covered by the retry (no gap).🤖 Generated with Claude Code (Opus 4.8)