fix(kb): wait for indexing, and stop re-ingesting while it runs - #901
Merged
Conversation
…runs A 1.5 MB PDF uploaded to a promoted knowledge base in dev sat at `uploading` indefinitely with a fully retrievable copy in the knowledge base. Three separate bugs stacked up. 1. The poll window was sized against the wrong measurement --------------------------------------------------------- The consumer ingested and then polled a *retrieval* for 30 s. Its own header justifies that window with "Bedrock reports INDEXED up to a second before it can be retrieved — measured at 0.75–1.03 s", but the poll starts the moment the ingest call returns, so it actually has to cover `ingest -> INDEXED -> retrievable`. §5.1 of the evaluation measured PDF ingestion at 37–264 s; this file took 5 m 30 s. The budget was smaller than the documented lower bound. Identical in shape to §5.30, where `verify` failed good migrations against that same 0.75–1.03 s figure. That fix never reached this component, which inherited the constant. 2. Every redelivery re-ingested, restarting the work it was waiting for ---------------------------------------------------------------------- `IngestKnowledgeBaseDocuments` is fire-and-forget, and nothing asked Bedrock what it already knew, so the consumer could not tell "not indexed yet" from "never submitted". Each of the three deliveries re-submitted the document. It reached INDEXED 54 s after the final attempt had been dead-lettered. `handle_object` now probes `GetKnowledgeBaseDocuments` first and branches on the real `DocumentStatus` enum, taken from the packaged service model: STARTING/PENDING/IN_PROGRESS means do not re-ingest; FAILED is terminal; PARTIALLY_INDEXED counts as usable, because the document IS retrievable and failing it would hide content the user can see. A probe failure reads as NOT_FOUND — no evidence of prior work — so it never blocks ingestion. 3. `indexedAt` was fabricated ----------------------------- `indexed_at = _now_iso()` ran immediately after the ingest call returned, so the field recorded when we asked, presented as when indexing finished — minutes apart for this document. It is now Bedrock's own `updatedAt`. The pre-existing test asserted only that the key existed and was truthy, which a fabricated value satisfies; that is why this survived. It also means my earlier claim that a measured 0.9 s gap "confirmed the 0.75–1.03 s figure" was wrong: that gap was ingest-return to retrievable, not INDEXED to retrievable. Why the wait is in-invocation and not more retries -------------------------------------------------- I first raised a RetryPolicy on the EventBridge target. That does nothing, and the construct now says so instead: for a Lambda target EventBridge hands the event off and the function's OWN async retry config governs — `retryAttempts: 2`, which is Lambda's hard maximum and exactly the 1 + 2 attempts seen in the logs. Redelivery therefore spans a few minutes and cannot be extended, so INDEXED_POLL_TIMEOUT_SECONDS is 600 s: covers the measured tail, leaves 5 minutes under the 15-minute Lambda timeout. Small documents still complete in one invocation, so the fast path is unchanged. Guards: 8 new tests in test_kb_ingestion_consumer.py, and a cross-language test in test_kb_migration_env_contract.py that parses the Lambda timeout out of the CDK construct and asserts the poll budget fits inside it with headroom — the two live in different languages with no compiler between them. `_FakeBackend` now models document STATUS, not just ingest calls. A fake that reported instant success is what let a 30 s window look adequate for work that takes minutes — the same failure as §5.28, where the fake modelled `clientToken` dedup as permanent. Mutations verified caught, each by correctly-named tests: in-flight guard removed, `indexedAt` back to the local clock, FAILED treated as retryable, budget raised past the Lambda timeout, budget dropped back to 25 s. Backend 2,510 passed across the affected areas; infra 640 passed.
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.
A 1.5 MB PDF uploaded to a promoted knowledge base sat at
uploadingindefinitely, with a fully retrievable copy already in the knowledge base. Three bugs stacked.1. The poll window was sized against the wrong measurement
The consumer ingested, then polled a retrieval for 30 s. Its own header justifies that with "Bedrock reports INDEXED up to a second before it can be retrieved — measured at 0.75–1.03 s". But the poll starts the instant the ingest call returns, so it actually has to cover:
The budget was smaller than the documented lower bound for a PDF.
Same shape as §5.30, where
verifyfailed good migrations against that identical 0.75–1.03 s figure. That fix never reached this component, which inherited the constant.2. Every redelivery re-ingested, restarting the work it was waiting for
IngestKnowledgeBaseDocumentsis fire-and-forget and nothing asked Bedrock what it already knew, so the consumer could not distinguish "not indexed yet" from "never submitted". All three deliveries re-submitted. The document reached INDEXED 54 seconds after the final attempt was dead-lettered.handle_objectnow probesGetKnowledgeBaseDocumentsfirst and branches on the realDocumentStatusenum (read from the packaged service model, not guessed):STARTING/PENDING/IN_PROGRESSFAILED/METADATA_UPDATE_FAILEDPARTIALLY_INDEXED/METADATA_PARTIALLY_INDEXEDNOT_FOUND(incl. probe failure)3.
indexedAtwas fabricatedindexed_at = _now_iso()ran immediately after the ingest call returned, recording when we asked and presenting it as when indexing finished — minutes apart here. Now Bedrock's ownupdatedAt.The pre-existing test asserted only that the key existed and was truthy, which a fabricated value satisfies perfectly. That is why it survived. It also means an earlier claim of mine — that a measured 0.9 s gap "confirmed the 0.75–1.03 s figure" — was wrong: that gap was ingest-return→retrievable, not INDEXED→retrievable.
Why the wait is in-invocation, not more retries
I first raised a
RetryPolicyon the EventBridge target. It does nothing, and the construct now says so instead of carrying it: for a Lambda target EventBridge hands the event off, and from there the function's own async retry config governs —retryAttempts: 2, which is Lambda's hard maximum and exactly the 1 + 2 attempts in the logs (verified against the liveget-function-event-invoke-config).Redelivery therefore spans only a few minutes and cannot be extended. So
INDEXED_POLL_TIMEOUT_SECONDS = 600: covers the measured tail, leaves 5 minutes under the 15-minute Lambda timeout. Small documents still finish in one invocation — the fast path is unchanged.Verification
8 new tests, plus a cross-language test that parses the Lambda timeout out of the CDK construct and asserts the poll budget fits inside it with headroom. Those two values live in different languages with no compiler between them, and a wait longer than the timeout is a killed invocation — the exact bug being fixed.
_FakeBackendnow models document status, not just ingest calls. A fake that reported instant success is what let a 30 s window look adequate for work that takes minutes — the same failure as §5.28, where the fake modelledclientTokendedup as permanent.Mutations verified caught, each by a correctly-named test:
test_a_document_already_being_indexed_is_not_re_ingestedindexedAtback to local clocktest_indexed_at_is_bedrocks_timestamp_not_our_clockFAILEDtreated as retryabletest_a_failed_document_is_marked_failed_and_not_retriedtest_the_poll_budget_leaves_headroom_under_the_lambda_timeouttest_the_budget_covers_the_measured_indexing_tailBackend 2,510 passed across the affected areas; infra 640 passed.
Why this surfaced now
Before #900 the legacy pipeline also wrote document status and would have marked this
completeregardless, masking it. Single ownership is correct — it just means latent failures in the owner are visible instead of hidden.Note
DOC-87323a4fa3efin dev is still parked atuploading. Its content is fine (Bedrock reportsINDEXED; 10 chunks retrieved), but nothing will reconcile the record — there is no reprocess endpoint yet (task 14.4). It needs a one-off repair or a re-upload.