- `put(len: Option<u64>, stream: impl AsyncRead + Unpin + Send) -> (Key, Pin)`: known-length one-pass flow; unknown-length buffer-to-threshold flow with the mid-stream fresh-stage flush (ADR-003 option (d), three numbered rules: flush-and-continue, derivation over the eventual stored artifact as a whole via buffer form and StagingCursor read-back form, zero residue on stream error) - staged-put seam amended (operator sign-off): `put_staged()` keyless handout, `StagingCursor::commit(self, key)` publishes under the store-core-resolved key — the canonical key completes at stream end and the wave-1 handout shape could not express the commit-rename flow - pin-before-publish rides the arbitration lock on both paths and both tiers; dedup re-puts refcount the existing entry - Batch seam: `Store::batch()` + `put_in_batch()`, members publish pin- before-publish, group pins drop together on `Batch::commit` (new public verb) or drop - kv-only rejection (ADR-012 §6.2): known-length over-threshold rejects immediately (stream never read), unknown-length at mid-stream overflow; zero residue; error carrier `Io(String)` (named design point) - declared-len mismatch rejects `Verification` zero-residue (named design point); stream shape generic-no-dyn (named design point) - incremental BlobHasher (pinned byte-equal to Algorithm::derive); AnyStaging cursor dispatch; git-interop put-path leg against git hash-object; eleven put-path behavior tests Verification: cargo test (default/--features mem/--all-features) green; clippy --all-targets -D warnings green in all three feature shapes; cargo fmt --check green; cargo doc --no-deps at the 24-warning baseline.
11 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| store-put-paths | Store put seam — two internal paths, the three numbered rules, Batch, kv-only rejection | completed |
|
moderate | medium | component | implementation |
|
Description
Implement the facade's put seam on the Store type (store-api.md
§Write path): put(len: Option<u64>, stream) → (Key, Pin) — two
internal paths behind one signature, with pin-before-publish from the
gc-protocol registry.
- Known-length path (encouraged, documented as such on the API):
the preamble (
"blob <len>\0") is known before content flows, so the hash initializes with it and one pass suffices — no staging needed for hashing. Routing by the dispatch seam: ≤ threshold → the kv tier's whole-value put; > threshold → the large tier's staged put (content streams into the staging cursor while hashing; the derivation completes at stream end; commit-rename publishes). - Unknown-length path: content buffers in memory up to the
dispatch threshold. ADR-003's three numbered rules (option (d)),
each a test-asserted behavior:
- Threshold exceeded mid-stream (non-restartable sources are normal): the buffered prefix flushes into a fresh stage file and the stream continues appending into it — there is no "restart the stream" requirement anywhere.
- Verification runs over the eventual stored artifact as a
whole: the memory buffer for the sub-threshold kv commit; the
complete staged bytes for the large commit — the derivation pass
restarts over preamble + staged bytes (forced by the preamble
being inside the hashed input, ADR-002), reading the staged
artifact back through the
StagingCursor: ReadCursorseam (the wave-1 design-led extension). There is no stitched-input case left ambiguous. - Stream error before commit: no entry lands and no stage file remains — buffer discard on the failure path, exactly like stage discard (the stage-hygiene invariant extends to the buffered case).
- Pin-before-publish (ADR-005): the pin refcount is acquired under
the arbitration lock before the entry becomes visible — publish
and protection are one step; an entry cannot appear in a sweep's
list()without already carrying its pin. - Dedup: a re-put of existing content acquires no new pool entry —
the returned
Pinrefcounts the existing one under the same lock. Batch(store-api.md §Write path): the batching put seam — multi-put writes group under one batch-scoped pin; member puts publish pin-before-publish exactly as lone puts do; the batch's pins drop together on commit or drop (thegc-protocolmachinery drives).- kv-only mode rejection (ADR-012 §6.2): known-length puts exceeding the threshold reject immediately; unknown-length puts reject at mid-stream threshold overflow — lengths are not known at construction, so put time is the only possible rejection point. Zero residue on either rejection.
- Named design points: (a) the stream parameter shape (
impl AsyncRead + Unpin + Sendvs a boxed trait object — the family's no-dyn posture vs ergonomics); (b) the declared-len mismatch case (a known-length stream that yields fewer/more bytes thanlen— which error family member carries it, and zero residue either way). Record both for the review gate.
Acceptance Criteria
put(len, stream) → (Key, Pin)with both internal paths behind the one signature; routing is the pure length function (no content ever migrates tiers)- The three numbered rules each behavior-tested: mid-stream overflow flushes into a fresh stage and the stream continues; the derivation pass runs over the eventual stored artifact as a whole (buffer form and staged-bytes form); stream error leaves no entry and no stage file
- Pin-before-publish holds on both paths and both tiers; dedup re-puts refcount the existing entry
Batchgroups multi-put writes; member pins drop together- kv-only mode: known-length over-threshold rejects immediately; unknown-length rejects at mid-stream overflow; zero residue on both
- Stream-shape and len-mismatch design points decided and recorded
cargo testgreen;cargo test --features memandcargo test --all-featuresgreen; clippy/fmt gates green
References
- docs/architecture/store-api.md (§Write path — the put seam, stage hygiene invariant)
- docs/architecture/decisions/003-backend-contract-and-dispatch.md (§Dispatch policy — the three numbered rules, option (d))
- docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §6.2 (kv-only put-time rejection)
- docs/architecture/decisions/005-pooled-cas-and-gc-mechanism.md (pin-before-publish, dedup refcount)
- docs/architecture/decisions/010-backend-io-seams-and-fleet-state-home.md §1 (the staged-put contract)
- src/backend.rs (StagingCursor: ReadCursor — the verification-pass seam)
Notes
- The staged-put seam was amended: the key resolves at commit, not
at handout. Implementing the two large-tier flows surfaced a
contradiction in the wave-1 pin:
put_staged(key)hands the staging cursor out with the key, but both large-tier put paths only know the canonical key when the derivation completes — at stream end (the preamble covers a length, and the digest covers content that has not streamed yet). Content streams once (non-restartable sources are the normal case), socommit(self)publishing under the handout key cannot express the specced flow ("the derivation completes at stream end; commit-rename publishes"). Amended with operator sign-off:put_staged()is a keyless handout andStagingCursor::commit(self, key)publishes under the store-core-resolved key (CAS first-writer-wins as the whole-value put). No ADR text pins the key's arrival point (ADR-003/010 pin handout/commit/discard mechanics only); the wave-1 task notes'put_staged(key)shape was pre-flow realism. Recorded for review-wave-2. Engines updated in place (mem, local, the wave-1 test double) with their hygiene suites re-run green. - Put-path named design points (recorded in
src/store.rsmodule docs, for review-wave-2):- Stream parameter shape: `put(len, stream: impl AsyncRead + Unpin
- Send)
— generic, no boxing; the family's no-dynposture carried to the write seam (a boxed reader is itselfAsyncRead + Unpin + Sendunder tokio's blanket impl, so callers needing erasure box at their call site).Sendkeeps spawn-site bounds resolvable for the ops wave; no'static` (nothing spawns).
- Send)
- Declared-len mismatch: a known-length stream yielding fewer or
more bytes than
lenrejectsError::Verificationon either tier, zero residue either side — the preamble carries the declared length, so a mismatched artifact's canonical address can never equal the stored key (storing it would break pool integrity by construction;Verificationis the put-path content-≠-key family member, ADR-006's whole-blob posture applied at put time). - kv-only rejection carrier (a named decision beyond the two the
task listed):
Error::Io(String)with a descriptive message — the ADR-pinned family (ADR-012 §6.5) holds no dedicated variant and the rejection is an operational/capability fact about the medium posture, the bucketIo(String)covers. A dedicated variant for typed matchability would be a new failure family — a new ADR.
- Stream parameter shape: `put(len, stream: impl AsyncRead + Unpin
- Flows as implemented (
src/store.rs, write-pathimpl Store): known-length initializes the derivation with the preamble before content flows and streams once (kv: buffer + hash in one pass → mint pin → whole-value put; large: stream into the staging cursor while hashing → mint pin →commit(key)). Unknown-length buffers to the threshold; a mid-stream overflow (strictly past it — exactly-at stays kv) flushes the buffered prefix into a fresh stage and the stream continues into it; at EOF the derivation pass runs over the eventual stored artifact as a whole — the buffer (kv form) or preamble + staged bytes read back through theStagingCursor: ReadCursorseam (staged form) — then pin, then commit. Every failure path (read error, mismatch, under-read) discards: no entry, no stage file. - API shapes:
Store::put(&self, len: Option<u64>, stream) → Result<(Key, Pin), Error>; the batching seam isStore::batch() → Batch+Store::put_in_batch(&mut Batch, len, stream) → Result<Key, Error>(members run the lone-put flow; the minted guard is contributed to the group via crate-internalBatch::add); a publicBatch::commit()was added consuming the batch with the same one-step release as drop (store-api.md's "pins drop together on batch commit or drop", as a named verb).hashing.rsgained crate-internalBlobHasher— the same derivation driven incrementally, pinned byte-equal toAlgorithm::deriveby tests (chunk-boundary invariance, both algorithms, the git vectors). - git-interop leg added (
tests/git_interop.rs): the put seam keys entries git-identically through both put paths on both tiers against livegit hash-object(INV-1's source-of-truth posture extended from the derivation constructor to the facade seam). - Wave-1 note updates:
put_staged/commitdoc text inbackend.rs,mem.rs,local.rsnow carries the key-at-commit design point; the store.rs module doc's remaining-deferral note shrunk to the actual remaining gaps (fleet clauses, wave-3 kv engine).
Summary
Implemented the facade's put seam (src/store.rs): put(len: Option<u64>, stream: impl AsyncRead + Unpin + Send) → Result<(Key, Pin), Error> with the two internal paths behind the one
signature — the known-length one-pass flow and the unknown-length
buffer-then-maybe-flush flow implementing ADR-003's option (d) three
numbered rules — plus batch()/put_in_batch() (ADR-005 §Marks 2's
batch-scoped pin group) and kv-only's put-time rejection at its two
pinned points (ADR-012 §6.2). Pin-before-publish rides
gc().acquire under the arbitration lock before both tiers'
publishes; dedup re-puts refcount the existing entry; every failure
path converges on discard with zero stage files. The staged-put seam
was amended (keyless handout; commit(self, key)) after operator
sign-off — the one blocking finding of the task. Supporting changes:
staging-cursor dispatch enum AnyStaging beside the read path's
AnyCursor; incremental BlobHasher in hashing.rs; public
Batch::commit; a Pin/Debug impl; the git-interop put-seam leg.
Eleven put-path tests in src/store.rs (plus four fixtures) pin the
three rules, both-tier round-trips, the threshold boundary, pin-
before-publish under a racing sweep, dedup refcounting across paths
and tiers, batch grouping/commit/failure, kv-only's two rejection
points, len-mismatch zero-residue, mem-only's threshold-free posture,
and the empty-content degenerate.
Verification: cargo test (default), cargo test --features mem,
cargo test --all-features all green (97 lib tests under mem plus
conformance and git-interop); cargo clippy --all-targets -- -D warnings green in all three feature shapes; cargo fmt --check
green; cargo doc --no-deps at the pre-existing 24-warning baseline.