Skip to content

chore: retire the multipart upload token from the staged ledger - #593

Merged
wchwawa merged 2 commits into
mainfrom
chore/retire-multipart-token
Oct 26, 2026
Merged

wchwawa merged 2 commits into
mainfrom
chore/retire-multipart-token

Conversation

@wchwawa

@wchwawa wchwawa commented Oct 26, 2026

Copy link
Copy Markdown
Member

TLDR

The multipart upload token leaves the wire, the durable staged-object record, the lifecycle cleaner, the Python inspection rows, and the docs, together with the Uploading provider state that nothing ever wrote. The staged-object record gets its own value version so a store written before this change is refused instead of misread.

Problem

StagedObject.multipart_token and StagedObjectRecord.multipart_upload_id were reserved for a provider multipart path that never existed: every producer set them to None, the lifecycle cleaner refused any request that carried one as ambiguous because no provider exposes a multipart abort, and StagedProviderState::Uploading was never written by any code path. Since the owner-mediated upload path landed (#586, the R5 slice of #566), the owner writes every block with one create and the token has no possible future either. This is the legacy cleanup item of R5 under #566; no separate issue exists.

Consequences

Every reader of the staged ledger, in Rust, in the Python SDK, in the acceptance gate, and in the schema docs, carried a field and a state that could only be None and never. Validation code, a proof-digest branch, a codec branch, and a refusal path in the cleaner existed to handle values no producer could create, and each new reader had to learn why they were there.

Example

Before this change Client.operation_inspect returned entries like {"sequence": 0, "object_identity": ..., "expected_length": 4194304, "expected_digest": "sha256:...", "multipart_token": None} and the acceptance gate asserted the last key was None; the lifecycle cleaner's proof digest hashed a presence byte for the absent id. After it the entry has the four fields that carry information, the gate checks those, and the proof digest covers the purpose, the key, and the disposition.

Solution

Remove the wire field and its size limit from StagedObject, the durable field and its limit from StagedObjectRecord, the Uploading state from the durable provider-state registry and the transition tables, the multipart branch from the lifecycle cleanup request, its proof digest, and its refusal, and the row key from the Python inspection result; reword the docs. The staged-object record now carries STAGED_OBJECT_VALUE_FORMAT_VERSION 8 while the shared PUBLISH_VALUE_FORMAT_VERSION stays at 7, because clients seal manifest rows against the shared encoding and the manifest row did not change.

Gain

One fewer field on every staged ledger surface, three dead codec helpers and one dead refusal path gone, and a proof digest and a transition table that describe only states the system can reach. The protocol's fused v15 golden and the record golden change with the encoding, on purpose.

Tradeoffs

A store holding version 7 staged-object records is refused when one is read, which only affects stores written by unreleased builds since the v15 cut, consistent with the decision that format 15 ships without a migrator. The wire change is breaking within the unreleased v15 protocol: a client built before it cannot stage objects against this owner.

Validation

  • cargo fmt --all -- --check and CI's five clippy combinations (--workspace --all-targets, nokv-meta/nokv-bench with metadata-read-stats, nokv-meta-holt with read-stats, nokv-server and nokv-bench with restore-crash-test-support), all with -D warnings on Rust 1.99.1: clean.
  • cargo test on the touched crates: nokv-types 189, nokv-protocol 74 + 1 doc, nokv-meta 398, nokv-server 229, nokv-client 153, nokv 19 passed, 0 failed; cargo test --workspace --no-fail-fast: every target passed.
  • Goldens updated on purpose: the protocol's fused v15 request digest, the staged-object record golden bytes, and the lifecycle cleanup proof digest.
  • Not run locally: the restore composition and other Docker-backed live gates (the Docker daemon on this machine stopped answering after a full-disk episode); the required CI checks run them. The Python contract gate was not run: the only SDK change removes a key from operation_inspect rows, and no Python test referenced it.

StagedObject.multipart_token and StagedObjectRecord.multipart_upload_id were
reserved for a provider multipart path that never existed: every producer
set them to None, the lifecycle cleaner refused any request carrying one as
ambiguous because no provider exposes a multipart abort, and the Uploading
provider state was never written. Now that the owner writes every block
with one create, the token has no possible future either.

Remove the wire field and its limit, the durable field and its limit, the
Uploading state from the registry and the transition tables, the multipart
branch from the lifecycle cleanup request, its proof digest and its
refusal, and the row key from the Python inspection result; reword the
docs. The staged-object record moves to its own value version 8 while the
shared publish value version stays at 7: clients seal manifest rows against
the shared encoding, and the manifest row did not change. A version 7
staged-object record is refused instead of read with its bytes shifted.

Signed-off-by: wchwawa <wch19961116@gmail.com>
@wchwawa
wchwawa requested a review from feichai0017 as a code owner October 26, 2026 06:41
Signed-off-by: wchwawa <wch19961116@gmail.com>
@wchwawa
wchwawa merged commit 05051f2 into main Oct 26, 2026
10 checks passed
@wchwawa
wchwawa deleted the chore/retire-multipart-token branch October 26, 2026 07:02
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