test(flybus): prove the two section 9 rows a review found cited but unproven
An independent review of 4f44894 accepted the audit with two required evidence
changes, neither of which needed a code change.
"Classification is not a topic-name heuristic" cited a lane-order test that
never varies a topic name. The row is now conforms-by-construction, naming the
four scheduler functions that read only the queue an item sits in, and
a_topic_named_like_a_notice_is_still_classified_as_topic_data publishes on
topics called call.failed, route.removed and subscription.closed and asserts
each arrives as a topic.message delivery with its payload intact.
"A latest subscriber never causes BACKPRESSURE" cited a bounded-mode test and
the ignored measurement. a_latest_subscriber_never_refuses_a_publication floods
one unconsumed latest slot with 100 publications of 60 KB, six times the
per-client bounded byte pool, and asserts none is refused, 98 coalesce, the
queue stays at one and the credit never comes back. That row also underwrites
the byte-budget deviation and the first contradiction.
Both tests run over both transports.
The coordinator resolved both contradictions in the spec rather than the code,
so bus-v1 carries them as dated amendments with one line of reason each: the
section 9 table row becomes "Per-client ordinary bounded queued envelope bytes"
with a new "Latest subscription slots | subscriptions x 64 KiB" row, and the
section 2 sketch drops the budget argument and shows the deadline at the caller.
With the amended wording the byte-budget row conforms, so the report's counts
are 178 / 9 / 1 fixed / 7, and the crate README's differences 4 and 8 say they
no longer differ.
This commit is contained in:
parent
4f44894ede
commit
c3bda90b82
4 changed files with 142 additions and 21 deletions
|
|
@ -14,13 +14,13 @@ Every normative sentence of bus-v1 sections 2 to 11 gets a row. Four statuses:
|
||||||
| deviates-must-fix | It differs and the draft requires otherwise. |
|
| deviates-must-fix | It differs and the draft requires otherwise. |
|
||||||
| not-implemented | Not built yet; the row names who owns it. |
|
| not-implemented | Not built yet; the row names who owns it. |
|
||||||
|
|
||||||
Counts over 195 rows: **conforms 177, deviates-allowed 10, deviates-must-fix 1 (fixed),
|
Counts over 195 rows: **conforms 178, deviates-allowed 9, deviates-must-fix 1 (fixed),
|
||||||
not-implemented 7**. The audit found the one deviates-must-fix — connection teardown could be
|
not-implemented 7**. The audit found the one deviates-must-fix — connection teardown could be
|
||||||
starved for the length of a whole frame by the writer it was waiting for — and it is fixed on
|
starved for the length of a whole frame by the writer it was waiting for — and it is fixed on
|
||||||
this branch, so the row for it now reads conforms and records the fix (section 9, "cannot be
|
this branch, so the row for it now reads conforms and records the fix (section 9, "cannot be
|
||||||
starved"). Two contradictions inside the draft are recorded at the end and left alone.
|
starved"). Two contradictions inside the draft are recorded at the end and left alone.
|
||||||
|
|
||||||
Test names below are the functions in `services/flysim/crates/flybus/tests`, 235 of them in
|
Test names below are the functions in `services/flysim/crates/flybus/tests`, 239 of them in
|
||||||
this branch (`cargo test -p flybus`), plus the ignored measurement. Everything marked
|
this branch (`cargo test -p flybus`), plus the ignored measurement. Everything marked
|
||||||
"(both)" is generated twice by the `both_transports!` macro, once over the in-memory transport
|
"(both)" is generated twice by the `both_transports!` macro, once over the in-memory transport
|
||||||
and once over a Unix socket, so `tests/rpc.rs::request_reply_roundtrip` means
|
and once over a Unix socket, so `tests/rpc.rs::request_reply_roundtrip` means
|
||||||
|
|
@ -245,7 +245,7 @@ and once over a Unix socket, so `tests/rpc.rs::request_reply_roundtrip` means
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Every default of the table, exactly: clients/services/topics 64/256/512; subscriptions 128 per client and 1024 total; control envelope 64 KiB; active calls 64; service queued/in-flight 16/16; latest 1/2; bounded 64/16; active owners 256; store 512 MiB and 128 MiB per object; per-client queued envelope bytes 1 MiB; reserved lane 128 frames and 1 MiB | conforms | `limits.rs::Limits::default`, `wire.rs::MAX_ENVELOPE_BYTES` | `tests/conformance_wire.rs::hello_reports_the_contract_digest_and_valid_limits` (both), `tests/pubsub.rs::topic_and_retention_quotas` (both) |
|
| Every default of the table, exactly: clients/services/topics 64/256/512; subscriptions 128 per client and 1024 total; control envelope 64 KiB; active calls 64; service queued/in-flight 16/16; latest 1/2; bounded 64/16; active owners 256; store 512 MiB and 128 MiB per object; per-client queued envelope bytes 1 MiB; reserved lane 128 frames and 1 MiB | conforms | `limits.rs::Limits::default`, `wire.rs::MAX_ENVELOPE_BYTES` | `tests/conformance_wire.rs::hello_reports_the_contract_digest_and_valid_limits` (both), `tests/pubsub.rs::topic_and_retention_quotas` (both) |
|
||||||
| Limits are configured explicitly and a configuration the router could not honour is refused | conforms | `limits.rs::Limits::validate`, `router/mod.rs::Router::new` | `tests/rpc.rs::active_call_limit` (both), `tests/artifacts.rs::quotas_are_enforced` (both) |
|
| Limits are configured explicitly and a configuration the router could not honour is refused | conforms | `limits.rs::Limits::validate`, `router/mod.rs::Router::new` | `tests/rpc.rs::active_call_limit` (both), `tests/artifacts.rs::quotas_are_enforced` (both) |
|
||||||
| The per-client queued envelope byte budget counts `bounded` subscriptions only | deviates-allowed: this section's own "latest spectator subscriptions cannot hold a required session transaction indefinitely" forbids a latest subscriber from causing a rejection, so its slot cannot sit in a budget whose overflow rejects publications. The latest queue is instead bounded structurally at one envelope per subscription (section 7), so the worst case is subscriptions x 64 KiB. See contradiction 1 | `limits.rs::max_queued_bytes_per_client`, `router/state.rs::op_publish` | `tests/sol_review_races.rs::{retained_replay_obeys_bounded_queue_byte_quota, latest_replay_remains_bounded_outside_the_bounded_byte_pool}` |
|
| The per-client queued envelope byte budget counts `bounded` subscriptions only, and latest slots are bounded at subscriptions x 64 KiB instead | conforms to the amended table. The draft's single row was the audit's contradiction 1: this section also forbids a latest spectator from being the reason a publication is refused, so its slot cannot sit in a budget whose overflow rejects one. The coordinator amended the row and gave latest slots their own (bus-v1 section 12, 2026-09-22) | `limits.rs::max_queued_bytes_per_client`, `router/state.rs::op_publish` | `tests/bus_acceptance.rs::a_latest_subscriber_never_refuses_a_publication` (both), `tests/sol_review_races.rs::{retained_replay_obeys_bounded_queue_byte_quota, latest_replay_remains_bounded_outside_the_bounded_byte_pool}` |
|
||||||
| Reserve an owner allowance for lifecycle and results separately from ordinary telemetry | conforms | `limits.rs::reserved_owners_per_client`, `router/state.rs::{ordinary_budget_left, dispatch_rpc}` | `tests/conformance_artifacts.rs::artifact_bounds_and_owner_budget_are_enforced` (both) |
|
| Reserve an owner allowance for lifecycle and results separately from ordinary telemetry | conforms | `limits.rs::reserved_owners_per_client`, `router/state.rs::{ordinary_budget_left, dispatch_rpc}` | `tests/conformance_artifacts.rs::artifact_bounds_and_owner_budget_are_enforced` (both) |
|
||||||
| Memory quotas account for staging, seal copies, queued deliveries and caches | conforms | `router/state.rs::{op_allocate, op_seal, Conn::queued_bytes}` | `tests/artifacts.rs::quotas_are_enforced` (both) |
|
| Memory quotas account for staging, seal copies, queued deliveries and caches | conforms | `router/state.rs::{op_allocate, op_seal, Conn::queued_bytes}` | `tests/artifacts.rs::quotas_are_enforced` (both) |
|
||||||
| Ownership metadata is bounded even when many roots share one artifact | conforms: a root is a counter, and owners are bounded per client | `router/state.rs::{Art::roots, ordinary_budget_left}` | `tests/conformance_artifacts.rs::artifact_bounds_and_owner_budget_are_enforced` (both) |
|
| Ownership metadata is bounded even when many roots share one artifact | conforms: a root is a counter, and owners are bounded per client | `router/state.rs::{Art::roots, ordinary_budget_left}` | `tests/conformance_artifacts.rs::artifact_bounds_and_owner_budget_are_enforced` (both) |
|
||||||
|
|
@ -254,13 +254,13 @@ and once over a Unix socket, so `tests/rpc.rs::request_reply_roundtrip` means
|
||||||
| Replies, release, cancellation and route-health control cannot be starved by telemetry | conforms: control first, then RPC, then topic data, with topic data given a turn after 16 higher-priority frames | `router/state.rs::{next_frame, TOPIC_STARVATION_LIMIT}` | `tests/sol_rereview_regressions.rs::{fair_topic_insertion_preserves_router_envelope_order, sdk_accepts_fair_topic_insertion_through_saturated_control_backlog}`, `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
| Replies, release, cancellation and route-health control cannot be starved by telemetry | conforms: control first, then RPC, then topic data, with topic data given a turn after 16 higher-priority frames | `router/state.rs::{next_frame, TOPIC_STARVATION_LIMIT}` | `tests/sol_rereview_regressions.rs::{fair_topic_insertion_preserves_router_envelope_order, sdk_accepts_fair_topic_insertion_through_saturated_control_backlog}`, `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
||||||
| ... and neither can connection teardown be starved by the frame it is waiting for | **was deviates-must-fix, fixed on this branch**. The write gate was a plain mutex held across each synchronous transport poll, so a writer sending a frame one byte per poll re-acquired it hundreds of times while teardown waited for it, and could finish a whole delivery before teardown got in: `teardown_waits_for_an_active_transport_poll_before_reclaiming` failed 6 runs out of 6 in release and about 1 in 5 in debug. The gate now separates "teardown has begun" (a flag set once, without waiting) from "a poll is in progress" (a condvar teardown waits on), so teardown's window is one poll instead of a whole frame, and no byte can follow it. No public signature changed | `router/state.rs::WriteGate`, `router/mod.rs::write_selected` | `tests/sol_rereview_regressions.rs::teardown_waits_for_an_active_transport_poll_before_reclaiming` (rewritten: a 50 KB delivery the resumed writer cannot finish, and a channel instead of a sleep) |
|
| ... and neither can connection teardown be starved by the frame it is waiting for | **was deviates-must-fix, fixed on this branch**. The write gate was a plain mutex held across each synchronous transport poll, so a writer sending a frame one byte per poll re-acquired it hundreds of times while teardown waited for it, and could finish a whole delivery before teardown got in: `teardown_waits_for_an_active_transport_poll_before_reclaiming` failed 6 runs out of 6 in release and about 1 in 5 in debug. The gate now separates "teardown has begun" (a flag set once, without waiting) from "a poll is in progress" (a condvar teardown waits on), so teardown's window is one poll instead of a whole frame, and no byte can follow it. No public signature changed | `router/state.rs::WriteGate`, `router/mod.rs::write_selected` | `tests/sol_rereview_regressions.rs::teardown_waits_for_an_active_transport_poll_before_reclaiming` (rewritten: a 50 KB delivery the resumed writer cannot finish, and a channel instead of a sleep) |
|
||||||
| Preserve FIFO for calls to a target despite lane scheduling | conforms: the service queue is FIFO and lane choice never reorders it | `router/state.rs::dispatch_rpc` | `tests/rpc.rs::fifo_dispatch_and_out_of_order_completion` (both) |
|
| Preserve FIFO for calls to a target despite lane scheduling | conforms: the service queue is FIFO and lane choice never reorders it | `router/state.rs::dispatch_rpc` | `tests/rpc.rs::fifo_dispatch_and_out_of_order_completion` (both) |
|
||||||
| Classification is an explicit generic envelope operation or policy, not a topic-name heuristic | conforms: lanes come from `Kind`/`op`, never from a name | `router/state.rs::next_frame` | `tests/sol_rereview_regressions.rs::fair_topic_insertion_preserves_router_envelope_order` |
|
| Classification is an explicit generic envelope operation or policy, not a topic-name heuristic | conforms by construction: `next_frame` chooses a lane from the queue an item sits in (control, then RPC, then topic), and neither it nor `pop_control`/`dispatch_rpc`/`dispatch_topic` reads a service or topic name. A name reaches the scheduler only as opaque bytes inside an already-classified frame | `router/state.rs::{next_frame, pop_control, dispatch_rpc, dispatch_topic}` | `tests/bus_acceptance.rs::a_topic_named_like_a_notice_is_still_classified_as_topic_data` (both), and `tests/sol_rereview_regressions.rs::fair_topic_insertion_preserves_router_envelope_order` for the lane order itself |
|
||||||
| No indefinite wait inside the router on subscriber readiness or artifact I/O | conforms: a slow reader stalls only its own writer task; I/O leaves the lock | `router/mod.rs::{write_loop, read_loop}` | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
| No indefinite wait inside the router on subscriber readiness or artifact I/O | conforms: a slow reader stalls only its own writer task; I/O leaves the lock | `router/mod.rs::{write_loop, read_loop}` | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
||||||
| Admission is bounded; rejected callers choose their own policy | conforms | `router/state.rs::{op_call, op_publish}` | `tests/rpc.rs::service_queue_backpressure` (both) |
|
| Admission is bounded; rejected callers choose their own policy | conforms | `router/state.rs::{op_call, op_publish}` | `tests/rpc.rs::service_queue_backpressure` (both) |
|
||||||
| The thirteen transport error codes exist with those names | conforms | `error.rs::ErrorCode` | `tests/wire.rs::body_errors_keep_the_connection` (both) |
|
| The thirteen transport error codes exist with those names | conforms | `error.rs::ErrorCode` | `tests/wire.rs::body_errors_keep_the_connection` (both) |
|
||||||
| Three more codes: `CONFLICT`, `NO_TOPIC`, `ARTIFACT_MISMATCH` | deviates-allowed: "Transport errors **include** ..." is not an exhaustive list, and each names a refusal the draft requires but leaves unnamed. Recorded as an amendment in bus-v1 section 12 | `error.rs::ErrorCode` | `tests/pubsub.rs::subscription_and_topic_validation` (both), `tests/artifacts.rs::seal_checks_length_and_digest` (both) |
|
| Three more codes: `CONFLICT`, `NO_TOPIC`, `ARTIFACT_MISMATCH` | deviates-allowed: "Transport errors **include** ..." is not an exhaustive list, and each names a refusal the draft requires but leaves unnamed. Recorded as an amendment in bus-v1 section 12 | `error.rs::ErrorCode` | `tests/pubsub.rs::subscription_and_topic_validation` (both), `tests/artifacts.rs::seal_checks_length_and_digest` (both) |
|
||||||
| Before admission report `not-dispatched`; once dispatch might have occurred report `dispatched` or `unknown` conservatively | conforms | `error.rs::BusError::new` (not-dispatched by default), `router/state.rs` dispatched notices, `client/reactor.rs::fail_all` (unknown) | `tests/rpc.rs::{cancellation_states, service_disconnect_fails_calls}` (both), `tests/sol_review_races.rs::writer_failure_terminates_reader_and_pending_work` |
|
| Before admission report `not-dispatched`; once dispatch might have occurred report `dispatched` or `unknown` conservatively | conforms | `error.rs::BusError::new` (not-dispatched by default), `router/state.rs` dispatched notices, `client/reactor.rs::fail_all` (unknown) | `tests/rpc.rs::{cancellation_states, service_disconnect_fails_calls}` (both), `tests/sol_review_races.rs::writer_failure_terminates_reader_and_pending_work` |
|
||||||
| Bounded subscriptions can reject a publication; latest spectators cannot hold a session transaction indefinitely | conforms: a latest subscriber never causes `BACKPRESSURE` | `router/state.rs::op_publish` (the latest branch skips every capacity check) | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both), `tests/perf.rs` (a consumer delayed 40 ms per frame coalesces 71 of 120 frames and never refuses one) |
|
| Bounded subscriptions can reject a publication; latest spectators cannot hold a session transaction indefinitely | conforms: a latest subscriber never causes `BACKPRESSURE` | `router/state.rs::op_publish` (the latest branch skips every capacity check) | `tests/bus_acceptance.rs::a_latest_subscriber_never_refuses_a_publication` (both: 100 publications of 60 KB into one unconsumed slot, six times the bounded pool, none refused, 98 coalesced), with `tests/pubsub.rs::{bounded_fifo_and_atomic_backpressure, saturated_subscriber_does_not_block_control}` (both) for the bounded half |
|
||||||
| Sustained pinned-artifact quota exhaustion is surfaced as pressure, not solved by freeing live data | conforms: `QUOTA_EXCEEDED`, never eviction | `router/state.rs::{op_allocate, op_seal}` | `tests/artifacts.rs::quotas_are_enforced` (both) |
|
| Sustained pinned-artifact quota exhaustion is surfaced as pressure, not solved by freeing live data | conforms: `QUOTA_EXCEEDED`, never eviction | `router/state.rs::{op_allocate, op_seal}` | `tests/artifacts.rs::quotas_are_enforced` (both) |
|
||||||
| Session and application policies choose disconnect, pause or fail; the router does not know which | conforms by absence | `router/state.rs` | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
| Session and application policies choose disconnect, pause or fail; the router does not know which | conforms by absence | `router/state.rs` | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both) |
|
||||||
|
|
||||||
|
|
@ -326,8 +326,10 @@ produce 29 events, identical over both transports; `FLYBUS_TRACE=1` prints them.
|
||||||
| Retained replay is ordered | `tests/pubsub.rs::retained_replay_clear_delete_and_incarnations` (both), `tests/conformance_routing.rs::{latest_replay_is_ordered_ahead_of_a_racing_publish, cleared_topic_gives_no_replay_until_a_fresh_publish}` (both), `tests/sol_review_races.rs::{retained_replay_obeys_bounded_queue_byte_quota, latest_replay_remains_bounded_outside_the_bounded_byte_pool}` |
|
| Retained replay is ordered | `tests/pubsub.rs::retained_replay_clear_delete_and_incarnations` (both), `tests/conformance_routing.rs::{latest_replay_is_ordered_ahead_of_a_racing_publish, cleared_topic_gives_no_replay_until_a_fresh_publish}` (both), `tests/sol_review_races.rs::{retained_replay_obeys_bounded_queue_byte_quota, latest_replay_remains_bounded_outside_the_bounded_byte_pool}` |
|
||||||
| Stalled observers cannot starve RPC replies | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both), `tests/sol_rereview_regressions.rs::{fair_topic_insertion_preserves_router_envelope_order, sdk_accepts_fair_topic_insertion_through_saturated_control_backlog}` |
|
| Stalled observers cannot starve RPC replies | `tests/pubsub.rs::saturated_subscriber_does_not_block_control` (both), `tests/sol_rereview_regressions.rs::{fair_topic_insertion_preserves_router_envelope_order, sdk_accepts_fair_topic_insertion_through_saturated_control_backlog}` |
|
||||||
|
|
||||||
All six were already covered, so BUS-02 added no test. The equivalence trace above carries a
|
All six were already covered, so BUS-02 added no test of its own. The equivalence trace above
|
||||||
pub/sub and artifact scenario, so BUS-02's behaviour is in the transport comparison too.
|
carries a pub/sub and artifact scenario, so BUS-02's behaviour is in the transport comparison
|
||||||
|
too, and `a_latest_subscriber_never_refuses_a_publication` (both) proves the rule that sits
|
||||||
|
behind "latest replaces only queued messages": the replacement never turns into a refusal.
|
||||||
|
|
||||||
### BUS-03 — artifact-backed messages and automatic lifetimes
|
### BUS-03 — artifact-backed messages and automatic lifetimes
|
||||||
|
|
||||||
|
|
@ -350,11 +352,11 @@ Each of the ten differences the crate lists, kept with the sentence that allows
|
||||||
| 1. Extra error codes `CONFLICT`, `NO_TOPIC`, `ARTIFACT_MISMATCH` | Kept. Allowed by section 9: "Transport errors **include** `INVALID_ENVELOPE`, ..." — an inclusive list. Each names a refusal the draft requires without naming its code, so all three are now in the bus-v1 amendment (section 12) |
|
| 1. Extra error codes `CONFLICT`, `NO_TOPIC`, `ARTIFACT_MISMATCH` | Kept. Allowed by section 9: "Transport errors **include** `INVALID_ENVELOPE`, ..." — an inclusive list. Each names a refusal the draft requires without naming its code, so all three are now in the bus-v1 amendment (section 12) |
|
||||||
| 2. Topics must be declared; `topic.clear` of an unknown topic is `NO_TOPIC` while `topic.delete` answers `deleted:false` | Kept. The draft is silent; section 7's "Topic count and retained bytes are capped" and `topic.declare`'s "conflicting settings fail" both presuppose a registry. See the section 7 row |
|
| 2. Topics must be declared; `topic.clear` of an unknown topic is `NO_TOPIC` while `topic.delete` answers `deleted:false` | Kept. The draft is silent; section 7's "Topic count and retained bytes are capped" and `topic.declare`'s "conflicting settings fail" both presuppose a registry. See the section 7 row |
|
||||||
| 3. When notices are sent | Kept. Section 5 requires the three notices but says nothing about audience or timing: "Required bounded notices are route removal, subscription closure and call failure" |
|
| 3. When notices are sent | Kept. Section 5 requires the three notices but says nothing about audience or timing: "Required bounded notices are route removal, subscription closure and call failure" |
|
||||||
| 4. Byte budgets: `max_queued_bytes_per_client` counts bounded subscriptions only; `max_retained_bytes` added | Kept. Section 9's "latest spectator subscriptions cannot hold a required session transaction indefinitely" excludes the latest slot from a rejecting budget (contradiction 1), and section 7's "Topic count and retained bytes are capped" requires the retained cap |
|
| 4. Byte budgets: `max_queued_bytes_per_client` counts bounded subscriptions only; `max_retained_bytes` added | Kept, and no longer a difference: the section 9 table now names the bounded pool and bounds latest slots separately (amendment, contradiction 1), and section 7's "Topic count and retained bytes are capped" requires the retained cap |
|
||||||
| 5. Admission sizes a delivery with the router-added ids at their longest, so an inbound envelope near 65,536 bytes can be refused although it fits | Kept, and required: section 5's "Message length includes this wrapper" plus section 4's fixed maximum mean the delivery the router would build must also fit. ipc-v1 section 2's "must not silently truncate a payload" forbids the alternative |
|
| 5. Admission sizes a delivery with the router-added ids at their longest, so an inbound envelope near 65,536 bytes can be refused although it fits | Kept, and required: section 5's "Message length includes this wrapper" plus section 4's fixed maximum mean the delivery the router would build must also fit. ipc-v1 section 2's "must not silently truncate a payload" forbids the alternative |
|
||||||
| 6. One live connection per client id; a client id's last incarnation may not be reused; only `*_as` endpoints authenticate the Hello id | Kept. See the section 3 and 4 rows: section 3's per-incarnation callId uniqueness and section 4's launcher-provided privileges |
|
| 6. One live connection per client id; a client id's last incarnation may not be reused; only `*_as` endpoints authenticate the Hello id | Kept. See the section 3 and 4 rows: section 3's per-incarnation callId uniqueness and section 4's launcher-provided privileges |
|
||||||
| 7. The seal reply's ownerId is the writer's own id, now a hold | Kept. Section 8.2: "seal transfers its unique writer" — one owner token, transferred |
|
| 7. The seal reply's ownerId is the writer's own id, now a hold | Kept. Section 8.2: "seal transfers its unique writer" — one owner token, transferred |
|
||||||
| 8. No `budget` argument on calls, and no router executable | Kept. Section 1 calls the executable "optional"; section 2 is labelled "Illustrative Rust surface (not yet implemented)"; section 6 puts the deadline in the calling client. See contradiction 2 |
|
| 8. No `budget` argument on calls, and no router executable | Kept. Section 1 calls the executable "optional"; section 6 puts the deadline in the calling client, and the section 2 sketch no longer shows a budget either (amendment, contradiction 2) |
|
||||||
| 9. Wire strictness: unknown fields in management bodies refused, `minor` must be 0 after hello | Kept. Stricter than section 4's "unknown envelope fields", forbidden nowhere, and section 4's envelope literally fixes `minor: 0` |
|
| 9. Wire strictness: unknown fields in management bodies refused, `minor` must be 0 after hello | Kept. Stricter than section 4's "unknown envelope fields", forbidden nowhere, and section 4's envelope literally fixes `minor: 0` |
|
||||||
| 10. `rpc.responder.release` and its terminal `call.failed` | Kept. Section 4 allows draft schema changes ("Changes to these draft schemas change contractDigest"), and it is how section 6's "bounded call correlation metadata" stays bounded when a handler outlives its request delivery. Added to the bus-v1 amendment (section 12) |
|
| 10. `rpc.responder.release` and its terminal `call.failed` | Kept. Section 4 allows draft schema changes ("Changes to these draft schemas change contractDigest"), and it is how section 6's "bounded call correlation metadata" stays bounded when a handler outlives its request delivery. Added to the bus-v1 amendment (section 12) |
|
||||||
|
|
||||||
|
|
@ -412,7 +414,10 @@ What the numbers do and do not say:
|
||||||
|
|
||||||
## Contradictions
|
## Contradictions
|
||||||
|
|
||||||
Two, both inside bus-v1, both minor, and neither resolved by changing code in this branch.
|
Two, both inside bus-v1, both minor, neither resolved by changing code. Both were referred to
|
||||||
|
the coordinator rather than guessed at, and both now carry a dated amendment in bus-v1
|
||||||
|
section 12; the rows above cite the amended wording. The original reading is kept here because
|
||||||
|
it is the reason for the amendment.
|
||||||
|
|
||||||
1. **The per-client queued envelope byte budget versus the latest-mode guarantee.** Section 9's
|
1. **The per-client queued envelope byte budget versus the latest-mode guarantee.** Section 9's
|
||||||
table has "Per-client ordinary queued envelope bytes | 1 MiB". Section 7 requires that a
|
table has "Per-client ordinary queued envelope bytes | 1 MiB". Section 7 requires that a
|
||||||
|
|
@ -422,15 +427,17 @@ Two, both inside bus-v1, both minor, and neither resolved by changing code in th
|
||||||
covered latest slots and rejected on overflow would violate the second statement; a budget
|
covered latest slots and rejected on overflow would violate the second statement; a budget
|
||||||
that excludes them is not the sentence in the table. The crate excludes them, which keeps the
|
that excludes them is not the sentence in the table. The crate excludes them, which keeps the
|
||||||
normative sentence and loosens the table row: the worst case becomes subscriptions x 64 KiB
|
normative sentence and loosens the table row: the worst case becomes subscriptions x 64 KiB
|
||||||
(8 MiB at the default 128 subscriptions per client) instead of 1 MiB. **Left as it is.** The
|
(8 MiB at the default 128 subscriptions per client) instead of 1 MiB. **Resolved in the
|
||||||
spec owner should either say the budget covers bounded subscriptions only, or give latest
|
spec, not the code** (2026-09-22): the table row now names the bounded pool and latest slots
|
||||||
slots their own cap.
|
have their own row, so the implementation conforms as written. No behaviour changed, and
|
||||||
|
`a_latest_subscriber_never_refuses_a_publication` now proves the guarantee directly.
|
||||||
2. **A call `budget` versus a client-owned deadline.** Section 2's illustrative surface passes a
|
2. **A call `budget` versus a client-owned deadline.** Section 2's illustrative surface passes a
|
||||||
`budget` into `bus.call(...)`, while section 6 states "A deadline belongs to the calling
|
`budget` into `bus.call(...)`, while section 6 states "A deadline belongs to the calling
|
||||||
client" and gives the router no timeout behaviour, and section 5's `rpc.call` body has no
|
client" and gives the router no timeout behaviour, and section 5's `rpc.call` body has no
|
||||||
budget field. The crate follows sections 5 and 6 and has no budget argument, which is safe
|
budget field. The crate follows sections 5 and 6 and has no budget argument, which is safe
|
||||||
because section 2 is labelled illustrative. **Left as it is.** The spec owner should drop
|
because section 2 is labelled illustrative. **Resolved in the spec, not the code**
|
||||||
`budget` from the section 2 sketch or say what the router would do with it.
|
(2026-09-22): the sketch drops `budget` and shows the deadline at the caller, so the
|
||||||
|
illustrative surface and the wire contract now agree.
|
||||||
|
|
||||||
Ambiguities resolved without treating them as contradictions, for the record:
|
Ambiguities resolved without treating them as contradictions, for the record:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ Illustrative Rust surface (not yet implemented):
|
||||||
```rust
|
```rust
|
||||||
let bus = Client::connect(config).await?;
|
let bus = Client::connect(config).await?;
|
||||||
let service = bus.register("agent.fly-a", service_config).await?;
|
let service = bus.register("agent.fly-a", service_config).await?;
|
||||||
let reply = bus.call(target, "Agent.Prepare", payload, attachments, budget).await?;
|
let reply = timeout(deadline, bus.call(target, "Agent.Prepare", payload, attachments)).await?;
|
||||||
let subscription = bus.subscribe("session.demo.snapshots", subscription_config).await?;
|
let subscription = bus.subscribe("session.demo.snapshots", subscription_config).await?;
|
||||||
bus.publish("session.demo.snapshots", payload, attachments).await?;
|
bus.publish("session.demo.snapshots", payload, attachments).await?;
|
||||||
|
|
||||||
|
|
@ -422,7 +422,8 @@ Configure limits explicitly; these defaults are a prototype starting point, not
|
||||||
| Bounded subscription queued / in-flight deliveries | 64 / 16 |
|
| Bounded subscription queued / in-flight deliveries | 64 / 16 |
|
||||||
| Active owners per client | 256 |
|
| Active owners per client | 256 |
|
||||||
| Total artifact storage / per object | 512 MiB / 128 MiB |
|
| Total artifact storage / per object | 512 MiB / 128 MiB |
|
||||||
| Per-client ordinary queued envelope bytes | 1 MiB |
|
| Per-client ordinary bounded queued envelope bytes | 1 MiB |
|
||||||
|
| Latest subscription slots | subscriptions × 64 KiB |
|
||||||
| Reserved management/reply lane | 128 frames and 1 MiB per client |
|
| Reserved management/reply lane | 128 frames and 1 MiB per client |
|
||||||
|
|
||||||
Reserve an owner allowance for lifecycle/results separately from ordinary telemetry; memory
|
Reserve an owner allowance for lifecycle/results separately from ordinary telemetry; memory
|
||||||
|
|
@ -510,3 +511,15 @@ request delivery:
|
||||||
| `rpc.responder.release` | `{callId, requestDeliveryId}` -> `{released}` | The recipient gives up reply authority for a dispatched call. The final release for an attached call retires the correlation and emits `call.failed` with dispatch `dispatched`: `CALL_GONE` while the route is live, `NO_SERVICE` after route loss. Request consumption (section 8.3) stays independent of it. |
|
| `rpc.responder.release` | `{callId, requestDeliveryId}` -> `{released}` | The recipient gives up reply authority for a dispatched call. The final release for an attached call retires the correlation and emits `call.failed` with dispatch `dispatched`: `CALL_GONE` while the route is live, `NO_SERVICE` after route loss. Request consumption (section 8.3) stays independent of it. |
|
||||||
|
|
||||||
Both amendments change `contractDigest`, which section 4 already provides for.
|
Both amendments change `contractDigest`, which section 4 already provides for.
|
||||||
|
|
||||||
|
**2026-09-22, coordinator decision on the audit's contradiction 1.** Section 9's table row
|
||||||
|
"Per-client ordinary queued envelope bytes | 1 MiB" now reads "Per-client ordinary **bounded**
|
||||||
|
queued envelope bytes", and latest slots get their own row, "subscriptions × 64 KiB", because
|
||||||
|
section 7's unconditional one-slot guarantee and the structural 1/2 cap outweigh one imprecise
|
||||||
|
table row: a budget whose overflow rejects a publication cannot contain a subscription that
|
||||||
|
this same section forbids to reject one.
|
||||||
|
|
||||||
|
**2026-09-22, coordinator decision on the audit's contradiction 2.** Section 2's sketch no
|
||||||
|
longer passes a `budget` into `bus.call` and shows the deadline at the caller instead, because
|
||||||
|
section 5's wire contract for `rpc.call` has no budget field and section 2 is self-labelled
|
||||||
|
illustrative.
|
||||||
|
|
|
||||||
|
|
@ -291,9 +291,11 @@ before admission are `not-dispatched`. A command in flight when the connection i
|
||||||
- `route.removed` goes to callers with queued or dispatched calls on the removed
|
- `route.removed` goes to callers with queued or dispatched calls on the removed
|
||||||
registration, not to every client.
|
registration, not to every client.
|
||||||
- `connection.closing` is an extra notice that precedes every router-initiated close.
|
- `connection.closing` is an extra notice that precedes every router-initiated close.
|
||||||
4. **Byte budgets.**
|
4. **Byte budgets.** No longer a difference: the draft's section 9 table was amended on
|
||||||
|
2026-09-22 to name the bounded pool and to bound latest slots separately.
|
||||||
- `max_queued_bytes_per_client` counts only `bounded` subscriptions. A `latest` slot is bounded
|
- `max_queued_bytes_per_client` counts only `bounded` subscriptions. A `latest` slot is bounded
|
||||||
by subscription count times envelope size.
|
by subscription count times envelope size, because a latest subscriber may never be the
|
||||||
|
reason a publication is refused.
|
||||||
- `max_retained_bytes` (not in the draft's table) counts the artifact bytes pinned by
|
- `max_retained_bytes` (not in the draft's table) counts the artifact bytes pinned by
|
||||||
retained values, once per topic.
|
retained values, once per topic.
|
||||||
5. **Delivery size.** Admission computes the delivery's size with the router-added ids at their
|
5. **Delivery size.** Admission computes the delivery's size with the router-added ids at their
|
||||||
|
|
@ -305,7 +307,9 @@ before admission are `not-dispatched`. A command in flight when the connection i
|
||||||
distinct client id ever seen.
|
distinct client id ever seen.
|
||||||
7. **Seal reply.** The reply's `ownerId` is the writer's own id, now an explicit hold.
|
7. **Seal reply.** The reply's `ownerId` is the writer's own id, now an explicit hold.
|
||||||
8. **No `budget` argument on calls, and no router executable.** Timeouts are the caller's
|
8. **No `budget` argument on calls, and no router executable.** Timeouts are the caller's
|
||||||
(`tokio::time::timeout` plus `cancel`). The draft's executable is optional; embed `Router`.
|
(`tokio::time::timeout` plus `cancel`); the draft's section 2 sketch was amended on
|
||||||
|
2026-09-22 to show the deadline there too. The draft's executable is optional; embed
|
||||||
|
`Router`.
|
||||||
9. **Wire strictness.** Management bodies reject unknown fields. After hello, envelopes must
|
9. **Wire strictness.** Management bodies reject unknown fields. After hello, envelopes must
|
||||||
carry `minor: 0`.
|
carry `minor: 0`.
|
||||||
10. **Reply capability release.** The SDK sends `rpc.responder.release {callId,
|
10. **Reply capability release.** The SDK sends `rpc.responder.release {callId,
|
||||||
|
|
@ -386,4 +390,7 @@ socket, through the same router code:
|
||||||
and requires the two transports to record the same 29 behaviour events. `Trace::record`
|
and requires the two transports to record the same 29 behaviour events. `Trace::record`
|
||||||
panics on a router-issued id, so a trace cannot drift into operational detail.
|
panics on a router-issued id, so a trace cannot drift into operational detail.
|
||||||
`FLYBUS_TRACE=1` prints it.
|
`FLYBUS_TRACE=1` prints it.
|
||||||
|
It also holds the two rules an audit reviewer found cited but unproven: a latest subscriber
|
||||||
|
flooded with 100 publications of 60 KB never refuses one, and a topic named exactly like a
|
||||||
|
router notice is still delivered as topic data.
|
||||||
- `tests/example_demo.rs`: runs `examples/demo.rs` and asserts every line it prints.
|
- `tests/example_demo.rs`: runs `examples/demo.rs` and asserts every line it prints.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ use std::collections::HashMap;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::{Env, Trace, Via, env, obj, quiet, sealed, within};
|
use common::{Env, Trace, Via, code, env, obj, quiet, sealed, within};
|
||||||
|
use flybus::wire::Kind;
|
||||||
use flybus::{
|
use flybus::{
|
||||||
CancelState, Client, Dispatch, ErrorCode, Retained, Service, ServiceConfig, SubscriptionConfig,
|
CancelState, Client, Dispatch, ErrorCode, Retained, Service, ServiceConfig, SubscriptionConfig,
|
||||||
};
|
};
|
||||||
|
|
@ -357,6 +358,97 @@ async fn a_status_rpc_responds_while_another_handler_is_delayed(via: Via) {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Section 9: the two rules an audit reviewer found cited but unproven
|
||||||
|
|
||||||
|
/// bus-v1 section 9: "Bounded event subscriptions can reject publication; latest spectator
|
||||||
|
/// subscriptions cannot hold a required session transaction indefinitely."
|
||||||
|
///
|
||||||
|
/// A latest subscriber that never consumes must never be the reason a publication is refused,
|
||||||
|
/// however many envelope bytes its slot would have accumulated: the slot sits outside the
|
||||||
|
/// per-client bounded-queue byte pool. 100 publications of 60 KB are six times that pool.
|
||||||
|
async fn a_latest_subscriber_never_refuses_a_publication(via: Via) {
|
||||||
|
let e = env(via).await;
|
||||||
|
let publisher = e.client("publisher").await;
|
||||||
|
let spectator = e.client("spectator").await;
|
||||||
|
publisher
|
||||||
|
.declare_topic("world.demo.frame", Retained::None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// One credit, and nothing ever consumes it: after the first delivery every later
|
||||||
|
// publication meets the single replaceable slot.
|
||||||
|
let _stuck = spectator
|
||||||
|
.subscribe(
|
||||||
|
"world.demo.frame",
|
||||||
|
SubscriptionConfig::latest().in_flight(1),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
e.settle("subscribed", |s| s.subscriptions == 1).await;
|
||||||
|
|
||||||
|
let blob = "s".repeat(60_000);
|
||||||
|
let publish = async |n: u64| {
|
||||||
|
within(
|
||||||
|
"publication",
|
||||||
|
publisher.publish("world.demo.frame", obj(json!({"n": n, "blob": blob})), &[]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| panic!("publication {n} was refused with {err}"))
|
||||||
|
};
|
||||||
|
publish(0).await;
|
||||||
|
e.settle("the only credit is in use", |s| s.owners == 1).await;
|
||||||
|
|
||||||
|
let mut replaced_total = 0;
|
||||||
|
for n in 1..100u64 {
|
||||||
|
let receipt = publish(n).await;
|
||||||
|
assert_eq!(receipt.subscribers, 1);
|
||||||
|
replaced_total += receipt.replaced;
|
||||||
|
}
|
||||||
|
// The first of those found an empty slot; the other 98 replaced an undelivered value.
|
||||||
|
assert_eq!(replaced_total, 98);
|
||||||
|
let stats = e.stats();
|
||||||
|
assert_eq!(stats.queued, 1, "the slot never grew: {stats:?}");
|
||||||
|
assert_eq!(stats.owners, 1, "no credit came back: {stats:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bus-v1 section 9: "classification is an explicit generic envelope operation/policy, not a
|
||||||
|
/// topic-name heuristic", and section 3: "The router treats names as opaque addresses."
|
||||||
|
///
|
||||||
|
/// A topic whose name is spelled exactly like a router notice is still declared, routed and
|
||||||
|
/// delivered as topic data: the delivery is a `topic.message`, not the notice it is named
|
||||||
|
/// after, and its payload arrives untouched.
|
||||||
|
async fn a_topic_named_like_a_notice_is_still_classified_as_topic_data(via: Via) {
|
||||||
|
let e = env(via).await;
|
||||||
|
let publisher = e.client("publisher").await;
|
||||||
|
let mut raw = e.raw_hello("watcher").await;
|
||||||
|
let names = ["call.failed", "route.removed", "subscription.closed"];
|
||||||
|
for name in names {
|
||||||
|
publisher.declare_topic(name, Retained::None).await.unwrap();
|
||||||
|
let reply = raw
|
||||||
|
.call(
|
||||||
|
"subscribe",
|
||||||
|
json!({"topic": name, "mode": "bounded", "maxQueued": 4, "maxInFlight": 4, "replayLatest": false}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(code(&reply), "OK", "{name} could not be subscribed to");
|
||||||
|
}
|
||||||
|
for name in names {
|
||||||
|
publisher
|
||||||
|
.publish(name, obj(json!({"named": name})), &[])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let envelope = raw.event().await;
|
||||||
|
assert_eq!(
|
||||||
|
(envelope.kind, envelope.op.as_str()),
|
||||||
|
(Kind::Delivery, "topic.message"),
|
||||||
|
"the topic name {name} changed how the router classified it"
|
||||||
|
);
|
||||||
|
assert_eq!(envelope.body["topic"], json!(name));
|
||||||
|
assert_eq!(envelope.body["payload"]["named"], json!(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// BUS-03
|
// BUS-03
|
||||||
|
|
||||||
|
|
@ -681,5 +773,7 @@ both_transports!(
|
||||||
a_retransmission_repeats_the_domain_request_under_a_fresh_call_id,
|
a_retransmission_repeats_the_domain_request_under_a_fresh_call_id,
|
||||||
no_automatic_retry_or_failover_onto_a_replacement_registration,
|
no_automatic_retry_or_failover_onto_a_replacement_registration,
|
||||||
a_status_rpc_responds_while_another_handler_is_delayed,
|
a_status_rpc_responds_while_another_handler_is_delayed,
|
||||||
|
a_latest_subscriber_never_refuses_a_publication,
|
||||||
|
a_topic_named_like_a_notice_is_still_classified_as_topic_data,
|
||||||
disconnect_releases_logical_ownership_without_mutating_open_bytes,
|
disconnect_releases_logical_ownership_without_mutating_open_bytes,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue