[FEATURE] Write a completion marker when every chunk for a document stored - #532
Merged
noel-improv merged 5 commits intoSep 14, 2026
Merged
Conversation
Nothing recorded whether a document's chunks all reached S3, so a truncated prefix and a short document read back the same. _drain swallowed each chunk's exception and returned nothing, which awslabs#507 documented rather than fixed. _drain now reports whether every upload landed, and a document whose uploads all succeeded gets a marker holding its chunk ids, their count and a hash over them. A failed chunk leaves no marker and still yields its document, as it does today. Both downloaders skip the marker key. Neither filtered by extension, and TextNode.from_json accepts a marker as a node with a generated uuid and empty text rather than rejecting it, so an unfiltered listing would have turned the marker into a phantom chunk on every read. Three duplications collapsed on the way: the index-key node filter, the node-id hash the document suffix already used, and the KMS-versus-AES256 branch each uploader wrote out per object.
Peer review found the marker's identity was the prefix while its lifecycle was a SourceDocument, and the two are not one-to-one. An auto-tuned run emits one source as several SourceDocuments, which share a prefix. With a fixed marker name the last one written spoke for all of them: two documents where the second lost a chunk left three of four objects in the prefix and a marker reading as complete, which inverts what the marker is for. PR awslabs#518 solved the same collision for the document key by hashing node ids into it; the marker name now does the same. A document with nothing to write no longer creates a prefix holding only a marker. Such a prefix read back as a document with no nodes, whose source_id() is None, and a re-stage cannot build a path from None. The test that asserted the old behaviour was asserting a bug. Encryption was four copies of one branch with no test on any of them, so removing the headers altogether kept the suite green. Both branches and the marker's own encryption are now asserted.
Second review pass caught a regression in the previous fix. Yielding an empty document as soon as it was seen jumped every document already in flight, and upload() promises documents come back in the order they went in. Across randomised interleavings 24 of 30 came back out of order. It now queues like any other document, with no prefix and no futures, so the order holds while nothing is written for it. Verified 0 of 30 out of order after the change. The two encryption tests asserted nothing when the uploader wrote nothing, since both looped over a capture list without checking it was populated. Both now assert the count. Also: the two downloaders read a listing the same way again, and a comment claimed S3DocUploader skips documents with nothing to write, which it does not.
Comments explained the same thing three times over. The rationale for the marker name belongs in the PR, not wrapped around the constant, and a one-line predicate does not need a seven-line docstring. is_completion_marker hand-rolled a basename with rsplit; os.path.basename was already half-imported in this module. Net 25 lines of comment removed, no behaviour change.
noel-improv
marked this pull request as ready for review
September 10, 2026 16:39
acarbonetto
reviewed
Sep 11, 2026
…space Markers sat beside the chunks and were recognised by name, but a chunk is keyed by its node id, so a node id opening with the marker prefix was written as a chunk and then excluded from the reconstructed document with no error. Markers now live under a reserved `_markers` segment and are recognised by that segment rather than by name. No collection has a marker yet, so the layout change costs nothing; the reader in AN-3520 has to look in `_markers`.
noel-improv
force-pushed
the
feat/uploader-completion-marker
branch
from
September 12, 2026 00:39
7a54943 to
f424d0d
Compare
oussamahansal
approved these changes
Sep 14, 2026
8 tasks
This was referenced Sep 16, 2026
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.
Description
Nothing recorded whether a document's chunks all reached S3, so a truncated prefix and a short document read back identically.
S3ChunkUploader._draincaught each chunk's exception, logged it and returned nothing, which #507 documented rather than fixed._drainnow reports whether every upload landed. A document whose chunks all stored gets a marker holding its chunk ids, their count and a hash over them, written last. A document with a failed chunk carries no marker and is still yielded, exactly as today.Related issue: #325.
Changes
_drainreturns whether all of a document's uploads succeeded._write_completion_markerwrites the marker after that check passes. A failed marker write is logged rather than raised. The chunks themselves uploaded, so stopping the run costs more than the re-stage a missing marker causes.TextNode.from_jsonreturns a node with a generated uuid and empty text for a marker rather than rejecting it, so an unfiltered listing would add one extra node to every read of that document.source_id()isNone, and a re-stage cannot build a path fromNone.Three duplications collapsed rather than extended:
written_nodes(doc)_doc_suffixnode_ids_hash()EncryptedPut._putWhy the marker name contains a digest
An auto-tuned run emits one source as several
SourceDocuments, which share a prefix. With a fixed marker name, the last one written replaces the others. With two documents where the second loses a chunk:Three of four objects are present, with a marker that reports the document as complete. #518 addressed the same collision for the document key by including a hash of the node ids; the marker name now does the same.
Five hex characters is 20 bits, the same budget
_doc_suffixuses, and the same population: the SourceDocuments sharing one prefix. Brute-forced against realistic chunk ids, the first name collision appears at 1,672 documents in one prefix, with birthday odds of 0.47% at 100 and 38% at 1,000. So the bound is tens of documents per prefix, which is what the auto-tune path produces when it slices a source intomax_batch_sizebuckets. A name collision loses a completeness record rather than data. The remaining marker still lists one document's chunks, so a reader comparing coverage re-stages rather than reporting the document complete.Testing
pytest)19 new tests. 2154 pass across
tests/unit.Each change was checked by mutation testing, not only by the tests passing. Reverting to a fixed marker name fails 2 tests; removing the
ServerSideEncryptionheaders fails 3; yielding an empty document immediately rather than queueing it fails 2. The encryption check matters because that branch had no assertions at all before this, so collapsing four copies of it was otherwise unverifiable.One pre-existing failure is unrelated and reproduces on a clean tree:
test_integ_dependency_compatibility.pyerrors withNo module named pipin this venv.Checklist
Verified against real S3
Uploaded a two-chunk document to
noel-graphrag-testand read it back:Two things worth your call
Every ingest now writes one extra object per document, and nothing reads it yet. The restart work is expected to be enabled by a setting, but that setting does not exist yet, and adding one with no code reading it would be configuration no caller can use. The write is therefore unconditional. Say if you would rather it waited until the restart setting exists.
The encryption consolidation is separable from the feature. Reverting the marker would revert it too. I folded it in because writing the marker otherwise meant a fifth copy of the same KMS branch, but it touches
S3DocUploader, which this feature does not otherwise change. Happy to split it into its own PR.Known limitations
Nothing reads the marker yet, so
chunk_ids,countandcontent_hashare stored and never validated. Read behaviour is deliberately out of scope here.The marker name is the first five characters of the
content_hashin its own body, so the two carry the same value at different precision. The name has to be short; the body field is what the acceptance criteria ask for.A stale marker from an earlier run is not deleted. With per-document names a re-chunked document writes a different name and leaves the old one, which a reader catches by comparing the hash against a live listing. That comparison belongs with the read path.
S3DocUploaderwrites no marker, butS3DocDownloaderskips one. Both uploaders write into the same prefix shape, so the skip is defensive rather than dead.The marker PUT runs on the consumer's thread rather than the pool, adding one serialized round trip per document. The drain already blocks that thread, so this is additive rather than a change in shape.