feat(ops): blobs/fetch — the verified Sub op and the fanout seam's consumer side (wave 7)

- the open triple (fetch_openables): the channels/blobs/sub spec with
  the channel_open marker on the alk/blobs ALPN (read action,
  closed-by-default), the awaited establisher whose reply fields carry
  the ack {source, len?, slice_digests?}, and the pump handler over
  the channel's stream (bounded read windows; the fanout feed
  forwarded chunk-for-chunk)
- verified streaming per ADR-006: store-first consult, a live
  broadcast taps (the tap-time-set discipline documented; the
  re-fetch is the partial-feed degrade), the ranged form carries
  read_range's slice-digest convention over exactly the served bytes
- the miss maps onto ADR-049's vocabulary (channel:open_failed,
  reason unknown_resource, the digest in the message; the declared
  BLOB_MISSING contract disclosed on the spec); unverifiable reads
  refuse with handler_error, never stream
- the producer-side design point decided and recorded against the A3
  text: fetches never produce read-side broadcasts (module doc,
  op doc, ops-surface.md)
- five harness tests through the channel halves: the verified whole
  round-trip + consumer hash-check, ranged + ack digests, the
  live-broadcast tap served without a commit, the late joiner served
  by the store path, the typed miss, the closed-by-default denial

Verification: cargo test (default) + --features ops + --all-features
green; clippy -D warnings on all three shapes; fmt --check clean;
cargo doc --no-deps clean on both shapes.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 00:43:52 +00:00
1 parent 5144b6a733
commit d927da2ee5
4 files changed
+954 -24

No files matched your search

+96 -3
View File
@@ -1,7 +1,7 @@
---
id: ops-fetch
name: blobs/fetch — the verified Sub op and the fanout seam's consumer side
status: pending
status: completed
depends_on: [ops-foundation, ops-call-harness]
scope: moderate
risk: medium
@@ -94,8 +94,101 @@ out** over the binary data channel.
## Notes
> To be filled by implementation agent
Implementation agent, 2026-10-13. The op landed as `fetch_openables(ctx)
-> (OperationSpec, OpenEstablisher, OpenHandler)` in `src/ops.rs` —
the `Sub` open triple the embedder assembles through
`ChannelCore::register_openable_with_establisher` (spec
`channels/blobs/sub`, `OperationType::Sub`, `channel_open` marker on
the `alk/blobs` ALPN `BLOBS_CHANNEL_ALPN`; the standard shape, so the
ALPN derives from the name and no explicit `channel_open_alpn` rides).
Exports: `fetch_openables`, `BLOBS_CHANNEL_ALPN`, `FETCH_OP_ID`.
Design points decided and recorded (op doc + the module doc's fetch
section + ops-surface.md's fetch bullet):
- **The ack rides ADR-049 amendment 3's establishment reply fields**:
the open reply merges `{channel_id, source, len?, slice_digests?}`.
`source` names the serving path (`store`/`fanout` — the test
criterion "asserted by which path served" is wire-visible, and a
consumer knows whether the stream is a live put's tail); the whole
form acks `{source: "store", len}` or `{source: "fanout"}` (no
length on a tapped broadcast — the hash-check runs at stream end);
the ranged form acks `{source: "store", slice_digests[]}` —
read_range's convention verbatim, one per range in request order,
over exactly the served bytes (over-reads clamp; the digest covers
the clamp).
- **The establisher's consult order**: store `get` first (a commit
that raced the broadcast's end is the same bytes under CAS; the
Blob opens into the plan at establishment, closing the
establish→pump race window), then the fanout subscribe (a live
broadcast taps), then the miss. The ranged form serves
`read_range` on the committed entry only — a not-yet-committed
digest's ranged fetch is the miss (ranges are the committed entry's
shapes; no tap-window feed serves them). The ranged form's reads
happen at establishment because the slice digests must ride the
ack, which lands before the pump runs.
- **The miss maps onto ADR-049's established vocabulary** — the only
op-plane failure surface an open op has: the establishment phase
refuses with reason `unknown_resource` ("the requested resource
does not exist"), the digest in the refusal's message; the
declared `BLOB_MISSING` ErrorDefinition (same code + digest-details
schema the JSON ops declare) is the op's disclosed missing-error
contract. `channel:open_failed` + `details.reason` is what a
consumer branches on; a phantom channel with an in-band error frame
(the ADR-049 antipattern) is never reachable. The same closure
covers the refusal-to-serve-unverifiable shape: a store-core error
mid-establishment (including a verification-failed read) refuses
with reason `handler_error` rather than streaming unverified bytes.
- **The producer-side design point (decided, against the A3 text)**:
fetches do NOT produce read-side broadcasts — the put handler's
stream reader is the canonical producer, concurrent fetchers of one
stored digest each ride their own facade read. Recorded in the
module doc's fanout section (a named design point paragraph), the
op doc, and ops-surface.md's fetch bullet. Rationale: A3's fanout
is the put stream's seam (its late-joiner note degrades to get,
never get-then-rebroadcast); a read-side broadcast adds a second
producer class with no lifecycle owner (its end would be an
arbitrary policy, its arms hold memory where the large tier exists
to avoid materializing), for a concurrency case bounded by the put
window the put-side broadcast already covers; under CAS every
per-fetcher read returns identical bytes.
- **Slow-subscriber policy stays producer-owned** (per-broadcast, set
by the put at `begin` — the foundation's registry semantics); a
fetch tap rides the broadcast's policy. The tap-time-set discipline
(a mid-put tap has no prefix) is documented on the op + registry:
the consumer's hash-check on a partial feed fails and the re-fetch
is the degrade, identical bytes post-commit.
- **A mid-stream note for the whole form**: between a store-hit
establishment and the pump, the entry cannot vanish (the Blob read
is already open; committed entries are immutable), so the ack's
`len` is trustworthy; a mid-stream store/transport failure ends the
pump (teardown closes the stream), the consumer's hash-check fails,
and the re-fetch is the degrade.
Verification: `cargo test` (default) green, `cargo test --features ops`
green (the five harness tests: verified whole round-trip + consumer
hash-check; ranged + ack slice digests vs `read_range`; live-broadcast
tap served without a commit; late joiner served by the store path; the
typed miss; the closed-by-default read denial), `--all-features` green,
clippy `-D warnings` on all three shapes, `cargo fmt --check` green,
`cargo doc --no-deps` clean on both shapes (the private
`FETCH_WINDOW` doc link de-linked, the PGLO-1 class). The pure
`--no-default-features --features ops` build fails on main already
(engine-typed match arms upstream of this task, unreachable without an
engine feature) — pre-existing, not this task's regression.
## Summary
> To be filled on completion
`blobs/fetch` landed complete: the verified `Sub` op as the
`fetch_openables` open triple (`channels/blobs/sub` + the
`channel_open` marker on `alk/blobs`; read-gated, closed-by-default),
the establishment ack in ADR-049's reply fields
(`{source, len?, slice_digests?}`), whole-blob consumer-side
verification per ADR-006, the ranged form carrying `read_range`'s
slice-digest convention, the fanout seam's consumer side (live tap /
late-joiner store degrade, the path named in the ack), the miss as the
typed missing-error refusal (BLOB_MISSING disclosed; the wire shape is
ADR-049's `unknown_resource`), and the producer-side design point
decided and recorded against the A3 text (fetches never produce
read-side broadcasts). All acceptance criteria verified; harness tests
green; all gates green on both feature shapes.