open-standards/joule-code/joule-code-rs/crates/joule-moq-relay/tests/namespace_withdrawn.rs
Transaction Science 811164ff32 joule-moq-relay: a namespace is withdrawn by STOP_SENDING after its publisher's FIN
The interop run of 11 November (D30) saw a PUBLISH_NAMESPACE withdrawn by STOP_SENDING after a
FIN go unnoticed. -22 §6.4.2.3: "An endpoint that has already sent a FIN on its sending
direction and subsequently wishes to cancel sends STOP_SENDING on the receiving direction"; a
FIN alone is no withdrawal (§4.1). Once the publisher's FIN came the relay waited for the
session's end alone.

ReqTx::stopped waits for the peer's STOP_SENDING on this end's direction (asking the stream a
quarter second at a time, so no send on it waits longer); publish_namespace, after the
publisher's FIN, withdraws the namespace when it comes, and its subscribers are told
NAMESPACE_DONE.

tests/namespace_withdrawn.rs, over native QUIC and the WebSocket binding: the namespace stands
after the publisher's FIN, and its STOP_SENDING then withdraws it. Falsified: with the
direction not watched, no NAMESPACE_DONE comes and the test fails.

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

44 lines
2.4 KiB
Rust

//! A PUBLISH_NAMESPACE withdrawn after its FIN (`draft-ietf-moq-transport-22` §6.4.2.3: "An
//! endpoint that has already sent a FIN on its sending direction and subsequently wishes to
//! cancel sends STOP_SENDING on the receiving direction"). Over native QUIC and the WebSocket
//! binding, a publisher's namespace stands after the publisher's FIN, as a FIN is no
//! withdrawal (§4.1); its STOP_SENDING then withdraws it, and a namespace subscriber is told
//! NAMESPACE_DONE. The relay used to wait for the session's end alone once the FIN came.
mod common;
use common::*;
use joule_moq::draft22::{ControlMessage, Parameters};
use joule_moq_relay::client::ClientOptions;
use std::time::Duration;
use tokio::time::timeout;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_namespace_is_withdrawn_by_stop_sending_after_its_fin() {
timeout(Duration::from_secs(90), run()).await.expect("the test finishes in time");
}
async fn run() {
let server = relay(|_| {}).await;
for (i, via) in [Via::Quic, Via::WebSocket].into_iter().enumerate() {
let top = format!("withdrawn{i}");
let prefix = ns(&[top.as_str(), "x"]);
let (p, _pe) = connect(&server, via, ClientOptions::default()).await;
let (s, _se) = connect(&server, via, ClientOptions::default()).await;
let (ptx, mut prx) = p.publish_namespace(prefix.clone(), Parameters::new()).await.expect("PUBLISH_NAMESPACE");
let (_stx, mut srx) = s.subscribe_namespace(ns(&[top.as_str()])).await.expect("SUBSCRIBE_NAMESPACE");
let seen = timeout(STEP, srx.next()).await.expect("NAMESPACE in time").expect("it reads");
assert!(matches!(seen, Some(ControlMessage::Namespace(_))), "{via:?}: {seen:?}");
// The publisher's FIN: no withdrawal.
ptx.fin().await;
let quiet = timeout(Duration::from_millis(500), srx.next()).await;
assert!(quiet.is_err(), "{via:?}: a FIN withdrew the namespace: {quiet:?}");
// Its STOP_SENDING on the direction it receives: a withdrawal.
prx.reader.stop(0x1);
let done = timeout(STEP, srx.next()).await.unwrap_or_else(|_| panic!("{via:?}: no NAMESPACE_DONE after the publisher's STOP_SENDING")).expect("it reads");
assert!(matches!(done, Some(ControlMessage::NamespaceDone(_))), "{via:?}: {done:?}");
p.close(0, "done");
s.close(0, "done");
}
server.shutdown().await;
}