fix: Unit 8 — Pub protocol semantics (P-03, P-04, P-07)
The publish path's decided-but-unimplemented behaviors land: per-chunk
schema validation, initiator call.error terminates the stream, and an
early handler return short-circuits the feed. alktype::validation::
build_validator is now used on both dispatch paths (the dependency was
declared and unused); jsonschema was added as a direct dependency
because its Validator type is part of alktype's public API.
P-03 — OperationSpec::publish_schema was declared, had a builder,
defaulted to None, and was never read. It now round-trips through
discovery: spec_to_json emits "publish_schema" when Some,
operation_spec_schema() advertises the property, and rebuild_spec_for
parses it back (null/absent treated as None, so an imported Pub op no
longer loses the schema). Both dispatch paths (pump_sink and
run_loop_single_stream) validate each call.published chunk's input
against the schema before yielding it to the SinkHandler; on
validation failure an Err(CallError::invalid_input(...)) with the
offending chunk in `details` is injected and the feed terminates,
matching the SinkHandler contract that an Err terminates the stream.
The validator is built once at dispatch time and carried in the
SinkDispatch (and, for the single-stream path, in a new InFlightSink
struct alongside chunk_tx); a schema that fails to compile logs a
warn and falls back to no validation. When publish_schema is None,
chunks are yielded as-is (unchanged behavior).
P-04 — pump_sink matched only call.published/call.completed/call.
aborted and dropped everything else into a debug-log ignore branch; a
call.error from the initiator was silently ignored. Both dispatch
paths now match call.error, parse the CallError from the payload,
inject it as Err(call_error) into the sink's chunk_tx, and terminate
the feed — the handler sees the initiator's error, not a synthetic
"aborted" message. A malformed payload falls back to
CallError::internal("publish error from initiator (malformed)").
P-07 — pump_sink used tokio::join!(feed_fut, handler) and only wrote
the response after both completed; an early-returning handler (e.g.
rejects after chunk 1) had its response deferred until the initiator
finished publishing or the next chunk_tx.send failed. Replaced with a
tokio::select! loop over handler.fuse() and reader.read_frame(): when
the handler completes first, the response is captured and the loop
breaks immediately, the feed is short-circuited (chunk_tx dropped,
the handler's PublishStream sees EOF), and the response is written
without waiting for the feed. The feed-wins branch (natural end /
abort / error / read failure) awaits the handler after the loop as
before. This removes the unbounded stall on a path ADR-046 intends
for long-lived streams.
Tests (15 new, 480 total):
- discovery: spec_to_json emits/omits publish_schema; operation_spec_
schema documents the property (3).
- from_call: rebuild_spec_for parses publish_schema (present/omitted/
null) and round-trips with spec_to_json (4).
- dispatch (stream-per-request): publish_schema rejects invalid chunk
+ passes valid chunks + no-schema yields as-is (3); initiator
call.error terminates with the initiator's error + malformed
fallback (2); early handler return short-circuits a slow feed and
the response is not deferred (1).
- channels/client (single-stream): publish_schema rejects invalid
chunk over channel 0; initiator call.error terminates the publish
over channel 0 (drives run_loop_single_stream directly with crafted
frames since the public publish() API takes Stream<Item = Value> and
cannot emit an initiator call.error) (2).
Verification:
- cargo test → 480 passed, 0 failed (was 465; +15 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 0 warnings
This commit is contained in:
1 parent
da12d65a03
commit
f362c9e596
7 files changed
+994
-55
No files matched your search
@@ -127,6 +127,13 @@ publish should register like `subscribe` with `timeout: None`, not the
|
||||
|
||||
## P-03 [major] — `publish_schema` per-chunk validation is written but never read
|
||||
|
||||
**Fixed (2026-08-13, Unit 8).** `publish_schema` now round-trips through
|
||||
`spec_to_json` / `operation_spec_schema()` / `rebuild_spec_for`, and both
|
||||
dispatch paths (`pump_sink` and `run_loop_single_stream`) validate each
|
||||
`call.published` chunk's `input` against the schema via
|
||||
`alktype::validation::build_validator`; a validation failure injects
|
||||
`Err(CallError::invalid_input(...))` and terminates the stream.
|
||||
|
||||
**ADR drift:** ADR-046 §4, and the field's own doc comment at
|
||||
`src/registry/spec.rs:181-186` which says "the dispatch path validates
|
||||
each `call.published` event's `payload.input` against this schema
|
||||
@@ -144,6 +151,12 @@ documented-but-unimplemented behavior.
|
||||
|
||||
## P-04 [major] — Initiator `call.error` does not terminate the publish stream
|
||||
|
||||
**Fixed (2026-08-13, Unit 8).** Both `pump_sink` and
|
||||
`run_loop_single_stream` now match `call.error`, parse the `CallError`
|
||||
from the payload, inject it as `Err(call_error)` into the sink's
|
||||
`chunk_tx`, and terminate the feed. A malformed payload falls back to
|
||||
`CallError::internal("publish error from initiator (malformed)")`.
|
||||
|
||||
**ADR drift:** ADR-046 §5 event table and §6 ("`Err(call_error)` for an
|
||||
initiator-side error (a `call.error` event from the initiator)"); plus
|
||||
the `SinkHandler`/`PublishStream` doc contract
|
||||
@@ -204,6 +217,13 @@ multiplexed on that stream.
|
||||
|
||||
## P-07 [major] — Handler-returns-early: response withheld until the initiator finishes
|
||||
|
||||
**Fixed (2026-08-13, Unit 8).** `pump_sink` now uses
|
||||
`tokio::select!` over `handler.fuse()` and `reader.read_frame()`; when
|
||||
the handler completes first, the response is written immediately and
|
||||
the feed is short-circuited (the loop breaks, `chunk_tx` is dropped,
|
||||
the handler's `PublishStream` sees EOF). The feed-wins branch awaits
|
||||
the handler after the loop as before.
|
||||
|
||||
**ADR drift:** ADR-046 §7 (long-lived publish streams).
|
||||
**Verified:** YES. `pump_sink` uses `tokio::join!(feed_fut, handler)`
|
||||
and only writes the response after **both** complete
|
||||
@@ -1101,6 +1121,21 @@ ALPN.
|
||||
|
||||
## Unit 8 — Pub protocol semantics (P-03, P-04, P-07)
|
||||
|
||||
**Status: complete (2026-08-13).** All three findings fixed; 15 new
|
||||
tests (7 in `dispatch.rs`, 4 in `discovery.rs`, 4 in `from_call.rs`…
|
||||
the single-stream P-03/P-04 tests live in `channels/client.rs`); full
|
||||
suite 480 passed, clippy/fmt/doc clean. `alktype::validation::build_validator`
|
||||
is now used on both dispatch paths (`pump_sink` and
|
||||
`run_loop_single_stream`); `jsonschema` was added as a direct
|
||||
dependency (the validator return type is part of `alktype`'s public
|
||||
API). The `SinkDispatch` struct gained a `publish_validator` field
|
||||
built once at dispatch time; the single-stream `in_flight_sinks` map
|
||||
gained an `InFlightSink` struct carrying the validator alongside
|
||||
`chunk_tx`. The P-07 rework replaced `tokio::join!` with a
|
||||
`tokio::select!` loop over `handler.fuse()` and `reader.read_frame()`
|
||||
so an early handler return writes the response immediately without
|
||||
waiting for the feed.
|
||||
|
||||
**Goal:** the publish path's decided-but-unimplemented behaviors land:
|
||||
per-chunk schema validation, initiator `call.error` terminates the
|
||||
stream, and an early handler return short-circuits the feed.
|
||||
|
||||
Reference in new issue
Block a user