joule-moq-relay, co-performance §7.3: count each author's audience inputs in its chain's order #462
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/floor-input-chain-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this fixes
The relay limits each audience member's inputs to one window (co-performance §7.3,
Floor::events_filter/input_verdict, counted inf.sent). Each group of a member's events track rides its own stream, and the relay reads each stream in its own task, so two inputs sent back to back can reach the floor in either order. The floor counted them in the order they arrived. With a limit of one, it could forward the member's second input (seq 1, chained after seq 0) and refuse the first (seq 0). Subscribers then held seq 1 with a gap at seq 0. A verifier counting in chain order accepts seq 0 and refuses seq 1.Found while debugging Train D:
tests/floor.rsthe_relay_refuses_invalid_audience_inputsfailed 2 runs in 10 at load 40–70, and the diagnostic showed seq 1 forwarded and seq 0 refused. Train D (#451, now merged) hardened that test by sending seq 1 only after seq 0 arrives. That stopped the flake but did not fix the relay. This PR fixes the relay and keeps the hardened test.The rule (co-performance §7.3, new paragraph)
A relay MUST count an author's inputs in the order of the author's chain (live-events §6.2), as a verifier does, never in the order they arrive.
seq, and it has decided every input among those records.input_out_of_order, and the relay counts the author's chain from the record after it.input_out_of_order, because counting it then would put it after an input that follows it in the chain.§8 now lists
input_out_of_orderbesidewindow_committedas a reason only the relay gives. §12 adds that an author who leaves a gap in its own chain holds up only its own inputs, and for no longer than the bound. CHANGELOG entry under v0.4.0.Why §10, the corpus and the verifiers do not change: no verifier ever raises
input_out_of_order. A verifier holds each chain in order (a record behind the head isseq_not_increasing).wai-rs(co_performance.rs,rr.order) andco_performance_verify.py(room["order"]) already count each input at its place in the merged order, which keeps each chain's order. So the reason follows the precedent ofwindow_committed: it is named in §7.3 and §8, not in §10. §11's test requires §10 to list exactly the reference's codes, and the corpus is a corpus for verifiers. A vector for a reason only the relay gives would have nothing to check.The relay (
joule-moq-relay)Chain { next, ahead, waiting, forwarded }. These are the first seq it lacks, the seqs above that it has received, the inputs waiting (by digest, each with its seq and the instant its hold ends), and the highest seq of an input it has forwarded.next > seqand no input below it is waiting (Chain::ready). It is refusedinput_out_of_orderifforwarded > seq, but only after the checks that don't depend on order (signature, form, power, window, type) have passed.Floor::events_filteris now async. An object with an input that cannot be decided yet waits, along with the rest of its stream, on aNotify. It wakes when another stream's object moves the author's chain on, or when the earliest hold in that chain runs out. This is the same pattern the door uses (door.rshold_ms).Floor::events_stepis the synchronous step at an explicit instant, with aPasscarrying what earlier steps decided. Unit tests drive it deterministically, with no real time.[floor] hold_ms, 2 000 ms by default (floor::DEFAULT_HOLD_MS), set viaFloor::holding.relay.example.tomlshows it.FloorConfignow has a manualDefault, and the struct literals in the tests gain..FloorConfig::default():tests/floor.rs(×2, including the hardened test's config line),tests/age_room_service.rs,joule-moq-room/tests/co_performance.rsand theconfig.rsunit test.input_out_of_orderis counted in the session's summary underco_refused.Design decisions
prevarrives is not enough. For a chain [vote, chat, vote] arriving last to first, the chat arrives (it is never held), and the second vote would then be counted before the first. The rule therefore needs every record below the input received, and no input below it still waiting.input_out_of_order.input_out_of_orderonly when ordering is the reason it is not forwarded.Tests
tests/floor.rsthe_relay_counts_inputs_in_chain_order: the member's seq 1 is sent first, on its own stream. The test waits until the relay holds it (Floor::waiting), then sends seq 0. Seq 0 is forwarded and seq 1 is refusedinput_over_limit; the summary shows[("input_over_limit", 1)]. The hold is set to 60 s so no wait in the test can outlast it under load.tests/floor.rsthe_relay_refuses_an_input_whose_gap_never_fills: seq 1 alone, with a 400 ms hold. It is refusedinput_out_of_order, and not before the hold runs out. A late seq 0 is then forwarded. The summary shows[("input_out_of_order", 1)].the_relay_refuses_invalid_audience_inputs(Train D's hardened test) is kept. Its config line gains..FloorConfig::default(), and one comment clause now points at the new test instead of saying the limit counts in arrival order.src/floor.rs), with explicit instants:inputs_are_counted_in_their_chains_order: seq 1 before seq 0; a chat between two votes, taken last to first; and an object whose records are out of seq order.an_input_whose_gap_never_fills_is_refused_when_its_hold_runs_out: the hold boundary exactly; the chain counted from after the refused input; a late seq 0 refused once a later input was forwarded, and forwarded when none was; an input whose stream is gone, refused by its author's next object, with its repeat decided the same way.Checks (build host via
rb, at the PR head's tree)cargo test --locked -p joule-moq-relay --all-targetscargo build --locked -p joule-moq-relay --all-targetscargo clippy --locked -p joule-moq-relay --all-targets;cd crates/joule-moq-room && cargo clippy --locked --all-targetsjoule-moq,jwp,server.rs:172,session.rs:359)cd crates/joule-moq-room && cargo test --locked --all-targetscargo test … --test floor the_relay_counts_inputs_in_chain_order -- --exactlooped… the_relay_refuses_an_input_whose_gap_never_fills -- --exactlooped--test floor,--test age_rising,--test age_room_service,--test age_serving,--lib floorlooped 5×cargo metadata --locked --offlinefor the workspace and for joule-moq-roomcoBROWSER_BIN=<the host browser> e2e/run.sh --only coinput_out_of_orderappearscargo test --no-default-features --features co_performance --lib co_performance::and--test co_performancepython3 -I live-conformance/co_performance_verify.py live-conformance/coperformancescripts/check-public-names.shon the changed files, the commit messages and this bodyNo workflow file changes, so wfsteps does not apply.
Falsifications (each committed first, then broken in the working tree, run, and restored)
Chain::readyalways true):the_relay_counts_inputs_in_chain_orderfails (exit 101) withseq 0 forwarded, seq 1 withheld. The relay forwarded the record with a non-zeroprev(seq 1) and refused seq 0, which is the original bug.the_relay_refuses_an_input_whose_gap_never_fillsand both unit tests also fail.the relay refuses seq 1), and so does the unit hold test.an_input_whose_gap_never_fills_is_refused_when_its_hold_runs_outfails (the late seq 0 is forwarded).inputs_are_counted_in_their_chains_orderfails (in the object out of seq order, seq 1 is kept and seq 0 refused).Not done
rate_exceeded) still sees non-input records as they arrive. Only inputs are held. The existingt ≤ e.tfilter covers chains ordered byt; records with equaltare left as they were.🤖 Generated with Claude Code