use crate::{group, origin, track};
use std::{collections::HashMap, task::Poll};
use futures::{FutureExt, StreamExt, stream::FuturesUnordered};
use crate::{
AsPath, Error, Timescale,
coding::{Stream, Writer},
ietf::{self, Control, FetchHeader, FetchType, FilterType, GroupOrder, Location, RequestId},
track::Subscription,
util::{MaybeBoxedExt, MaybeSendBox},
};
use super::{Message, Version, cluster, peer};
struct Watched {
broadcast: crate::broadcast::Consumer,
demand: crate::broadcast::Demand,
sent: Advert,
idle_at: Option<web_async::time::Instant>,
dead: bool,
deferred: bool,
refused: Refused,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum Refused {
#[default]
No,
Until(web_async::time::Instant),
Never,
}
impl Refused {
fn offerable(&self, now: web_async::time::Instant) -> bool {
match self {
Self::No => true,
Self::Until(at) => now >= *at,
Self::Never => false,
}
}
fn pending(&self) -> bool {
*self != Self::Never
}
}
impl Watched {
fn new(broadcast: crate::broadcast::Consumer) -> Self {
Self {
demand: broadcast.demand(),
broadcast,
sent: Advert::None,
idle_at: None,
dead: false,
deferred: false,
refused: Refused::No,
}
}
fn set_sent(&mut self, sent: Advert) {
if !sent.discounted() {
self.idle_at = None;
}
self.sent = sent;
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
enum Advert {
#[default]
None,
Plain,
Cluster(cluster::Advert),
}
impl Advert {
fn wanted(&self) -> bool {
!matches!(self, Self::None)
}
fn params(&self) -> Option<cluster::Advert> {
match self {
Self::Cluster(advert) => Some(advert.clone()),
_ => None,
}
}
fn discounted(&self) -> bool {
matches!(self, Self::Cluster(advert) if advert.cost == 0)
}
}
enum Watch {
Changed(crate::PathOwned),
Idle(crate::PathOwned),
}
const ADVERTISE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const RETRY_BASE: std::time::Duration = std::time::Duration::from_millis(100);
const RETRY_MAX: std::time::Duration = std::time::Duration::from_secs(5);
fn jitter(delay: std::time::Duration) -> std::time::Duration {
use rand::RngExt;
delay.mul_f64(0.5 + rand::rng().random::<f64>() / 2.0)
}
enum Target<S: web_transport_trait::Session> {
Inline(Stream<S, Version>),
Requests(Option<Stream<S, Version>>),
}
impl<S: web_transport_trait::Session> Target<S> {
fn stream(&mut self) -> Option<&mut Stream<S, Version>> {
match self {
Self::Inline(stream) | Self::Requests(Some(stream)) => Some(stream),
Self::Requests(None) => None,
}
}
async fn closed(&mut self) -> Result<(), Error> {
match self.stream() {
Some(stream) => stream.reader.closed().await,
None => std::future::pending().await,
}
}
}
struct Namespaces<S: web_transport_trait::Session> {
peer: cluster::Peer,
target: Target<S>,
watched: HashMap<crate::PathOwned, Watched>,
requests: HashMap<crate::PathOwned, NamespaceRequest<S>>,
}
impl<S: web_transport_trait::Session> Namespaces<S> {
fn new(peer: cluster::Peer, target: Target<S>) -> Self {
Self {
peer,
target,
watched: HashMap::new(),
requests: HashMap::new(),
}
}
}
enum NamespaceEvent {
Closed(Result<(), Error>),
Update(Option<crate::announce::Update>),
Routes(crate::PathOwned),
Idle(crate::PathOwned),
Linger,
Retry,
}
#[derive(Clone)]
pub(super) struct Publisher<S: web_transport_trait::Session> {
session: S,
origin: origin::Consumer,
control: Control,
self_origin: crate::Origin,
peer_origin: Option<crate::Origin>,
peer_setup: peer::PeerSetup,
version: Version,
}
impl<S: web_transport_trait::Session> Publisher<S> {
pub fn new(
session: S,
origin: origin::Consumer,
control: Control,
peer_origin: Option<crate::Origin>,
peer_setup: peer::PeerSetup,
version: Version,
) -> Self {
Self {
session,
self_origin: *origin,
origin,
control,
peer_origin,
peer_setup,
version,
}
}
async fn peer(&self) -> cluster::Peer {
match cluster::supported(self.version) {
true => self.peer_setup.get().await.cluster,
false => cluster::Peer::default(),
}
}
async fn requires_solicitation(&self) -> bool {
self.peer_setup.get().await.solicit.unwrap_or(false)
}
async fn serving_origin(&self) -> origin::Consumer {
let peer = self.peer().await;
match self.exclude(&peer) {
crate::Origin::UNKNOWN => self.origin.clone(),
exclude => self.origin.clone().excluding(exclude),
}
}
fn exclude(&self, peer: &cluster::Peer) -> crate::Origin {
match peer.negotiated() {
true => peer.exclude(),
false => self.peer_origin.unwrap_or(crate::Origin::UNKNOWN),
}
}
fn select(&self, watch: &Watched, peer: &cluster::Peer) -> Advert {
let routes = watch.broadcast.routes();
let exclude = self.exclude(peer);
for (route, serving) in crate::broadcast::advertisable_routes(&routes, self.self_origin, exclude) {
if !peer.negotiated() {
return Advert::Plain;
}
let cost = crate::broadcast::outgoing_cost(&watch.demand, route, serving);
match cluster::Advert::forward(&route.hops, cost, self.self_origin) {
Ok(advert) => return Advert::Cluster(advert),
Err(_) => continue,
}
}
Advert::None
}
fn poll_watched(
watched: &mut HashMap<crate::PathOwned, Watched>,
fired: Option<web_async::time::Instant>,
waiter: &kio::Waiter,
) -> Poll<Watch> {
for (path, watch) in watched.iter_mut() {
if watch.dead {
continue;
}
match watch.broadcast.poll_routes_changed(waiter) {
Poll::Ready(Ok(())) => return Poll::Ready(Watch::Changed(path.clone())),
Poll::Ready(Err(_)) => {
watch.dead = true;
continue;
}
Poll::Pending => {}
}
if !matches!(watch.sent, Advert::Cluster(_)) {
continue;
}
if !watch.sent.discounted() {
if let Poll::Ready(Ok(())) = watch.demand.poll_used(waiter) {
return Poll::Ready(Watch::Changed(path.clone()));
}
continue;
}
match watch.idle_at {
Some(_) if watch.demand.is_used() => watch.idle_at = None,
Some(at) if fired.is_some_and(|now| now >= at + crate::broadcast::COST_LINGER) => {
watch.idle_at = None;
return Poll::Ready(Watch::Changed(path.clone()));
}
Some(_) => {
let _ = watch.demand.poll_used(waiter);
continue;
}
None => {}
}
if let Poll::Ready(Ok(())) = watch.demand.poll_unused(waiter) {
return Poll::Ready(Watch::Idle(path.clone()));
}
}
Poll::Pending
}
fn linger_deadline(watched: &HashMap<crate::PathOwned, Watched>) -> Option<web_async::time::Instant> {
watched
.values()
.filter_map(|watch| watch.idle_at)
.min()
.map(|at| at + crate::broadcast::COST_LINGER)
}
pub fn handle_stream(
&self,
id: u64,
mut data: bytes::Bytes,
stream: Stream<S, Version>,
) -> Result<MaybeSendBox<'static, ()>, Error> {
let this = self.clone();
let task = match id {
ietf::Subscribe::ID => {
let msg = ietf::Subscribe::decode_msg(&mut data, this.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received subscribe");
async move {
if let Err(err) = this.run_subscribe_stream(stream, msg).await {
tracing::debug!(%err, "subscribe stream error");
}
}
.maybe_boxed()
}
ietf::Fetch::ID => {
let msg = ietf::Fetch::decode_msg(&mut data, this.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received fetch");
async move {
if let Err(err) = this.run_fetch_stream(stream, msg).await {
tracing::debug!(%err, "fetch stream error");
}
}
.maybe_boxed()
}
ietf::SubscribeNamespace::ID | ietf::SubscribeNamespaceLegacy::ID => {
let msg = if id == ietf::SubscribeNamespace::ID {
ietf::SubscribeNamespace::decode_msg(&mut data, this.version)?
} else {
let legacy = ietf::SubscribeNamespaceLegacy::decode_msg(&mut data, this.version)?;
ietf::SubscribeNamespace {
request_id: legacy.request_id,
namespace: legacy.namespace,
}
};
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received subscribe_namespace");
async move {
if let Err(err) = this.run_subscribe_namespace_stream(stream, msg).await {
tracing::debug!(%err, "subscribe_namespace stream error");
}
}
.maybe_boxed()
}
ietf::TrackStatus::ID => {
tracing::warn!("TrackStatus not supported");
async {}.maybe_boxed()
}
_ => {
tracing::warn!(id, "unexpected bidi stream type for publisher");
return Err(Error::UnexpectedStream);
}
};
Ok(task)
}
async fn run_subscribe_stream(self, mut stream: Stream<S, Version>, msg: ietf::Subscribe<'_>) -> Result<(), Error> {
match msg.filter_type {
FilterType::AbsoluteStart | FilterType::AbsoluteRange => {
tracing::warn!(?msg, "absolute subscribe not supported, ignoring");
}
FilterType::NextGroup => {
tracing::warn!(?msg, "next group subscribe not supported, ignoring");
}
FilterType::LargestObject => {}
};
let request_id = msg.request_id;
let track_name = msg.track_name.clone();
let absolute = self.origin.absolute(&msg.track_namespace).to_owned();
tracing::info!(id = %request_id, broadcast = %absolute, track = %track_name, "subscribe started");
let broadcast = match self
.serving_origin()
.await
.request_broadcast(&msg.track_namespace)
.await
{
Ok(broadcast) => broadcast,
Err(_) => {
return self
.reject_subscribe(stream, request_id, 404, "Broadcast not found")
.await;
}
};
let subscription = Subscription {
priority: super::priority::from_wire(msg.subscriber_priority),
..Default::default()
};
let track = match async { broadcast.track(&msg.track_name)?.subscribe(subscription).await }.await {
Ok(track) => track,
Err(err) => {
return self.reject_subscribe(stream, request_id, 404, &err.to_string()).await;
}
};
stream.writer.encode(&ietf::SubscribeOk::ID).await?;
stream
.writer
.encode(&ietf::SubscribeOk {
request_id: match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id),
_ => None,
},
track_alias: request_id.0,
properties: ietf::Properties {
timescale: Some(track.info().timescale),
group_order: Some(GroupOrder::Descending),
},
})
.await?;
let res = {
let mut serve = std::pin::pin!(self.run_track(track, request_id));
let mut reader_closed = std::pin::pin!(stream.reader.closed());
let mut session_closed = std::pin::pin!(self.session.closed());
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(serve.as_mut()) {
return Poll::Ready(res);
}
if waiter.poll_future(reader_closed.as_mut()).is_ready()
|| waiter.poll_future(session_closed.as_mut()).is_ready()
{
return Poll::Ready(Ok(()));
}
Poll::Pending
})
.await
};
let (status_code, reason) = match &res {
Ok(()) => (200, "OK"),
Err(_) => (500, "error"),
};
let _ = stream.writer.encode(&ietf::PublishDone::ID).await;
let _ = stream
.writer
.encode(&ietf::PublishDone {
request_id: match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id),
_ => None,
},
status_code,
stream_count: 0,
reason_phrase: reason.into(),
})
.await;
let _ = stream.writer.close().await;
res
}
async fn reject_subscribe(
&self,
mut stream: Stream<S, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
self.write_subscribe_error(&mut stream.writer, request_id, error_code, reason)
.await?;
let _ = stream.writer.close().await;
Ok(())
}
async fn write_subscribe_error(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::SubscribeError::ID).await?;
writer
.encode(&ietf::SubscribeError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
async fn run_track(&self, mut track: track::Subscriber, request_id: RequestId) -> Result<(), Error> {
let mut tasks = FuturesUnordered::new();
loop {
let group = {
kio::wait(|waiter| {
let mut cx = std::task::Context::from_waker(waiter.waker());
while let std::task::Poll::Ready(Some(())) = tasks.poll_next_unpin(&mut cx) {}
track.poll_recv_group(waiter)
})
.await
};
let Some(group) = group? else {
while tasks.next().await.is_some() {}
return Ok(());
};
let sequence = group.sequence;
tracing::debug!(subscribe = %request_id, track = %track.name(), sequence, "serving group");
let msg = ietf::GroupHeader {
track_alias: request_id.0,
group_id: sequence,
sub_group_id: 0,
publisher_priority: 0,
flags: ietf::GroupFlags {
has_extensions: true,
..Default::default()
},
};
let priority = track.subscription().priority;
let timescale = track.info().timescale;
tasks
.push(Self::run_group(self.session.clone(), msg, priority, group, timescale, self.version).map(|_| ()));
}
}
async fn run_group(
session: S,
msg: ietf::GroupHeader,
priority: u8,
mut group: group::Consumer,
timescale: Timescale,
version: Version,
) -> Result<(), Error> {
let stream = session.open_uni().await.map_err(Error::from_transport)?;
let mut stream = Writer::new(stream, version);
stream.set_priority(priority);
stream.encode(&msg).await?;
loop {
let frame = {
let mut closed = std::pin::pin!(stream.closed());
kio::wait(|waiter| {
if waiter.poll_future(closed.as_mut()).is_ready() {
return Poll::Ready(Err(Error::Cancel));
}
group.poll_next_frame(waiter)
})
.await
};
let mut frame = match frame? {
Some(frame) => frame,
None => break,
};
stream.encode(&0u64).await?;
if msg.flags.has_extensions {
let mut ext = bytes::BytesMut::new();
ietf::encode_object_time(&mut ext, frame.timestamp, timescale, version)?;
stream.encode(&(ext.len() as u64)).await?;
stream.write_chunk(ext.freeze()).await?;
}
stream.encode(&frame.size).await?;
if frame.size == 0 {
stream.encode(&0u8).await?;
} else {
loop {
let chunk = {
let mut closed = std::pin::pin!(stream.closed());
kio::wait(|waiter| {
if waiter.poll_future(closed.as_mut()).is_ready() {
return Poll::Ready(Err(Error::Cancel));
}
frame.poll_read_chunk(waiter)
})
.await
};
match chunk? {
Some(chunk) => {
stream.write_chunk(chunk).await?;
}
None => break,
}
}
}
}
stream.close().await?;
tracing::debug!(sequence = %msg.group_id, "finished group");
Ok(())
}
async fn run_fetch_stream(self, mut stream: Stream<S, Version>, msg: ietf::Fetch<'_>) -> Result<(), Error> {
let _subscribe_id = match msg.fetch_type {
FetchType::Standalone { .. } => {
return self.reject_fetch(stream, msg.request_id, 500, "not supported").await;
}
FetchType::RelativeJoining {
subscriber_request_id,
group_offset,
} => {
if group_offset != 0 {
return self.reject_fetch(stream, msg.request_id, 500, "not supported").await;
}
subscriber_request_id
}
FetchType::AbsoluteJoining { .. } => {
return self.reject_fetch(stream, msg.request_id, 500, "not supported").await;
}
};
self.write_fetch_ok(&mut stream.writer, msg.request_id).await?;
let uni = self.session.open_uni().await.map_err(Error::from_transport)?;
let mut writer = Writer::new(uni, self.version);
writer.encode(&FetchHeader::TYPE).await?;
writer
.encode(&FetchHeader {
request_id: msg.request_id,
})
.await?;
writer.close().await?;
Ok(())
}
async fn write_fetch_ok(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::FetchOk::ID).await?;
writer
.encode(&ietf::FetchOk {
request_id: Some(request_id),
group_order: GroupOrder::Descending,
end_of_track: false,
end_location: Location { group: 0, object: 0 },
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestOk::ID).await?;
writer
.encode(&ietf::RequestOk {
request_id: Some(request_id),
})
.await?;
}
_ => {
writer.encode(&ietf::RequestOk::ID).await?;
writer.encode(&ietf::RequestOk { request_id: None }).await?;
}
}
Ok(())
}
async fn reject_fetch(
&self,
mut stream: Stream<S, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
self.write_fetch_error(&mut stream.writer, request_id, error_code, reason)
.await?;
let _ = stream.writer.close().await;
Ok(())
}
async fn write_fetch_error(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::FetchError::ID).await?;
writer
.encode(&ietf::FetchError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
async fn sync_namespace(
&self,
ns: &mut Namespaces<S>,
suffix: &crate::PathOwned,
path: &crate::PathOwned,
) -> Result<(), Error> {
let Namespaces {
peer,
target,
watched,
requests,
} = ns;
let Some(watch) = watched.get(suffix) else {
return Ok(());
};
let advert = self.select(watch, peer);
let refused = watch.refused;
let wanted = advert.wanted();
let held = watch.sent.wanted();
let unchanged = advert == watch.sent;
if unchanged {
if !wanted && let Some(watch) = watched.get_mut(suffix) {
watch.deferred = false;
}
return Ok(());
}
if wanted && !held && !refused.offerable(web_async::time::Instant::now()) {
return Ok(());
}
let absolute = self.origin.absolute(path).to_owned();
let mut refused = watch.refused;
let sent = match target {
Target::Requests(_) => {
match (advert.wanted(), requests.get_mut(suffix)) {
(false, _) => {
if held {
tracing::debug!(broadcast = %absolute, "namespace_done");
}
self.withdraw_namespace(target, requests, suffix.clone()).await?;
}
(true, Some(request)) => {
tracing::debug!(broadcast = %absolute, "announce update");
request.stream.writer.encode(&ietf::PublishNamespace::ID).await?;
request
.stream
.writer
.encode(&ietf::PublishNamespace {
request_id: request.request_id,
track_namespace: request.path.as_path(),
cluster: advert.params(),
})
.await?;
}
(true, None) => {
tracing::debug!(broadcast = %absolute, "publish_namespace");
refused = self
.advertise_namespace(requests, path, suffix.clone(), advert.params())
.await?;
}
}
match requests.contains_key(suffix) {
true => advert,
false => Advert::None,
}
}
Target::Inline(stream) => {
match (advert.wanted(), held) {
(true, _) => {
tracing::debug!(broadcast = %absolute, "namespace");
stream.writer.encode(&ietf::Namespace::ID).await?;
stream
.writer
.encode(&ietf::Namespace {
suffix: suffix.as_path(),
cluster: advert.params(),
})
.await?;
}
(false, true) => {
tracing::debug!(broadcast = %absolute, "namespace_done");
stream.writer.encode(&ietf::NamespaceDone::ID).await?;
stream
.writer
.encode(&ietf::NamespaceDone {
suffix: suffix.as_path(),
})
.await?;
}
(false, false) => {}
}
advert
}
};
if let Some(watch) = watched.get_mut(suffix) {
watch.refused = refused;
watch.deferred = wanted && !sent.wanted() && refused.pending();
watch.set_sent(sent);
}
Ok(())
}
async fn advertise_namespace(
&self,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
path: &crate::PathOwned,
suffix: crate::PathOwned,
cluster: Option<cluster::Advert>,
) -> Result<Refused, Error> {
let request_id = self.control.next_request_id().await?;
let Some(request) = self.open_request().await? else {
tracing::debug!(broadcast = %self.origin.absolute(path), "no stream for the advertisement");
return Ok(Refused::No);
};
let mut request = request;
request.writer.encode(&ietf::PublishNamespace::ID).await?;
request
.writer
.encode(&ietf::PublishNamespace {
request_id,
track_namespace: path.as_path(),
cluster,
})
.await?;
let Some((type_id, mut data)) = Self::read_response(&mut request).await? else {
tracing::debug!(broadcast = %self.origin.absolute(path), "no answer to the advertisement");
return Ok(Refused::No);
};
match (self.version, type_id) {
(Version::Draft14, ietf::PublishNamespaceOk::ID) => {
let msg = ietf::PublishNamespaceOk::decode_msg(&mut data, self.version)?;
tracing::debug!(message = ?msg, "publish namespace ok");
}
(Version::Draft14, ietf::PublishNamespaceError::ID) => {
let msg = ietf::PublishNamespaceError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "publish namespace error");
return Ok(Refused::No);
}
(_, ietf::RequestOk::ID) => {
let msg = ietf::RequestOk::decode_msg(&mut data, self.version)?;
tracing::debug!(message = ?msg, "publish namespace ok");
}
(_, ietf::RequestError::ID) => {
let msg = ietf::RequestError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "publish namespace error");
return Ok(self.refusal(msg.retry_interval));
}
_ => return Err(Error::UnexpectedMessage),
}
requests.insert(
suffix,
NamespaceRequest {
path: path.clone(),
request_id,
stream: request,
},
);
Ok(Refused::No)
}
fn refusal(&self, retry_interval: u64) -> Refused {
match (self.version, retry_interval) {
(Version::Draft14 | Version::Draft15, _) => Refused::No,
(_, 0) => Refused::Never,
(_, ms) => Refused::Until(web_async::time::Instant::now() + std::time::Duration::from_millis(ms)),
}
}
async fn open_request(&self) -> Result<Option<Stream<S, Version>>, Error> {
let mut open = std::pin::pin!(Stream::open(&self.session, self.version));
let mut timeout = kio::time::Deadline::after(ADVERTISE_TIMEOUT);
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(open.as_mut()) {
return Poll::Ready(res.map(Some));
}
if timeout.poll(waiter).is_ready() {
return Poll::Ready(Ok(None));
}
Poll::Pending
})
.await
}
async fn read_response(request: &mut Stream<S, Version>) -> Result<Option<(u64, bytes::Bytes)>, Error> {
let mut read = std::pin::pin!(async {
let type_id: u64 = request.reader.decode().await?;
let size: u16 = request.reader.decode().await?;
let data = request.reader.read_exact(size as usize).await?;
Ok::<_, Error>((type_id, data))
});
let mut timeout = kio::time::Deadline::after(ADVERTISE_TIMEOUT);
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(read.as_mut()) {
return Poll::Ready(res.map(Some));
}
if timeout.poll(waiter).is_ready() {
return Poll::Ready(Ok(None));
}
Poll::Pending
})
.await
}
async fn withdraw_namespace(
&self,
target: &mut Target<S>,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
suffix: crate::PathOwned,
) -> Result<(), Error> {
match target {
Target::Requests(_) => {
if let Some(mut request) = requests.remove(&suffix) {
if matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16) {
let _ = request
.stream
.writer
.encode_message(&ietf::PublishNamespaceDone {
track_namespace: request.path.as_path(),
request_id: request.request_id,
})
.await;
}
let _ = request.stream.writer.close().await;
}
}
Target::Inline(stream) => {
stream.writer.encode(&ietf::NamespaceDone::ID).await?;
stream
.writer
.encode(&ietf::NamespaceDone {
suffix: suffix.as_path(),
})
.await?;
}
}
Ok(())
}
async fn withdraw_requests(
&self,
target: &mut Target<S>,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
) {
let suffixes: Vec<crate::PathOwned> = requests.keys().cloned().collect();
for suffix in suffixes {
let _ = self.withdraw_namespace(target, requests, suffix).await;
}
}
pub async fn run_publish_namespaces(self) -> Result<(), Error> {
if self.requires_solicitation().await {
return Ok(());
}
let peer = self.peer().await;
let origin = match self.exclude(&peer) {
crate::Origin::UNKNOWN => self.origin.clone(),
exclude => self.origin.clone().excluding(exclude),
};
let ns = Namespaces::new(peer, Target::Requests(None));
self.run_namespaces(origin, crate::Path::empty().to_owned(), ns).await
}
async fn run_subscribe_namespace_stream(
self,
mut stream: Stream<S, Version>,
msg: ietf::SubscribeNamespace<'_>,
) -> Result<(), Error> {
let prefix = msg.namespace.to_owned();
tracing::debug!(prefix = %self.origin.absolute(&prefix), "subscribe_namespace stream");
let origin = self
.origin
.scope(&[prefix.as_path()])
.unwrap_or_else(|| self.origin.empty());
match self.version {
Version::Draft14 => {
stream.writer.encode(&ietf::SubscribeNamespaceOk::ID).await?;
stream
.writer
.encode(&ietf::SubscribeNamespaceOk {
request_id: msg.request_id,
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream
.writer
.encode(&ietf::RequestOk {
request_id: Some(msg.request_id),
})
.await?;
}
_ => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream.writer.encode(&ietf::RequestOk { request_id: None }).await?;
}
}
let peer = self.peer().await;
let origin = match self.exclude(&peer) {
crate::Origin::UNKNOWN => origin,
exclude => origin.excluding(exclude),
};
let target = match self.version {
Version::Draft14 | Version::Draft15 => Target::Requests(Some(stream)),
_ => Target::Inline(stream),
};
let origin = match self.requires_solicitation().await {
true => origin,
false => origin.empty(),
};
let ns = Namespaces::new(peer, target);
self.run_namespaces(origin, prefix, ns).await
}
async fn run_namespaces(
&self,
origin: origin::Consumer,
prefix: crate::PathOwned,
mut ns: Namespaces<S>,
) -> Result<(), Error> {
let mut announced = origin.announced();
let mut linger = kio::time::Deadline::new();
let mut retry = kio::time::Deadline::new();
let mut retry_at: Option<web_async::time::Instant> = None;
let mut retry_delay = RETRY_BASE;
let res = loop {
linger.set(Self::linger_deadline(&ns.watched));
match ns.watched.values().any(|watch| watch.deferred) {
true => retry_at = retry_at.or_else(|| Some(web_async::time::Instant::now() + jitter(retry_delay))),
false => {
retry_at = None;
retry_delay = RETRY_BASE;
}
}
retry.set(retry_at);
let event = {
let mut closed = std::pin::pin!(ns.target.closed());
let watched = &mut ns.watched;
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(closed.as_mut()) {
return Poll::Ready(NamespaceEvent::Closed(res));
}
if let Poll::Ready(update) = announced.poll_next(waiter) {
return Poll::Ready(NamespaceEvent::Update(update));
}
if retry.poll(waiter).is_ready() {
return Poll::Ready(NamespaceEvent::Retry);
}
let fired = linger.poll(waiter).is_ready().then(web_async::time::Instant::now);
match Self::poll_watched(watched, fired, waiter) {
Poll::Ready(Watch::Changed(path)) => return Poll::Ready(NamespaceEvent::Routes(path)),
Poll::Ready(Watch::Idle(path)) => return Poll::Ready(NamespaceEvent::Idle(path)),
Poll::Pending => {}
}
match fired.is_some() {
true => Poll::Ready(NamespaceEvent::Linger),
false => Poll::Pending,
}
})
.await
};
match event {
NamespaceEvent::Closed(res) => break res,
NamespaceEvent::Linger => continue,
NamespaceEvent::Retry => {
retry_at = None;
retry_delay = (retry_delay * 2).min(RETRY_MAX);
let deferred: Vec<crate::PathOwned> = ns
.watched
.iter()
.filter(|(_, watch)| watch.deferred)
.map(|(suffix, _)| suffix.clone())
.collect();
for suffix in deferred {
let path = prefix.join(&suffix);
self.sync_namespace(&mut ns, &suffix, &path).await?;
}
}
NamespaceEvent::Update(None) => {
self.withdraw_requests(&mut ns.target, &mut ns.requests).await;
let Some(stream) = ns.target.stream() else {
return Ok(());
};
stream.writer.finish()?;
return stream.writer.closed().await;
}
NamespaceEvent::Update(Some(crate::announce::Update { path, broadcast })) => {
let suffix = path
.strip_prefix(&prefix)
.expect("origin returned invalid path")
.to_owned();
let path = path.to_owned();
match broadcast {
Some(broadcast) => {
ns.watched.insert(suffix.clone(), Watched::new(broadcast));
self.sync_namespace(&mut ns, &suffix, &path).await?;
}
None => {
let held = ns.watched.remove(&suffix).is_some_and(|watch| watch.sent.wanted());
if held {
tracing::debug!(broadcast = %self.origin.absolute(&path), "namespace_done");
self.withdraw_namespace(&mut ns.target, &mut ns.requests, suffix)
.await?;
}
}
}
}
NamespaceEvent::Routes(suffix) => {
let path = prefix.join(&suffix);
self.sync_namespace(&mut ns, &suffix, &path).await?;
}
NamespaceEvent::Idle(suffix) => {
if let Some(watch) = ns.watched.get_mut(&suffix) {
watch.idle_at = Some(web_async::time::Instant::now());
}
}
}
};
self.withdraw_requests(&mut ns.target, &mut ns.requests).await;
res
}
}
struct NamespaceRequest<S: web_transport_trait::Session> {
path: crate::PathOwned,
request_id: RequestId,
stream: Stream<S, Version>,
}
#[cfg(test)]
mod group_priority_test {
use super::*;
use crate::lite::test_transport::SinkSession;
#[tokio::test]
async fn group_stream_preserves_model_priority() {
let log = crate::lite::test_transport::Log::default();
let session = SinkSession::new(log.clone());
let mut track = track::Producer::new(std::sync::Arc::new(crate::broadcast::Info::default()), "test", None);
let mut group = track.create_group(group::Info { sequence: 0 }).unwrap();
group
.write_frame(crate::Timestamp::from_millis(0).unwrap(), b"hello".as_slice())
.unwrap();
let consumer = group.consume();
group.finish().unwrap();
let msg = ietf::GroupHeader {
track_alias: 0,
group_id: 0,
sub_group_id: 0,
publisher_priority: 0,
flags: Default::default(),
};
Publisher::<SinkSession>::run_group(session, msg, 200, consumer, Timescale::default(), Version::Draft14)
.await
.unwrap();
assert_eq!(
log.priorities(),
vec![200],
"model priority must pass through unchanged"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lite::test_transport::SinkSession;
async fn settle() {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
fn occurrences(log: &crate::lite::test_transport::Log, needle: &[u8]) -> usize {
let writes = log.writes.lock().unwrap();
writes.windows(needle.len()).filter(|window| *window == needle).count()
}
fn declared(solicit: Option<bool>) -> peer::PeerSetup {
let slot = peer::PeerSetup::default();
slot.set(peer::Peer {
solicit,
..Default::default()
});
slot
}
fn requires_solicitation() -> peer::PeerSetup {
declared(Some(true))
}
#[tokio::test]
async fn assigned_peer_origin_filters_echoed_announces() {
let assigned = crate::Origin::new(777).unwrap();
let other = crate::Origin::new(778).unwrap();
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let consumer = origin.consume();
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
Some(assigned),
peer::PeerSetup::default(),
Version::Draft16,
);
let mut echoed_hops = crate::OriginList::new();
echoed_hops.push(assigned).unwrap();
let _echoed = origin
.create_broadcast(
"from/peer",
crate::broadcast::Route::new()
.with_hops(echoed_hops)
.with_announce(true),
)
.unwrap();
let mut local_hops = crate::OriginList::new();
local_hops.push(other).unwrap();
let _local = origin
.create_broadcast(
"from/us",
crate::broadcast::Route::new().with_hops(local_hops).with_announce(true),
)
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
let peer = cluster::Peer::default();
let echoed = consumer.get_broadcast("from/peer").unwrap();
assert!(!publisher.select(&Watched::new(echoed), &peer).wanted());
let local = consumer.get_broadcast("from/us").unwrap();
assert_eq!(publisher.select(&Watched::new(local), &peer), Advert::Plain);
}
#[tokio::test]
async fn linger_clears_when_the_advert_stops_being_discounted() {
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let broadcast = origin
.clone()
.create_broadcast("cam", crate::broadcast::Route::announced())
.unwrap();
let mut watch = Watched::new(broadcast.consume());
let hops = crate::OriginList::try_from(vec![crate::Origin::new(7).unwrap()]).unwrap();
watch.set_sent(Advert::Cluster(cluster::Advert {
hops: cluster::HopPath::new(hops.clone()),
cost: 0,
}));
watch.idle_at = Some(web_async::time::Instant::now());
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert!(Publisher::<SinkSession>::linger_deadline(&watched).is_some());
let mut watch = watched.into_values().next().unwrap();
watch.set_sent(Advert::Cluster(cluster::Advert {
hops: cluster::HopPath::new(hops),
cost: 9,
}));
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert_eq!(
Publisher::<SinkSession>::linger_deadline(&watched),
None,
"a non-discounted advert must not leave a deadline behind"
);
let mut watch = watched.into_values().next().unwrap();
watch.idle_at = Some(web_async::time::Instant::now());
watch.set_sent(Advert::None);
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert_eq!(Publisher::<SinkSession>::linger_deadline(&watched), None);
}
#[tokio::test]
async fn namespace_follows_route_eligibility_changes() {
let assigned = crate::Origin::new(777).unwrap();
let clean_publisher = crate::Origin::new(778).unwrap();
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let gate = kio::Producer::new(true);
let session = SinkSession::gated_bi(gate.consume());
let log = session.log.clone();
let publisher = Publisher::new(
session.clone(),
origin.consume(),
Control::new(None, false),
Some(assigned),
requires_solicitation(),
Version::Draft16,
);
let mut tainted_hops = crate::OriginList::new();
tainted_hops.push(assigned).unwrap();
let _tainted = origin
.create_broadcast(
"route-flip-cam",
crate::broadcast::Route::new()
.with_hops(tainted_hops)
.with_announce(true),
)
.unwrap();
settle().await;
let stream = Stream::open(&session, Version::Draft16).await.unwrap();
let msg = ietf::SubscribeNamespace {
request_id: RequestId(1),
namespace: crate::Path::new(""),
};
let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg));
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(occurrences(&log, b"route-flip-cam"), 0);
let mut clean_hops = crate::OriginList::new();
clean_hops.push(clean_publisher).unwrap();
let clean = origin
.create_broadcast(
"route-flip-cam",
crate::broadcast::Route::new().with_hops(clean_hops).with_announce(true),
)
.unwrap();
settle().await;
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(
occurrences(&log, b"route-flip-cam"),
1,
"NAMESPACE after a clean route joins"
);
drop(clean);
settle().await;
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(
occurrences(&log, b"route-flip-cam"),
2,
"NAMESPACE_DONE after the last clean route detaches"
);
}
async fn publish_namespace_error(version: Version, retry_interval: u64) -> Vec<u8> {
let log = crate::lite::test_transport::Log::default();
let mut writer = crate::coding::Writer::new(crate::lite::test_transport::SinkSend::new(log.clone()), version);
writer.encode(&ietf::RequestError::ID).await.unwrap();
writer
.encode(&ietf::RequestError {
request_id: matches!(version, Version::Draft15 | Version::Draft16).then_some(RequestId(1)),
error_code: 403,
reason_phrase: "no".into(),
retry_interval,
})
.await
.unwrap();
log.writes.lock().unwrap().clone()
}
#[tokio::test(start_paused = true)]
async fn a_refusal_that_forbids_retrying_is_not_retried() {
const VERSION: Version = Version::Draft17;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let _cam = origin
.create_broadcast("lonely-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let refusal = publish_namespace_error(VERSION, 0).await;
let session =
crate::lite::test_transport::ScriptedSession::per_stream(vec![refusal.clone(), refusal.clone(), refusal]);
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(Some(false)),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"lonely-cam") > 0 {
break;
}
settle().await;
}
assert_eq!(occurrences(&log, b"lonely-cam"), 1, "the advertisement never went out");
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
tick().await;
}
assert_eq!(
occurrences(&log, b"lonely-cam"),
1,
"re-offered a namespace the peer asked not to be offered again"
);
}
async fn publish_namespace_ok(version: Version) -> Vec<u8> {
let log = crate::lite::test_transport::Log::default();
let mut writer = crate::coding::Writer::new(crate::lite::test_transport::SinkSend::new(log.clone()), version);
match version {
Version::Draft14 => {
writer.encode(&ietf::PublishNamespaceOk::ID).await.unwrap();
writer
.encode(&ietf::PublishNamespaceOk {
request_id: RequestId(1),
})
.await
.unwrap();
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestOk::ID).await.unwrap();
writer
.encode(&ietf::RequestOk {
request_id: Some(RequestId(1)),
})
.await
.unwrap();
}
_ => {
writer.encode(&ietf::RequestOk::ID).await.unwrap();
writer.encode(&ietf::RequestOk { request_id: None }).await.unwrap();
}
}
let writes = log.writes.lock().unwrap();
writes.clone()
}
#[tokio::test]
async fn v14_subscribe_namespace_is_answered_with_publish_namespace() {
const VERSION: Version = Version::Draft14;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let consumer = origin.consume();
let early = origin
.create_broadcast("early-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let ok = publish_namespace_ok(VERSION).await;
let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![Vec::new(), ok.clone(), ok]);
let log = session.log.clone();
let publisher = Publisher::new(
session.clone(),
consumer,
Control::new(None, false),
None,
requires_solicitation(),
VERSION,
);
let stream = Stream::open(&session, VERSION).await.unwrap();
let msg = ietf::SubscribeNamespace {
request_id: RequestId(1),
namespace: crate::Path::new(""),
};
let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg));
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"early-cam") >= 1 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"early-cam"),
1,
"PUBLISH_NAMESPACE after subscribing"
);
let _late = origin
.create_broadcast("late-cam", crate::broadcast::Route::announced())
.unwrap();
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"late-cam") >= 1 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"late-cam"),
1,
"PUBLISH_NAMESPACE for a live announce"
);
drop(early);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"early-cam") >= 2 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"early-cam"),
2,
"PUBLISH_NAMESPACE_DONE on unannounce"
);
assert_eq!(log.bi_opens(), 3, "no extra stream for the withdrawal");
}
#[tokio::test]
async fn a_peer_that_declared_nothing_is_told_unsolicited() {
const VERSION: Version = Version::Draft17;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let _local = origin
.create_broadcast("local-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let session =
crate::lite::test_transport::ScriptedSession::per_stream(vec![publish_namespace_ok(VERSION).await]);
let log = session.log.clone();
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer::default());
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
peer_setup,
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"local-cam") >= 1 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"local-cam"),
1,
"PUBLISH_NAMESPACE without a SUBSCRIBE_NAMESPACE"
);
assert_eq!(log.bi_opens(), 1, "one request stream");
}
async fn advertise_both_ways(solicit: Option<bool>) -> (usize, usize) {
const VERSION: Version = Version::Draft17;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let _cam = origin
.create_broadcast("cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![
Vec::new(),
publish_namespace_ok(VERSION).await,
]);
let log = session.log.clone();
let publisher = Publisher::new(
session.clone(),
origin.consume(),
Control::new(None, false),
None,
declared(solicit),
VERSION,
);
let stream = Stream::open(&session, VERSION).await.unwrap();
let msg = ietf::SubscribeNamespace {
request_id: RequestId(1),
namespace: crate::Path::new(""),
};
let mut solicited = std::pin::pin!(publisher.clone().run_subscribe_namespace_stream(stream, msg));
let mut unsolicited = std::pin::pin!(publisher.run_publish_namespaces());
let mut quiet = false;
for _ in 0..100 {
assert!(futures::poll!(solicited.as_mut()).is_pending());
if !quiet {
quiet = futures::poll!(unsolicited.as_mut()).is_ready();
}
settle().await;
}
(occurrences(&log, b"cam"), log.bi_opens())
}
#[tokio::test]
async fn each_namespace_is_advertised_exactly_once() {
let (unsolicited, streams) = advertise_both_ways(Some(false)).await;
assert_eq!(unsolicited, 1, "a peer that required nothing is told once");
assert_eq!(streams, 2, "on its own PUBLISH_NAMESPACE request");
let (solicited, streams) = advertise_both_ways(Some(true)).await;
assert_eq!(solicited, 1, "a peer that asked to be told on request is told once");
assert_eq!(streams, 1, "inline on the SUBSCRIBE_NAMESPACE stream it asked on");
}
#[tokio::test(start_paused = true)]
async fn a_parked_open_still_lets_a_namespace_be_withdrawn() {
const VERSION: Version = Version::Draft14;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let first = origin
.create_broadcast("first-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let gate = kio::Producer::new(true);
let ok = publish_namespace_ok(VERSION).await;
let session = crate::lite::test_transport::ScriptedSession::gated_open(vec![ok.clone(), ok], gate.consume());
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(Some(false)),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"first-cam") > 0 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"first-cam"),
1,
"the first advertisement never went out"
);
set_gate(&gate, false);
let _second = origin
.create_broadcast("second-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
drop(first);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"first-cam") >= 2 {
break;
}
tick().await;
}
assert_eq!(
occurrences(&log, b"first-cam"),
2,
"PUBLISH_NAMESPACE_DONE never sent: the open wedged the loop"
);
set_gate(&gate, true);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"second-cam") > 0 {
break;
}
tick().await;
}
assert_eq!(
occurrences(&log, b"second-cam"),
1,
"never retried once credit returned"
);
}
#[tokio::test(start_paused = true)]
async fn a_namespace_that_stops_being_advertisable_stops_being_deferred() {
let assigned = crate::Origin::new(777).unwrap();
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let mut hops = crate::OriginList::new();
hops.push(assigned).unwrap();
let _echoed = origin
.create_broadcast(
"from/peer",
crate::broadcast::Route::new().with_hops(hops).with_announce(true),
)
.unwrap();
settle().await;
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
Some(assigned),
declared(Some(false)),
Version::Draft17,
);
let suffix: crate::PathOwned = crate::Path::new("from/peer").to_owned();
let broadcast = origin.consume().get_broadcast("from/peer").unwrap();
let mut watch = Watched::new(broadcast);
watch.deferred = true;
let mut ns = Namespaces::new(cluster::Peer::default(), Target::Requests(None));
ns.watched.insert(suffix.clone(), watch);
publisher.sync_namespace(&mut ns, &suffix, &suffix).await.unwrap();
assert!(
!ns.watched[&suffix].deferred,
"the retry timer stays armed for a namespace that can never be advertised"
);
}
#[tokio::test(start_paused = true)]
async fn a_route_change_still_waits_out_a_refusal() {
const VERSION: Version = Version::Draft17;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let cam = origin
.create_broadcast("solo-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let refusal = publish_namespace_error(VERSION, 600_000).await;
let session =
crate::lite::test_transport::ScriptedSession::per_stream(vec![refusal.clone(), refusal.clone(), refusal]);
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(Some(false)),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"solo-cam") > 0 {
break;
}
settle().await;
}
assert_eq!(occurrences(&log, b"solo-cam"), 1, "the advertisement never went out");
let _standby = origin
.create_broadcast("solo-cam", crate::broadcast::Route::announced())
.unwrap();
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
tick().await;
}
assert_eq!(
occurrences(&log, b"solo-cam"),
1,
"re-offered inside the wait the peer asked for"
);
drop(cam);
}
#[tokio::test]
async fn a_modern_withdrawal_is_the_fin_alone() {
const VERSION: Version = Version::Draft17;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let cam = origin
.create_broadcast("solo-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let session =
crate::lite::test_transport::ScriptedSession::per_stream(vec![publish_namespace_ok(VERSION).await]);
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(None),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"solo-cam") > 0 {
break;
}
settle().await;
}
assert_eq!(occurrences(&log, b"solo-cam"), 1, "the advertisement never went out");
let advertised = log.writes.lock().unwrap().len();
drop(cam);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
settle().await;
}
assert_eq!(
log.writes.lock().unwrap().len(),
advertised,
"a draft-17+ withdrawal wrote a message; the FIN alone retracts"
);
}
#[tokio::test(start_paused = true)]
async fn a_silent_answer_still_lets_the_next_namespace_be_advertised() {
const VERSION: Version = Version::Draft14;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let _first = origin
.create_broadcast("first-cam", crate::broadcast::Route::announced())
.unwrap();
let _second = origin
.create_broadcast("second-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![Vec::new(), Vec::new()]);
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(Some(false)),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..200 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"first-cam") > 0 && occurrences(&log, b"second-cam") > 0 {
break;
}
tick().await;
}
assert!(
occurrences(&log, b"first-cam") > 0,
"the first advertisement never went out"
);
assert!(
occurrences(&log, b"second-cam") > 0,
"the silent answer wedged the loop: the second namespace never went out"
);
}
#[tokio::test(start_paused = true)]
async fn a_namespace_refused_a_stream_is_retried_on_its_own() {
const VERSION: Version = Version::Draft14;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let _cam = origin
.create_broadcast("lonely-cam", crate::broadcast::Route::announced())
.unwrap();
settle().await;
let gate = kio::Producer::new(false);
let ok = publish_namespace_ok(VERSION).await;
let session = crate::lite::test_transport::ScriptedSession::gated_open(vec![ok], gate.consume());
let log = session.log.clone();
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
None,
declared(Some(false)),
VERSION,
);
let mut run = std::pin::pin!(publisher.run_publish_namespaces());
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
tick().await;
}
assert_eq!(occurrences(&log, b"lonely-cam"), 0, "advertised without a stream");
set_gate(&gate, true);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"lonely-cam") > 0 {
break;
}
tick().await;
}
assert_eq!(occurrences(&log, b"lonely-cam"), 1, "never came back on its own");
}
async fn tick() {
tokio::time::advance(std::time::Duration::from_millis(200)).await;
}
fn set_gate(gate: &kio::Producer<bool>, open: bool) {
let Ok(mut gate) = gate.write() else {
panic!("gate closed")
};
*gate = open;
}
struct Harness {
publisher: Publisher<crate::lite::test_transport::ScriptedSession>,
session: crate::lite::test_transport::ScriptedSession,
log: crate::lite::test_transport::Log,
_origin: origin::Producer,
}
fn harness(version: Version) -> Harness {
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![Vec::new()]);
let log = session.log.clone();
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer::default());
let publisher = Publisher::new(
session.clone(),
origin.consume(),
Control::new(None, false),
None,
peer_setup,
version,
);
Harness {
publisher,
session,
log,
_origin: origin,
}
}
async fn subscribe_missing(version: Version) -> (Vec<u8>, Vec<u32>) {
let h = harness(version);
let stream = Stream::open(&h.session, version).await.unwrap();
h.publisher
.clone()
.run_subscribe_stream(
stream,
ietf::Subscribe {
request_id: RequestId(1),
track_namespace: crate::Path::new("nothing/here"),
track_name: "video".into(),
subscriber_priority: 128,
group_order: GroupOrder::Descending,
filter_type: FilterType::LargestObject,
},
)
.await
.unwrap();
let writes = h.log.writes.lock().unwrap().clone();
(writes, h.log.resets())
}
async fn fetch_unsupported(version: Version, fetch_type: FetchType<'_>) -> (Vec<u8>, Vec<u32>) {
let h = harness(version);
let stream = Stream::open(&h.session, version).await.unwrap();
h.publisher
.clone()
.run_fetch_stream(
stream,
ietf::Fetch {
request_id: RequestId(1),
subscriber_priority: 128,
group_order: GroupOrder::Descending,
fetch_type,
},
)
.await
.unwrap();
let writes = h.log.writes.lock().unwrap().clone();
(writes, h.log.resets())
}
#[tokio::test]
async fn missing_broadcast_is_refused_without_resetting_the_stream() {
for version in [Version::Draft17, Version::Draft18, Version::Draft19] {
let (writes, resets) = subscribe_missing(version).await;
assert!(!writes.is_empty(), "{version}: nothing was sent");
assert_eq!(
writes[0],
ietf::RequestError::ID as u8,
"{version}: not a REQUEST_ERROR"
);
assert!(resets.is_empty(), "{version}: stream reset, discarding the error");
}
}
#[tokio::test]
async fn unsupported_fetch_is_refused_without_resetting_the_stream() {
let unsupported = || {
[
(
"standalone",
FetchType::Standalone {
namespace: crate::Path::new("nothing/here"),
track: "video".into(),
start: Location { group: 0, object: 0 },
end: Location { group: 1, object: 0 },
},
),
(
"relative joining with an offset",
FetchType::RelativeJoining {
subscriber_request_id: RequestId(3),
group_offset: 1,
},
),
(
"absolute joining",
FetchType::AbsoluteJoining {
subscriber_request_id: RequestId(3),
group_id: 7,
},
),
]
};
for version in [Version::Draft17, Version::Draft18, Version::Draft19] {
for (label, fetch_type) in unsupported() {
let (writes, resets) = fetch_unsupported(version, fetch_type).await;
assert!(!writes.is_empty(), "{version} {label}: nothing was sent");
assert_eq!(
writes[0],
ietf::RequestError::ID as u8,
"{version} {label}: not a REQUEST_ERROR"
);
assert!(
resets.is_empty(),
"{version} {label}: stream reset, discarding the error"
);
}
}
}
}