open-standards/joule-code/joule-code-rs/crates/joule-moq-relay/tests/subscribe_ended_at_once.rs
Transaction Science c28ba66852
Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 36s
WAI Studio / does this change touch it (push) Successful in 31s
wai-fleet service / does this change touch it (push) Successful in 45s
WAI examples / WAI examples · does this change touch them (push) Successful in 44s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m59s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 6m39s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 8m35s
wai-app and Create / wai-app · does this change touch it (push) Successful in 8m34s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 8m41s
wai-fleet service / service · linux (push) Successful in 3m23s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 8m39s
WAI Studio / Studio in WebKit · linux (push) Successful in 15m54s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 45s
WAI examples / WAI examples · portable · linux (push) Successful in 33m16s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 51s
wai-fleet service / service · macos (push) Successful in 43s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 9m3s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 17m3s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 16m14s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 21m24s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 23m22s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 16m29s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 47s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 12m14s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m4s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m26s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m47s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 8m37s
wai release binaries build on Windows / the seven release binaries · windows (push) Has been cancelled
joule-moq-relay: an upstream that answers SUBSCRIBE_OK and at once PUBLISH_DONE is passed on as such
The interop run of 11 November (D30) saw, at -23 and at -16, a downstream SUBSCRIBE answered
REQUEST_ERROR DOES_NOT_EXIST "the publisher left" where the upstream had answered SUBSCRIBE_OK
and at once PUBLISH_DONE: the upstream subscription was Established and ended before the
downstream one was made of it, a race. -22 §7.4: "The relay MUST have an Established upstream
subscription before sending SUBSCRIBE_OK in response to a downstream SUBSCRIBE", which it had;
§9.9: PUBLISH_DONE is the final message of a subscription.

session.rs: where the upstream subscription this SUBSCRIBE waited for has ended since (the
track's upstreams empty and its PUBLISH_DONE status kept), the subscriber is sent SUBSCRIBE_OK
(the Largest Object where one is known) and then PUBLISH_DONE with that status and no stream,
and the stream is finished.

tests/subscribe_ended_at_once.rs: a publisher of a namespace answers each of the relay's
SUBSCRIBEs with SUBSCRIBE_OK and PUBLISH_DONE at once, 24 times over native QUIC and over the
WebSocket binding; each subscriber gets SUBSCRIBE_OK and then PUBLISH_DONE (TRACK_ENDED, no
stream). Falsified: without the change 8 and then 10 of the 24 QUIC subscribers were refused
DOES_NOT_EXIST "the publisher left" in two runs, and the test fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 16:58:38 -05:00

62 lines
3.4 KiB
Rust

//! An upstream subscription answered SUBSCRIBE_OK and at once PUBLISH_DONE. The relay has its
//! Established upstream subscription before it answers the downstream SUBSCRIBE (`draft-ietf-
//! moq-transport-22` §7.4); when that subscription ends before the downstream one is made of it,
//! the subscriber is told what happened, SUBSCRIBE_OK and then PUBLISH_DONE with the publisher's
//! status and no stream (§9.9), not REQUEST_ERROR DOES_NOT_EXIST "the publisher left", which
//! the interop run saw at -23 and at -16. A publisher of a namespace answers each of the relay's
//! SUBSCRIBEs so, many times over native QUIC and the WebSocket binding, each subscriber held to
//! that answer.
mod common;
use common::*;
use joule_moq::draft22::{ControlMessage, Parameters, PublishDone, PublishDoneCode, SubscribeOk};
use joule_moq_relay::client::ClientOptions;
use std::time::Duration;
use tokio::time::timeout;
const ROUNDS: usize = 24;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn an_upstream_that_ends_at_once_is_answered_ok_then_done() {
timeout(Duration::from_secs(150), run()).await.expect("the test finishes in time");
}
async fn run() {
let server = relay(|_| {}).await;
for via in [Via::Quic, Via::WebSocket] {
let prefix = ns(&["ended-at-once", if via == Via::Quic { "q" } else { "w" }]);
let (p, _pe) = connect(&server, via, ClientOptions::default()).await;
let (s, _se) = connect(&server, via, ClientOptions::default()).await;
let _ns = p.publish_namespace(prefix.clone(), Parameters::new()).await.expect("PUBLISH_NAMESPACE");
let mut refused = Vec::new();
for i in 0..ROUNDS {
let name = name(&format!("t{i}"));
let (pfx, nm) = (prefix.clone(), name.clone());
let s2 = &s;
let asked = async move { s2.subscribe(pfx, nm, Parameters::new()).await };
let answered = async {
let (tx, _rx, first) = p.next_request(STEP).await.expect("the relay's SUBSCRIBE in time");
assert!(matches!(first, ControlMessage::Subscribe(_)), "the relay sent {}", first.name());
tx.send(&ControlMessage::SubscribeOk(SubscribeOk { track_alias: 1000 + i as u64, params: Parameters::new(), track_properties: track_props(64, None) })).await.unwrap();
tx.send(&ControlMessage::PublishDone(PublishDone { status: PublishDoneCode::TRACK_ENDED, stream_count: 0, reason: Vec::new() })).await.unwrap();
tx.fin().await;
};
let (got, ()) = tokio::join!(asked, answered);
match got {
Ok((_ok, _tx, mut rx)) => {
let done = timeout(STEP, rx.next()).await.expect("PUBLISH_DONE in time").expect("it reads");
match done {
Some(ControlMessage::PublishDone(d)) => assert_eq!((d.status, d.stream_count), (PublishDoneCode::TRACK_ENDED, 0), "{via:?} round {i}"),
other => panic!("{via:?} round {i}: after SUBSCRIBE_OK, {other:?}"),
}
}
Err(why) => refused.push(format!("round {i}: {why}")),
}
}
assert!(refused.is_empty(), "{via:?}: {} of {ROUNDS} subscribers refused where the upstream answered: {:?}", refused.len(), &refused[..refused.len().min(3)]);
p.close(0, "done");
s.close(0, "done");
}
server.shutdown().await;
}