use crate::runtime::Timers as _;
use std::{
collections::{HashMap, hash_map::Entry},
task::{Poll, ready},
time::Duration,
};
use crate::{
Error, Path, PathOwned, SessionError, Timescale, broadcast,
coding::{Reader, Stream},
frame, group,
ietf::{self, Control, FetchType, Filter, GroupOrder, RequestId},
origin, track,
util::{MaybeBoxedExt, MaybeSendBox, TaskSet, Tasks},
};
use super::{Message, Version, cluster, error::request, peer};
use kio::Lock;
const TRACK_ALIAS_TIMEOUT: Duration = Duration::from_secs(1);
const RETIRED_ALIAS_CAPACITY: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Alias {
Active(RequestId),
Retired,
}
#[derive(Default)]
struct AliasTable {
map: HashMap<u64, Alias>,
retired: std::collections::VecDeque<u64>,
}
type TrackAliases = kio::Producer<AliasTable>;
fn insert_track_alias(aliases: &TrackAliases, alias: u64, request_id: RequestId) -> Result<(), Error> {
let mut aliases = aliases.write().map_err(|_| Error::Dropped)?;
let table = &mut *aliases;
match table.map.entry(alias) {
Entry::Occupied(mut entry) if *entry.get() == Alias::Retired => {
entry.insert(Alias::Active(request_id));
table.retired.retain(|&retired| retired != alias);
Ok(())
}
Entry::Occupied(entry) if *entry.get() == Alias::Active(request_id) => Ok(()),
Entry::Occupied(_) => Err(Error::Duplicate),
Entry::Vacant(entry) => {
entry.insert(Alias::Active(request_id));
Ok(())
}
}
}
pub(super) fn is_protocol_violation(err: &Error) -> bool {
matches!(
err,
Error::Decode(_)
| Error::BoundsExceeded(_)
| Error::WrongSize
| Error::TooManyParameters
| Error::ProtocolViolation
| Error::UnexpectedMessage
| Error::UnexpectedStream
)
}
fn retire_track_alias(aliases: &TrackAliases, alias: u64, request_id: RequestId) {
let Ok(mut aliases) = aliases.write() else {
return;
};
let table = &mut *aliases;
if table.map.get(&alias) != Some(&Alias::Active(request_id)) {
return;
}
table.map.insert(alias, Alias::Retired);
table.retired.push_back(alias);
while table.retired.len() > RETIRED_ALIAS_CAPACITY {
let oldest = table.retired.pop_front().expect("non-empty above the capacity");
if table.map.get(&oldest) == Some(&Alias::Retired) {
table.map.remove(&oldest);
}
}
}
#[derive(Default)]
struct State {
subscribes: HashMap<RequestId, TrackState>,
fetches: HashMap<RequestId, RequestId>,
aliases: TrackAliases,
broadcasts: HashMap<PathOwned, BroadcastState>,
}
impl Drop for State {
fn drop(&mut self) {
for (_, track) in self.subscribes.drain() {
if let Fill::Ready { producer, .. } = &*track.fill.read() {
let _ = producer.clone().abort(Error::Cancel);
}
let _ = track.producer.abort(Error::Cancel);
}
}
}
enum Fill {
Requested,
Serving(Option<Timescale>),
Active,
Ready {
sequence: u64,
next: u64,
producer: group::Producer,
},
Done,
}
impl Fill {
fn outstanding(&self) -> bool {
!matches!(self, Fill::Done)
}
fn claim(&mut self, sequence: u64, start: Option<u64>) -> Result<Option<group::Producer>, Error> {
match *self {
Fill::Ready { sequence: s, next, .. } if s == sequence => {
if start.is_some_and(|start| start != next) {
tracing::warn!(sequence, next, start, "the fill does not meet the live tail");
self.release();
return Err(Error::Unsupported);
}
}
_ => return Ok(None),
}
match std::mem::replace(self, Fill::Done) {
Fill::Ready { producer, .. } => Ok(Some(producer)),
_ => Ok(None),
}
}
fn install(&mut self, head: Fill) {
match self {
Fill::Done => {
let mut head = head;
head.release();
}
_ => *self = head,
}
}
fn release(&mut self) {
if let Fill::Ready { producer, .. } = std::mem::replace(self, Fill::Done) {
let _ = producer.finish();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JoiningFetch {
Relative { group_offset: u64 },
Absolute { group_id: u64 },
}
struct Accepted {
alias: u64,
timescale: Option<Timescale>,
largest: Option<ietf::Location>,
}
struct TrackState {
producer: track::Producer,
alias: Option<u64>,
fill: kio::Producer<Fill>,
broadcast: PathOwned,
timescale: Option<Timescale>,
largest: Option<ietf::Location>,
fetch_id: Option<RequestId>,
joining: Option<JoiningFetch>,
}
impl TrackState {
fn new(
producer: track::Producer,
broadcast: PathOwned,
fill: kio::Producer<Fill>,
joining: Option<JoiningFetch>,
) -> Self {
Self {
producer,
alias: None,
broadcast,
timescale: None,
fill,
largest: None,
fetch_id: None,
joining,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Detach {
Graceful,
Abrupt,
}
struct BroadcastState {
route: crate::origin::Route,
dynamic: crate::origin::Dynamic,
count: usize,
sources: HashMap<PathOwned, crate::model::broadcast::SourceGuard>,
}
struct Advertised {
route: crate::origin::Route,
}
#[derive(Clone)]
pub(super) struct Subscriber<S: crate::transport::poll::Session> {
runtime: crate::time::Clock,
session: S,
origin: origin::Producer,
control: Control,
session_origin: crate::Hop,
self_origin: crate::Hop,
peer_setup: peer::PeerSetup,
cost: Option<u64>,
state: Lock<State>,
tasks: Tasks,
version: Version,
going_away: crate::goaway::GoingAway,
}
async fn resolve_track_alias(
runtime: &crate::time::Clock,
aliases: kio::Consumer<AliasTable>,
alias: u64,
) -> Result<RequestId, Error> {
let mut timeout = crate::runtime::Deadline::after(runtime, TRACK_ALIAS_TIMEOUT);
kio::wait(|waiter| {
let resolved = aliases.poll(waiter, |aliases| match aliases.map.get(&alias) {
Some(Alias::Active(request_id)) => Poll::Ready(Ok(*request_id)),
Some(Alias::Retired) => Poll::Ready(Err(Error::Cancel)),
None => Poll::Pending,
});
if let Poll::Ready(result) = resolved {
return Poll::Ready(result.unwrap_or(Err(Error::Dropped)));
}
if timeout.poll(waiter).is_ready() {
return Poll::Ready(Err(Error::NotFound));
}
Poll::Pending
})
.await
}
impl<S> Subscriber<S>
where
S: crate::transport::poll::Boxable,
{
#[allow(clippy::too_many_arguments)]
pub fn new(
runtime: crate::time::Clock,
session: S,
origin: origin::Producer,
control: Control,
peer_hop: Option<crate::Hop>,
peer_setup: peer::PeerSetup,
self_origin: crate::Hop,
cost: Option<u64>,
version: Version,
tasks: Tasks,
going_away: crate::goaway::GoingAway,
) -> Self {
Self {
runtime,
session,
origin,
control,
session_origin: peer_hop.unwrap_or(crate::Hop::UNKNOWN),
self_origin,
peer_setup,
cost,
state: Default::default(),
tasks,
version,
going_away,
}
}
#[cfg(test)]
pub(super) fn retire_alias(&self, alias: u64) {
const REQUEST_ID: RequestId = RequestId(0);
let aliases = self.state.lock().aliases.clone();
insert_track_alias(&aliases, alias, REQUEST_ID).expect("bind the alias");
retire_track_alias(&aliases, alias, REQUEST_ID);
}
pub(super) async fn peer(&self) -> cluster::Peer {
match cluster::supported(self.version) {
true => self.peer_setup.get().await.cluster,
false => cluster::Peer::default(),
}
}
fn via(&self, peer: &cluster::Peer) -> crate::Hop {
peer.identity().unwrap_or(self.session_origin)
}
fn session_route(&self, peer: &cluster::Peer) -> crate::origin::Route {
let mut hops = crate::Hops::new();
hops.push(crate::Hop::UNKNOWN)
.expect("an empty hop chain has room for one entry");
crate::origin::Route::default()
.with_hops(hops)
.with_via(self.via(peer))
.with_cost(crate::origin::Cost::UNKNOWN.charged(cluster::link_cost(self.cost, peer)))
}
fn route(&self, advert: Option<&cluster::Advert>, peer: &cluster::Peer) -> Option<Advertised> {
let Some(advert) = advert else {
return Some(Advertised {
route: self.session_route(peer),
});
};
if advert.loops(self.self_origin) {
return None;
}
Some(Advertised {
route: advert
.route(cluster::link_cost(self.cost, peer))
.with_via(self.via(peer)),
})
}
fn register_alias(&self, request_id: RequestId, alias: u64) -> Result<(), Error> {
let mut state = self.state.lock();
if !state.subscribes.contains_key(&request_id) {
return Err(Error::NotFound);
}
if let Err(err) = insert_track_alias(&state.aliases, alias, request_id) {
return Err(match self.alias_names_same_track(&state, alias, request_id) {
true => Error::Unsupported,
false => err,
});
}
state.subscribes.get_mut(&request_id).unwrap().alias = Some(alias);
Ok(())
}
fn alias_names_same_track(&self, state: &State, alias: u64, request_id: RequestId) -> bool {
let aliases = state.aliases.read();
let Some(Alias::Active(holder)) = aliases.map.get(&alias).copied() else {
return false;
};
let (Some(held), Some(new)) = (state.subscribes.get(&holder), state.subscribes.get(&request_id)) else {
return false;
};
held.broadcast == new.broadcast && held.producer.name() == new.producer.name()
}
fn remove_subscribe(&self, request_id: RequestId) -> Option<TrackState> {
let mut state = self.state.lock();
let track = state.subscribes.remove(&request_id)?;
if let Some(fetch_id) = track.fetch_id {
state.fetches.remove(&fetch_id);
}
if let Some(alias) = track.alias {
retire_track_alias(&state.aliases, alias, request_id);
}
if let Ok(mut fill) = track.fill.write() {
fill.release();
}
Some(track)
}
pub fn subscribe_prefixes(&self) -> Vec<PathOwned> {
crate::model::interest_prefixes(&self.origin.allowed())
}
pub async fn run_subscribe_namespace<T: crate::transport::poll::Session>(
&mut self,
mut stream: Stream<T, Version>,
prefix: PathOwned,
) -> Result<(), Error> {
if self.going_away.is_set() {
return Err(Error::GoingAway);
}
let hidden = self.peer_setup.get().await.hidden;
let request_id = self.control.next_request_id(&self.runtime).await?;
match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => {
let msg = ietf::SubscribeNamespaceLegacy {
request_id,
namespace: prefix.clone(),
subscribe_options: 0x01, hidden,
};
stream.writer.encode(&ietf::SubscribeNamespaceLegacy::ID).await?;
stream.writer.encode(&msg).await?;
}
_ => {
let msg = ietf::SubscribeNamespace {
request_id,
namespace: prefix.clone(),
hidden,
};
stream.writer.encode(&ietf::SubscribeNamespace::ID).await?;
stream.writer.encode(&msg).await?;
}
}
tracing::debug!(%prefix, "subscribe_namespace sent");
let type_id: u64 = stream.reader.decode().await?;
let size: u16 = stream.reader.decode().await?;
let mut data = stream.reader.read_exact(size as usize).await?;
match type_id {
ietf::SubscribeNamespaceOk::ID if self.version == Version::Draft14 => {
let _msg = ietf::SubscribeNamespaceOk::decode_msg(&mut data, self.version)?;
}
ietf::RequestOk::ID => {
let _msg = ietf::RequestOk::decode_msg(&mut data, self.version)?;
}
ietf::SubscribeNamespaceError::ID if self.version == Version::Draft14 => {
let msg = ietf::SubscribeNamespaceError::decode_msg(&mut data, self.version)?;
let err = request::from_code(msg.error_code, request::Kind::SubscribeNamespace, self.version);
tracing::warn!(%err, reason = %msg.reason_phrase, "subscribe_namespace error");
return Err(err);
}
ietf::RequestError::ID => {
let msg = ietf::RequestError::decode_msg(&mut data, self.version)?;
let err = request::from_code(msg.error_code, request::Kind::SubscribeNamespace, self.version);
tracing::warn!(%err, reason = %msg.reason_phrase, "subscribe_namespace error");
return Err(err);
}
_ => return Err(Error::UnexpectedMessage),
}
tracing::debug!(%prefix, "subscribe_namespace ok");
let peer = self.peer().await;
let mut live: std::collections::HashSet<PathOwned> = std::collections::HashSet::new();
let res = self.run_namespace_entries(&mut stream, &prefix, &peer, &mut live).await;
for path in live {
let _ = self.stop_announce(path, Detach::Abrupt);
}
res
}
async fn run_namespace_entries<T: crate::transport::poll::Session>(
&mut self,
stream: &mut Stream<T, Version>,
prefix: &PathOwned,
peer: &cluster::Peer,
live: &mut std::collections::HashSet<PathOwned>,
) -> Result<(), Error> {
loop {
let type_id: u64 = match stream.reader.decode_maybe().await? {
Some(id) => id,
None => break, };
let size: u16 = stream.reader.decode().await?;
let mut data = stream.reader.read_exact(size as usize).await?;
match type_id {
ietf::Namespace::ID => {
let msg = ietf::Namespace::decode_body(&mut data, self.version, peer.negotiated())?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
let path = prefix.join(&msg.suffix);
let Some(advert) = self.route(msg.cluster.as_ref(), peer) else {
tracing::debug!(%path, "dropping reflected namespace");
if live.remove(&path) {
let _ = self.stop_announce(path, Detach::Graceful);
}
continue;
};
tracing::debug!(%path, hops = advert.route.hops.len(), cost = ?advert.route.cost, "namespace");
if live.contains(&path) {
self.update_announce(path, advert)?;
} else {
match self.start_announce(path.clone(), advert) {
Ok(()) => {
live.insert(path);
}
Err(Error::Unauthorized) => {
tracing::debug!(%path, "namespace outside the subscribe scope; ignoring");
}
Err(err) => return Err(err),
}
}
}
ietf::NamespaceDone::ID => {
let msg = ietf::NamespaceDone::decode_msg(&mut data, self.version)?;
let path = prefix.join(&msg.suffix);
tracing::debug!(%path, "namespace_done");
if live.remove(&path) {
let _ = self.stop_announce(path, Detach::Graceful);
}
}
_ => {
tracing::warn!(type_id, "unexpected message on subscribe_namespace stream");
return Err(Error::UnexpectedMessage);
}
}
}
Ok(())
}
pub fn handle_stream(
&mut self,
id: u64,
mut data: bytes::Bytes,
stream: Stream<S, Version>,
peer: cluster::Peer,
declared: Option<bool>,
) -> Result<MaybeSendBox<'static, ()>, Error> {
let mut this = self.clone();
let task = match id {
ietf::Publish::ID => {
let msg = ietf::Publish::decode_msg(&mut data, this.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received publish");
async move {
if let Err(err) = this.run_publish_stream(stream, msg).await {
tracing::debug!(%err, "publish stream error");
}
}
.maybe_boxed()
}
ietf::PublishNamespace::ID => {
let msg = ietf::PublishNamespace::decode_body(&mut data, this.version, peer.negotiated())?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received publish_namespace");
async move {
if let Err(err) = this.run_publish_namespace_stream(stream, msg, peer, declared).await {
if is_protocol_violation(&err) {
this.session
.close(SessionError::from(&err).to_code(), err.to_string().as_ref());
}
tracing::debug!(%err, "publish_namespace stream error");
}
}
.maybe_boxed()
}
_ => {
tracing::warn!(id, "unexpected bidi stream type for subscriber");
return Err(Error::UnexpectedStream);
}
};
Ok(task)
}
pub(super) async fn solicit(&self) -> Option<bool> {
self.peer_setup.get().await.solicit
}
fn unsolicited_is_a_violation(&self, declared: Option<bool>) -> bool {
match self.version {
Version::Draft14 | Version::Draft15 => false,
_ => declared.is_some(),
}
}
async fn run_publish_namespace_stream(
&mut self,
mut stream: Stream<S, Version>,
msg: ietf::PublishNamespace<'_>,
peer: cluster::Peer,
declared: Option<bool>,
) -> Result<(), Error> {
let request_id = msg.request_id;
let path = msg.track_namespace.to_owned();
if self.unsolicited_is_a_violation(declared) {
tracing::warn!(%path, "unsolicited publish_namespace from a peer that implements MoQ Solicit");
return Err(Error::ProtocolViolation);
}
let Some(advert) = self.route(msg.cluster.as_ref(), &peer) else {
tracing::debug!(%path, "dropping reflected publish_namespace");
self.write_error(
&mut stream,
request_id,
&Error::Unroutable,
"route loops through this relay",
)
.await?;
let _ = stream.writer.close().await;
return Ok(());
};
match self.start_announce(path.clone(), advert) {
Ok(_) => {
if let Err(err) = self.write_ok(&mut stream, request_id).await {
let _ = self.stop_announce(path, Detach::Graceful);
return Err(err);
}
}
Err(err) => {
self.write_error(&mut stream, request_id, &err, &err.to_string())
.await?;
let _ = stream.writer.close().await;
return Ok(());
}
}
let mut attached = true;
let res = self
.run_publish_namespace_updates(&mut stream, &path, msg.cluster, peer, &mut attached)
.await;
if attached {
let detach = match res.is_ok() {
true => Detach::Graceful,
false => Detach::Abrupt,
};
self.stop_announce(path, detach)?;
}
res
}
fn terminal_publish_namespace(&self, type_id: u64) -> bool {
match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => type_id == ietf::PublishNamespaceDone::ID,
_ => false,
}
}
async fn run_publish_namespace_updates(
&mut self,
stream: &mut Stream<S, Version>,
path: &PathOwned,
mut held: Option<cluster::Advert>,
peer: cluster::Peer,
attached: &mut bool,
) -> Result<(), Error> {
loop {
let type_id: u64 = match stream.reader.decode_maybe().await? {
Some(id) => id,
None => return Ok(()),
};
let terminal = self.terminal_publish_namespace(type_id);
if type_id != ietf::PublishNamespaceUpdate::ID && !terminal {
tracing::warn!(type_id, "unexpected message on publish_namespace stream");
return Err(Error::UnexpectedMessage);
}
let size: u16 = stream.reader.decode().await?;
let mut data = stream.reader.read_exact(size as usize).await?;
if terminal {
ietf::PublishNamespaceDone::decode_msg(&mut data, self.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(%path, "publish_namespace_done");
return Ok(());
}
let msg = ietf::PublishNamespaceUpdate::decode_msg(&mut data, self.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
held = match &held {
Some(current) => {
if let Some(hops) = &msg.hops
&& hops.hops().iter().next() != current.hops.hops().iter().next()
{
tracing::warn!(%path, "publish_namespace update changes the publisher");
self.write_error(
stream,
msg.request_id,
&Error::Unsupported,
"a new publisher is a new advertisement",
)
.await?;
if stream.writer.finish().is_ok() {
let _ = stream.writer.closed().await;
}
return Ok(());
}
Some(msg.apply(current))
}
None if msg.hops.is_some() || msg.cost.is_some() => {
tracing::warn!(%path, "cluster parameters on a session that negotiated none");
return Err(Error::ProtocolViolation);
}
None => None,
};
let Some(advert) = self.route(held.as_ref(), &peer) else {
if std::mem::take(attached) {
tracing::debug!(%path, "publish_namespace now loops back; detaching");
let _ = self.stop_announce(path.clone(), Detach::Graceful);
}
self.write_ok(stream, msg.request_id).await?;
continue;
};
tracing::debug!(%path, hops = advert.route.hops.len(), cost = ?advert.route.cost, "publish_namespace update");
let applied = match *attached {
true => self.update_announce(path.clone(), advert),
false => self.start_announce(path.clone(), advert).map(|()| *attached = true),
};
match applied {
Ok(()) => self.write_ok(stream, msg.request_id).await?,
Err(err) => {
tracing::warn!(%path, %err, "publish_namespace update refused");
self.write_error(stream, msg.request_id, &err, &err.to_string()).await?;
if stream.writer.finish().is_ok() {
let _ = stream.writer.closed().await;
}
return Ok(());
}
}
}
}
async fn run_publish_stream(
&mut self,
mut stream: Stream<S, Version>,
msg: ietf::Publish<'_>,
) -> Result<(), Error> {
tracing::debug!(broadcast = %msg.track_namespace, track = %msg.track_name, "rejecting publish");
self.write_publish_error(
&mut stream,
msg.request_id,
&Error::Unsupported,
"PUBLISH is not supported",
)
.await?;
let _ = stream.writer.close().await;
Ok(())
}
async fn write_ok(&self, stream: &mut Stream<S, Version>, request_id: RequestId) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
stream.writer.encode(&ietf::PublishNamespaceOk::ID).await?;
stream.writer.encode(&ietf::PublishNamespaceOk { request_id }).await?;
}
Version::Draft15 | Version::Draft16 => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream
.writer
.encode(&ietf::RequestOk {
request_id: Some(request_id),
})
.await?;
}
_ => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream.writer.encode(&ietf::RequestOk { request_id: None }).await?;
}
}
Ok(())
}
async fn write_error(
&self,
stream: &mut Stream<S, Version>,
request_id: RequestId,
err: &Error,
reason: &str,
) -> Result<(), Error> {
let error_code = request::to_code(err, request::Kind::PublishNamespace, self.version);
match self.version {
Version::Draft14 => {
stream.writer.encode(&ietf::PublishNamespaceError::ID).await?;
stream
.writer
.encode(&ietf::PublishNamespaceError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
stream.writer.encode(&ietf::RequestError::ID).await?;
stream
.writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
stream.writer.encode(&ietf::RequestError::ID).await?;
stream
.writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
async fn write_publish_error(
&self,
stream: &mut Stream<S, Version>,
request_id: RequestId,
err: &Error,
reason: &str,
) -> Result<(), Error> {
let error_code = request::to_code(err, request::Kind::Publish, self.version);
match self.version {
Version::Draft14 => {
stream.writer.encode(&ietf::PublishError::ID).await?;
stream
.writer
.encode(&ietf::PublishError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
stream.writer.encode(&ietf::RequestError::ID).await?;
stream
.writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
stream.writer.encode(&ietf::RequestError::ID).await?;
stream
.writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
fn start_announce(&mut self, path: PathOwned, advert: Advertised) -> Result<(), Error> {
let mut state = self.state.lock();
let existing = state.broadcasts.contains_key(&path);
self.attach(&mut state, path.clone(), advert)?;
if existing && let Some(entry) = state.broadcasts.get_mut(&path) {
entry.count += 1;
}
Ok(())
}
fn update_announce(&mut self, path: PathOwned, advert: Advertised) -> Result<(), Error> {
let mut state = self.state.lock();
if !state.broadcasts.contains_key(&path) {
return Err(Error::NotFound);
}
self.attach(&mut state, path, advert)?;
Ok(())
}
fn attach(&self, state: &mut State, path: PathOwned, advert: Advertised) -> Result<(), Error> {
let Advertised { mut route } = advert;
if self.going_away.is_set() {
route.cost = crate::origin::Cost::DRAIN;
}
match state.broadcasts.entry(path.clone()) {
Entry::Occupied(entry) => {
let entry = entry.into_mut();
entry.route = route.clone();
entry.dynamic.update(route)?;
Ok(())
}
Entry::Vacant(entry) => {
let dynamic = self.origin.dynamic(&path, route.clone())?;
entry.insert(BroadcastState {
route,
dynamic,
count: 1,
sources: HashMap::new(),
});
tracing::debug!(route = %self.origin.absolute(&path), "announce");
let this = self.clone();
self.tasks.push(async move {
this.run_route(path).await;
});
Ok(())
}
}
}
fn stop_announce(&mut self, path: PathOwned, detach: Detach) -> Result<(), Error> {
let mut state = self.state.lock();
match state.broadcasts.entry(path.clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().count -= 1;
if entry.get().count == 0 {
tracing::debug!(route = %self.origin.absolute(&path), ?detach, "unannounced");
let removed = entry.remove();
for (_, source) in removed.sources {
match detach {
Detach::Graceful => source.finish(),
Detach::Abrupt => {}
}
}
}
}
Entry::Vacant(_) => return Err(Error::NotFound),
};
Ok(())
}
async fn drain(&self, tasks: &mut TaskSet) {
let mut session = self.session.clone();
kio::wait(|waiter| {
let mut cx = std::task::Context::from_waker(waiter.waker());
if session.poll_closed(&mut cx).is_ready() {
return Poll::Ready(());
}
tasks.poll(waiter)
})
.await
}
async fn run_route(&self, path: PathOwned) {
let mut broadcasts = TaskSet::owned();
let mut closed_session = self.session.clone();
loop {
let next = broadcasts
.drive(|waiter| {
let mut cx = std::task::Context::from_waker(waiter.waker());
if closed_session.poll_closed(&mut cx).is_ready() {
return Poll::Ready(None);
}
if self.going_away.poll(waiter).is_ready() {
self.drain_route(&path);
}
let mut state = self.state.lock();
match state.broadcasts.get_mut(&path) {
Some(entry) => entry.dynamic.poll_requested_broadcast(waiter).map(Some),
None => Poll::Ready(None),
}
})
.await;
let request = match next {
Some(Ok(request)) => request,
Some(Err(_)) | None => {
self.drain(&mut broadcasts).await;
break;
}
};
let requested = match request.path().strip_prefix(self.origin.root()) {
Some(requested) => requested.to_owned(),
None => continue,
};
let source = self.origin.create_source(&requested);
let dynamic = source.dynamic();
request.accept(&source);
let guard = crate::model::broadcast::SourceGuard::new(source);
let retracted = {
let mut state = self.state.lock();
match state.broadcasts.get_mut(&path) {
Some(entry) => {
entry.sources.insert(requested.clone(), guard);
None
}
None => Some(guard),
}
};
if let Some(guard) = retracted {
guard.finish();
}
let this = self.clone();
broadcasts.push(async move {
if let Err(err) = this.run_broadcast(requested.borrow(), dynamic).await {
tracing::debug!(%err, "error running broadcast");
}
});
}
}
fn drain_route(&self, path: &PathOwned) {
let mut state = self.state.lock();
let Some(entry) = state.broadcasts.get_mut(path) else {
return;
};
if entry.route.cost == crate::origin::Cost::DRAIN {
return;
}
entry.route.cost = crate::origin::Cost::DRAIN;
let _ = entry.dynamic.update(entry.route.clone());
}
async fn run_broadcast(&self, path: Path<'_>, mut broadcast: broadcast::Dynamic) -> Result<(), Error> {
let mut subscribes = TaskSet::owned();
let mut closed_session = self.session.clone();
loop {
let next = subscribes
.drive(|waiter| {
let mut cx = std::task::Context::from_waker(waiter.waker());
if closed_session.poll_closed(&mut cx).is_ready() {
return Poll::Ready(None);
}
broadcast.poll_requested_track(waiter).map(Some)
})
.await;
let request = match next {
Some(Ok(request)) => request,
Some(Err(err)) => {
tracing::debug!(%err, "broadcast closed");
self.drain(&mut subscribes).await;
break;
}
None => break,
};
let mut this = self.clone();
let path = path.to_owned();
let broadcast = broadcast.clone();
subscribes.push(async move {
this.run_subscribe(path, broadcast, request).await;
});
}
Ok(())
}
async fn run_subscribe(
&mut self,
broadcast_path: Path<'_>,
_broadcast: broadcast::Dynamic,
request: track::Request,
) {
let info = track::Info::default()
.with_timescale(crate::Timescale::MICRO)
.with_max_age(self.origin.default_max_age());
let mut track = request.accept(info);
if self.going_away.is_set() {
let _ = track.abort(Error::GoingAway);
return;
}
let subscription = track.subscription();
let join = match subscribe_join(
subscription.as_ref().and_then(|s| s.start),
subscription.as_ref().and_then(|s| s.end),
self.version,
) {
Ok(join) => join,
Err(err) => {
let _ = track.abort(err);
return;
}
};
let request_id = match self.control.next_request_id(&self.runtime).await {
Ok(id) => id,
Err(err) => {
let _ = track.abort(err);
return;
}
};
let mut stream = match Stream::open(&mut self.session.clone(), self.version).await {
Ok(s) => s,
Err(err) => {
tracing::debug!(%err, "failed to open subscribe stream");
let _ = track.abort(err);
return;
}
};
let joining = join.fetch;
let fill = kio::Producer::new(match join.fill.is_some() || join.fetch.is_some() {
true => Fill::Requested,
false => Fill::Done,
});
{
let mut state = self.state.lock();
state.subscribes.insert(
request_id,
TrackState::new(track.clone(), broadcast_path.to_owned(), fill, joining),
);
}
if let Err(err) = self
.write_subscribe(&mut stream, request_id, &broadcast_path, &track, join)
.await
{
tracing::debug!(%err, "failed to write subscribe");
self.remove_subscribe(request_id);
let _ = track.abort(err);
return;
}
tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name(), "subscribe started");
enum Setup {
Response(Result<Option<Accepted>, Error>),
Unused,
}
let track_name = track.name().to_owned();
let setup = {
let mut response = std::pin::pin!(self.read_subscribe_response(&mut stream));
loop {
let setup = kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(response.as_mut()) {
return Poll::Ready(Setup::Response(res));
}
if track.poll_unused(waiter).is_ready() {
return Poll::Ready(Setup::Unused);
}
Poll::Pending
})
.await;
match setup {
Setup::Response(res) => break Some((res, track)),
Setup::Unused => match track.abort_unused(Error::Cancel) {
Ok(()) => break None,
Err(used) => track = used,
},
}
}
};
let Some((response, mut track)) = setup else {
tracing::info!(
broadcast = %self.origin.absolute(&broadcast_path),
track = %track_name,
"subscribe abandoned before it was accepted"
);
self.remove_subscribe(request_id);
self.cancel_subscribe(stream, request_id).await;
return;
};
let mut fetching: Option<MaybeSendBox<'static, ()>> = None;
match response {
Ok(Some(Accepted {
alias,
timescale,
largest,
})) => {
{
let mut state = self.state.lock();
if let Some(track) = state.subscribes.get_mut(&request_id) {
if let Some(timescale) = timescale {
track.timescale = Some(timescale);
}
track.largest = largest;
if let Ok(mut fill) = track.fill.write()
&& matches!(*fill, Fill::Requested)
{
*fill = match largest {
Some(_) => Fill::Serving(timescale),
None => Fill::Done,
};
}
}
}
if let Err(err) = self.register_alias(request_id, alias) {
if matches!(err, Error::Duplicate) {
tracing::warn!(track_alias = %alias, "publisher reused a live track alias for another track");
self.session
.close(SessionError::from(&err).to_code(), err.to_string().as_ref());
} else {
tracing::warn!(track_alias = %alias, %err, "could not bind track alias");
self.cancel_subscribe(stream, request_id).await;
}
self.remove_subscribe(request_id);
let _ = track.abort(err);
return;
}
if let Some(joining) = joining
&& largest.is_some()
{
fetching = self.start_joining_fetch(request_id, &track, joining).await;
}
}
Ok(None) => {}
Err(err) => {
tracing::debug!(%err, "subscribe response error");
self.remove_subscribe(request_id);
let _ = track.abort(err);
return;
}
};
enum End {
Unused,
StreamClosed(Result<(), Error>),
}
let mut fetch_done = fetching.is_none();
let cancelled = loop {
let end = kio::wait(|waiter| {
if !fetch_done
&& let Some(fut) = fetching.as_mut()
&& waiter.poll_future(fut.as_mut()).is_ready()
{
fetch_done = true;
}
if track.poll_unused(waiter).is_ready() {
return Poll::Ready(End::Unused);
}
let mut cx = std::task::Context::from_waker(waiter.waker());
stream.reader.poll_closed(&mut cx).map(End::StreamClosed)
})
.await;
match end {
End::Unused => match track.abort_unused(Error::Cancel) {
Ok(()) => {
tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track_name, "subscribe cancelled");
break true;
}
Err(used) => track = used,
},
End::StreamClosed(res) => {
match res {
Ok(()) => {
tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track_name, "subscribe complete");
let _ = track.finish();
}
Err(err) => {
tracing::debug!(%err, "subscribe stream closed with error");
let _ = track.abort(err);
}
}
break false;
}
}
};
self.remove_subscribe(request_id);
match cancelled {
true => self.cancel_subscribe(stream, request_id).await,
false => {
stream.writer.finish().ok();
}
}
}
async fn cancel_subscribe(&self, stream: Stream<S, Version>, request_id: RequestId) {
let Stream { mut writer, mut reader } = stream;
if self.unsubscribes()
&& let Err(err) = self.write_unsubscribe(&mut writer, request_id).await
{
tracing::debug!(%err, "failed to write unsubscribe");
}
reader.abort(&Error::Cancel);
if let Err(err) = writer.close().await {
tracing::debug!(%err, "failed to close the subscribe stream");
}
}
fn unsubscribes(&self) -> bool {
matches!(self.version, Version::Draft14 | Version::Draft15 | Version::Draft16)
}
async fn write_unsubscribe(
&self,
writer: &mut crate::coding::Writer<S::SendStream, Version>,
request_id: RequestId,
) -> Result<(), Error> {
writer.encode(&ietf::Unsubscribe::ID).await?;
writer.encode(&ietf::Unsubscribe { request_id }).await?;
Ok(())
}
async fn write_subscribe(
&self,
stream: &mut Stream<S, Version>,
request_id: RequestId,
broadcast: &Path<'_>,
track: &track::Producer,
join: Join,
) -> Result<(), Error> {
stream.writer.encode(&ietf::Subscribe::ID).await?;
stream
.writer
.encode(&ietf::Subscribe {
request_id,
track_namespace: broadcast.to_owned(),
track_name: track.name().into(),
subscriber_priority: super::priority::to_wire(track.subscription().map(|s| s.priority).unwrap_or(0)),
group_order: GroupOrder::Descending,
filter: join.filter,
fill: join.fill,
properties_wanted: true,
})
.await?;
Ok(())
}
async fn start_joining_fetch(
&self,
subscribe_id: RequestId,
track: &track::Producer,
joining: JoiningFetch,
) -> Option<MaybeSendBox<'static, ()>> {
let fill = {
let state = self.state.lock();
state.subscribes.get(&subscribe_id)?.fill.clone()
};
let fetch_id = match self.control.next_request_id(&self.runtime).await {
Ok(id) => id,
Err(_) => {
settle_join_live(&fill);
return None;
}
};
{
let mut state = self.state.lock();
let track = state.subscribes.get_mut(&subscribe_id)?;
track.fetch_id = Some(fetch_id);
state.fetches.insert(fetch_id, subscribe_id);
}
let mut stream = match Stream::open(&mut self.session.clone(), self.version).await {
Ok(s) => s,
Err(err) => {
tracing::debug!(%err, "failed to open joining FETCH stream");
settle_join_live(&fill);
return None;
}
};
let fetch_type = match joining {
JoiningFetch::Relative { group_offset } => FetchType::RelativeJoining {
subscriber_request_id: subscribe_id,
group_offset,
},
JoiningFetch::Absolute { group_id } => FetchType::AbsoluteJoining {
subscriber_request_id: subscribe_id,
group_id,
},
};
if let Err(err) = async {
stream.writer.encode(&ietf::Fetch::ID).await?;
stream
.writer
.encode(&ietf::Fetch {
request_id: fetch_id,
subscriber_priority: super::priority::to_wire(
track.subscription().map(|s| s.priority).unwrap_or(0),
),
group_order: GroupOrder::Ascending,
fetch_type,
})
.await?;
Ok::<(), Error>(())
}
.await
{
tracing::debug!(%err, "failed to write joining FETCH");
settle_join_live(&fill);
return None;
}
let mut this = self.clone();
Some(
async move {
this.finish_joining_fetch(stream, fill).await;
}
.maybe_boxed(),
)
}
async fn finish_joining_fetch(&mut self, mut stream: Stream<S, Version>, fill: kio::Producer<Fill>) {
if !matches!(self.read_fetch_response(&mut stream).await, Ok(true)) {
settle_join_live(&fill);
let _ = stream.writer.close().await;
return;
}
let _stream = stream;
std::future::pending::<()>().await;
}
async fn read_fetch_response(&self, stream: &mut Stream<S, Version>) -> Result<bool, Error> {
let type_id: u64 = stream.reader.decode().await?;
let size: u16 = stream.reader.decode().await?;
let mut data = stream.reader.read_exact(size as usize).await?;
match type_id {
ietf::FetchOk::ID => {
let _msg = ietf::FetchOk::decode_msg(&mut data, self.version)?;
Ok(true)
}
ietf::FetchError::ID if self.version == Version::Draft14 => {
let _msg = ietf::FetchError::decode_msg(&mut data, self.version)?;
Ok(false)
}
ietf::RequestError::ID => {
let _msg = ietf::RequestError::decode_msg(&mut data, self.version)?;
Ok(false)
}
_ => Err(Error::UnexpectedMessage),
}
}
async fn read_subscribe_response(&self, stream: &mut Stream<S, Version>) -> Result<Option<Accepted>, Error> {
let type_id: u64 = stream.reader.decode().await?;
let size: u16 = stream.reader.decode().await?;
let mut data = stream.reader.read_exact(size as usize).await?;
match type_id {
ietf::SubscribeOk::ID => {
let msg = ietf::SubscribeOk::decode_msg(&mut data, self.version)?;
tracing::debug!(message = ?msg, "received subscribe ok");
Ok(Some(Accepted {
alias: msg.track_alias,
timescale: msg.properties.timescale,
largest: msg.largest,
}))
}
ietf::SubscribeError::ID if self.version == Version::Draft14 => {
let msg = ietf::SubscribeError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "subscribe error");
Err(request::from_code(
msg.error_code,
request::Kind::Subscribe,
self.version,
))
}
ietf::RequestError::ID => {
let msg = ietf::RequestError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "request error");
Err(request::from_code(
msg.error_code,
request::Kind::Subscribe,
self.version,
))
}
_ => Err(Error::UnexpectedMessage),
}
}
pub async fn recv_group(&mut self, stream: &mut Reader<S::RecvStream, Version>) -> Result<(), Error> {
let group: ietf::GroupHeader = stream.decode().await?;
if group.sub_group_id != 0 {
tracing::warn!(sub_group_id = %group.sub_group_id, "subgroup ID is not supported, dropping stream");
return Err(Error::Unsupported);
}
let aliases = self.state.lock().aliases.consume();
let request_id = match resolve_track_alias(&self.runtime, aliases, group.track_alias).await {
Ok(request_id) => request_id,
Err(err @ Error::Cancel) => {
tracing::debug!(track_alias = %group.track_alias, "dropping group for a cancelled subscription");
return Err(err);
}
Err(err) => {
tracing::warn!(
track_alias = %group.track_alias,
timeout = ?TRACK_ALIAS_TIMEOUT,
"unknown track alias: no SUBSCRIBE_OK bound it"
);
return Err(err);
}
};
let (track, timescale, fill) = {
let state = self.state.lock();
let track = state.subscribes.get(&request_id).ok_or(Error::NotFound)?;
(track.producer.clone(), track.timescale, track.fill.clone())
};
if !group.flags.first_object && !fill.read().outstanding() {
tracing::debug!(
track_alias = %group.track_alias,
group = %group.group_id,
"dropping a group with no head"
);
return Err(Error::Unsupported);
}
let (producer, start) = {
let mut opening = track.clone();
let mut open = std::pin::pin!(self.open_group(stream, &mut opening, &fill, group.group_id));
kio::wait(|waiter| {
if let Poll::Ready(err) = track.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
waiter.poll_future(open.as_mut())
})
.await?
};
let producer = crate::recv::Group::new(producer);
let res = {
let mut ingest = GroupIngest::new(self.runtime.clone(), &group, timescale, self.version, start);
let mut writing = producer.clone();
kio::wait(|waiter| {
if let Poll::Ready(err) = track.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
if let Poll::Ready(err) = producer.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
ingest.poll(stream, &mut writing, waiter)
})
.await
};
match res {
Err(err @ (Error::Cancel | Error::Stream(crate::StreamError::Cancel))) => {
let _ = producer.abort(err);
}
Err(err) => {
tracing::debug!(%err, group = %producer.sequence, "group error");
let _ = producer.abort(err);
}
_ => {
let _ = producer.finish();
}
}
Ok(())
}
}
impl<S> Subscriber<S>
where
S: crate::transport::poll::Boxable,
{
async fn open_group(
&self,
stream: &mut Reader<S::RecvStream, Version>,
track: &mut track::Producer,
fill: &kio::Producer<Fill>,
sequence: u64,
) -> Result<(group::Producer, u64), Error> {
let create = |track: &mut track::Producer| track.create_group(group::Info { sequence });
if !fill.read().outstanding() {
return Ok((create(track)?, 0));
}
match stream.decode_peek_maybe::<u64>().await? {
Some(0) => {
let headed = matches!(*fill.read(), Fill::Ready { sequence: s, .. } if s == sequence);
if headed {
tracing::warn!(sequence, "a whole group arrived for one the fill already headed");
if let Ok(mut state) = fill.write() {
state.release();
}
return Err(Error::Unsupported);
}
Ok((create(track)?, 0))
}
Some(start) => match self.claim_fill(fill, track, sequence, Some(start)).await? {
Some(producer) => Ok((producer, start)),
None => {
tracing::warn!(sequence, start, "no fill to stitch a mid-group stream onto");
Err(Error::Unsupported)
}
},
None => match self.claim_fill(fill, track, sequence, None).await? {
Some(producer) => Ok((producer, 0)),
None => Ok((create(track)?, 0)),
},
}
}
async fn claim_fill(
&self,
fill: &kio::Producer<Fill>,
track: &track::Producer,
sequence: u64,
start: Option<u64>,
) -> Result<Option<group::Producer>, Error> {
kio::wait(|waiter| {
if let Poll::Ready(err) = track.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
let settled = fill.poll(waiter, |fill| match **fill {
Fill::Requested | Fill::Serving(_) | Fill::Active => Poll::Pending,
Fill::Ready { .. } | Fill::Done => Poll::Ready(()),
});
match settled {
Poll::Ready(Ok(mut fill)) => Poll::Ready(fill.claim(sequence, start)),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::Dropped)),
Poll::Pending => Poll::Pending,
}
})
.await
}
}
struct GroupIngest {
runtime: crate::time::Clock,
has_extensions: bool,
has_end: bool,
timescale: Option<Timescale>,
version: Version,
prior_object: Option<u64>,
start: u64,
phase: IngestPhase,
}
enum IngestPhase {
Delta,
ExtSize,
ExtBytes { size: usize },
Size { timestamp: Option<crate::Timestamp> },
Status { timestamp: Option<crate::Timestamp> },
Payload { frame: frame::ProducerOwned },
Finished,
}
impl GroupIngest {
fn new(
runtime: crate::time::Clock,
group: &ietf::GroupHeader,
timescale: Option<Timescale>,
version: Version,
start: u64,
) -> Self {
Self {
runtime,
has_extensions: group.flags.has_extensions,
has_end: group.flags.has_end,
timescale,
version,
prior_object: None,
start,
phase: IngestPhase::Delta,
}
}
}
impl<S> Subscriber<S>
where
S: crate::transport::poll::Boxable,
{
pub async fn recv_fill(&mut self, stream: &mut Reader<S::RecvStream, Version>) -> Result<(), Error> {
let _: u64 = stream.decode().await?;
let header: ietf::FetchHeader = stream.decode().await?;
let (track, fill, joining, largest) = {
let state = self.state.lock();
let subscribe_id = state
.fetches
.get(&header.request_id)
.copied()
.unwrap_or(header.request_id);
let track = state.subscribes.get(&subscribe_id).ok_or(Error::NotFound)?;
(track.producer.clone(), track.fill.clone(), track.joining, track.largest)
};
let timescale = kio::wait(|waiter| {
if let Poll::Ready(err) = track.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
let accepted = fill.poll(waiter, |fill| match **fill {
Fill::Requested => Poll::Pending,
_ => Poll::Ready(()),
});
match accepted {
Poll::Ready(Ok(mut fill)) => Poll::Ready(match *fill {
Fill::Serving(timescale) => {
*fill = Fill::Active;
Ok(timescale)
}
_ => Err(Error::Unsupported),
}),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::Dropped)),
Poll::Pending => Poll::Pending,
}
})
.await?;
let res = {
let mut serving = track.clone();
let mut serve = std::pin::pin!(self.run_fill(stream, &mut serving, timescale, joining, largest));
kio::wait(|waiter| {
if let Poll::Ready(err) = track.poll_closed(waiter) {
return Poll::Ready(Err(err));
}
waiter.poll_future(serve.as_mut())
})
.await
};
let head = match res {
Ok(head) => head,
Err(err) => {
if let Ok(mut state) = fill.write() {
*state = Fill::Done;
}
return Err(err);
}
};
match fill.write() {
Ok(mut state) => state.install(head),
Err(_) => {
let mut head = head;
head.release();
return Err(Error::Dropped);
}
}
Ok(())
}
async fn run_fill(
&mut self,
stream: &mut Reader<S::RecvStream, Version>,
track: &mut track::Producer,
timescale: Option<Timescale>,
joining: Option<JoiningFetch>,
largest: Option<ietf::Location>,
) -> Result<Fill, Error> {
let mut head: Option<(u64, u64, crate::recv::Group)> = None;
match self
.run_fill_objects(stream, track, timescale, joining, largest, &mut head)
.await
{
Ok(()) => Ok(match head {
Some((sequence, next, producer)) => {
if matches!(joining, Some(JoiningFetch::Absolute { .. }))
&& largest.is_some_and(|largest| sequence < largest.group)
{
producer.finish()?;
Fill::Done
} else {
Fill::Ready {
sequence,
next,
producer: producer.into_inner(),
}
}
}
None => Fill::Done,
}),
Err(err) => {
if let Some((_, _, producer)) = head {
let _ = producer.abort(err.clone());
}
Err(err)
}
}
}
async fn run_fill_objects(
&mut self,
stream: &mut Reader<S::RecvStream, Version>,
track: &mut track::Producer,
timescale: Option<Timescale>,
joining: Option<JoiningFetch>,
largest: Option<ietf::Location>,
head: &mut Option<(u64, u64, crate::recv::Group)>,
) -> Result<(), Error> {
let mut prior_group = None;
while let Some(object) = decode_fetch_object(stream, self.version).await? {
if !object.subgroup_ok {
tracing::warn!("subgroup ID is not supported, dropping fill");
return Err(Error::Unsupported);
}
let group = resolve_fetch_group(self.version, prior_group, object.group)?;
if let Some(sequence) = group {
prior_group = Some(sequence);
}
match head.as_ref().map(|(sequence, next, _)| (*sequence, *next)) {
None => {
let (Some(sequence), Some(0)) = (group, object.object) else {
tracing::warn!(
group = ?group,
object = ?object.object,
"a fill must start at a group's first object"
);
return Err(Error::Unsupported);
};
open_fill_group(track, head, sequence)?;
}
Some((sequence, _)) if group.is_some_and(|group| group != sequence) => {
let Some(group) = group else {
unreachable!("the filter above proved group is Some");
};
if object.object != Some(0) {
tracing::warn!(
group,
object = ?object.object,
"a fill must start at a group's first object"
);
return Err(Error::Unsupported);
}
advance_fill_group(track, head, group, joining, largest)?;
}
Some((sequence, next)) => {
let id = match (object.group.is_some(), object.object) {
(true, Some(id)) => id,
(false, None | Some(1)) => next,
_ => {
tracing::warn!(
sequence,
next,
object = ?object.object,
"fill object IDs must increment by 1"
);
return Err(Error::Unsupported);
}
};
if id != next {
tracing::warn!(sequence, next, object = id, "fill object IDs must increment by 1");
return Err(Error::Unsupported);
}
}
}
let timestamp = match (object.properties, timescale) {
(Some(properties), Some(timescale)) => {
let mut properties = bytes::Bytes::from(properties);
ietf::decode_object_time(&mut properties, timescale, self.version)?
}
_ => None,
};
let timestamp = timestamp.unwrap_or_else(|| crate::Timestamp::from(self.runtime.now()));
let size: u64 = stream.decode().await?;
if size == 0 && matches!(self.version, Version::Draft14 | Version::Draft15) {
let status: u64 = stream.decode().await?;
if status != 0 {
return Err(Error::Unsupported);
}
}
let (_, next, producer) = head.as_mut().expect("the head was created above");
let mut frame = producer.create_frame_owned(frame::Info { size, timestamp })?;
if let Err(err) = std::future::poll_fn(|cx| stream.poll_read_frame(cx, &mut frame)).await {
let _ = frame.abort(err.clone());
return Err(err);
}
frame.finish()?;
*next += 1;
}
Ok(())
}
}
struct FetchedObject {
group: Option<u64>,
object: Option<u64>,
subgroup_ok: bool,
properties: Option<Vec<u8>>,
}
async fn decode_fetch_object<R: crate::transport::poll::RecvStream>(
stream: &mut Reader<R, Version>,
version: Version,
) -> Result<Option<FetchedObject>, Error> {
if version == Version::Draft14 {
let Some(group) = stream.decode_maybe::<u64>().await? else {
return Ok(None);
};
let subgroup: u64 = stream.decode().await?;
let object: u64 = stream.decode().await?;
let _priority: u8 = stream.decode().await?;
let properties: Vec<u8> = stream.decode().await?;
return Ok(Some(FetchedObject {
group: Some(group),
object: Some(object),
subgroup_ok: subgroup == 0,
properties: Some(properties),
}));
}
Ok(match stream.decode_maybe::<ietf::FetchObject>().await? {
None => None,
Some(ietf::FetchObject::EndOfRange { .. }) => {
tracing::warn!("a fill with an End of Range cannot be stitched");
return Err(Error::Unsupported);
}
Some(ietf::FetchObject::Object {
subgroup,
group,
object,
properties,
..
}) => Some(FetchedObject {
group,
object,
subgroup_ok: matches!(
subgroup,
ietf::FetchSubgroup::Zero | ietf::FetchSubgroup::Prior | ietf::FetchSubgroup::Explicit(0)
),
properties,
}),
})
}
fn resolve_fetch_group(version: Version, prior: Option<u64>, wire: Option<u64>) -> Result<Option<u64>, Error> {
let Some(wire) = wire else {
return Ok(None);
};
let Some(prior) = prior else {
return Ok(Some(wire));
};
match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 | Version::Draft17 => Ok(Some(wire)),
_ => {
let step = wire.checked_add(1).ok_or(Error::Unsupported)?;
prior.checked_add(step).map(Some).ok_or(Error::Unsupported)
}
}
}
fn open_fill_group(
track: &mut track::Producer,
head: &mut Option<(u64, u64, crate::recv::Group)>,
sequence: u64,
) -> Result<(), Error> {
let producer = track.create_group(group::Info { sequence })?;
*head = Some((sequence, 0, crate::recv::Group::new(producer)));
Ok(())
}
fn advance_fill_group(
track: &mut track::Producer,
head: &mut Option<(u64, u64, crate::recv::Group)>,
sequence: u64,
joining: Option<JoiningFetch>,
largest: Option<ietf::Location>,
) -> Result<(), Error> {
if !matches!(joining, Some(JoiningFetch::Absolute { .. })) {
tracing::warn!("a fill spanning several groups cannot be stitched");
return Err(Error::Unsupported);
}
let Some((prev, _, producer)) = head.take() else {
return open_fill_group(track, head, sequence);
};
if largest.is_some_and(|largest| prev >= largest.group) {
tracing::warn!("a joining FETCH continued past the subscribe's Largest Location");
let _ = producer.abort(Error::Unsupported);
return Err(Error::Unsupported);
}
producer.finish()?;
open_fill_group(track, head, sequence)
}
fn settle_join_live(fill: &kio::Producer<Fill>) {
let Ok(mut state) = fill.write() else {
return;
};
if matches!(*state, Fill::Requested | Fill::Serving(_)) {
*state = Fill::Done;
}
}
impl GroupIngest {
fn poll<R: crate::transport::poll::RecvStream>(
&mut self,
reader: &mut Reader<R, Version>,
group: &mut group::Producer,
waiter: &kio::Waiter,
) -> Poll<Result<(), Error>> {
let mut cx = std::task::Context::from_waker(waiter.waker());
loop {
match &mut self.phase {
IngestPhase::Delta => {
let Some(id_delta) = ready!(reader.poll_decode_maybe::<u64>(&mut cx))? else {
return Poll::Ready(Ok(()));
};
self.prior_object = Some(next_object_id(self.prior_object, id_delta, self.start)?);
self.phase = match self.has_extensions {
true => IngestPhase::ExtSize,
false => IngestPhase::Size { timestamp: None },
};
}
IngestPhase::ExtSize => {
let size: usize = ready!(reader.poll_decode(&mut cx))?;
self.phase = IngestPhase::ExtBytes { size };
}
IngestPhase::ExtBytes { size } => {
let mut ext = ready!(reader.poll_read_exact(&mut cx, *size))?;
let timestamp = match self.timescale {
Some(timescale) => ietf::decode_object_time(&mut ext, timescale, self.version)?,
None => None,
};
self.phase = IngestPhase::Size { timestamp };
}
IngestPhase::Size { timestamp } => {
let size: u64 = ready!(reader.poll_decode(&mut cx))?;
if size == 0 {
self.phase = IngestPhase::Status { timestamp: *timestamp };
continue;
}
let timestamp = timestamp.unwrap_or_else(|| crate::Timestamp::from(self.runtime.now()));
let frame = group.create_frame_owned(frame::Info { size, timestamp })?;
self.phase = IngestPhase::Payload { frame };
}
IngestPhase::Status { timestamp } => {
let status: u64 = ready!(reader.poll_decode(&mut cx))?;
if status == 0 {
let timestamp = timestamp.unwrap_or_else(|| crate::Timestamp::from(self.runtime.now()));
let frame = group.create_frame_owned(frame::Info { size: 0, timestamp })?;
frame.finish()?;
self.phase = IngestPhase::Delta;
} else if status == 3 && !self.has_end {
self.phase = IngestPhase::Finished;
} else {
return Poll::Ready(Err(Error::Unsupported));
}
}
IngestPhase::Payload { frame } => {
let failed = ready!(reader.poll_read_frame(&mut cx, frame)).err();
let IngestPhase::Payload { frame } = std::mem::replace(&mut self.phase, IngestPhase::Delta) else {
unreachable!()
};
match failed {
None => frame.finish()?,
Some(err) => {
let _ = frame.abort(err.clone());
return Poll::Ready(Err(err));
}
}
}
IngestPhase::Finished => return Poll::Ready(Ok(())),
}
}
}
}
#[cfg(test)]
mod tests {
use crate::model::ProduceTest;
use futures::poll;
use super::*;
#[tokio::test(start_paused = true)]
async fn track_alias_waits_for_control_message() {
let runtime = crate::time::Clock::tokio();
let aliases = TrackAliases::default();
let pending = resolve_track_alias(&runtime, aliases.consume(), 7);
tokio::pin!(pending);
assert!(poll!(&mut pending).is_pending());
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
assert_eq!(pending.await.unwrap(), RequestId(11));
}
#[tokio::test(start_paused = true)]
async fn unknown_track_alias_times_out() {
let aliases = TrackAliases::default();
assert!(matches!(
resolve_track_alias(&crate::time::Clock::tokio(), aliases.consume(), 7).await,
Err(Error::NotFound)
));
}
async fn settle() {
tokio::time::sleep(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 unsolicited_is_a_violation(solicit: Option<bool>, version: Version) -> bool {
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer {
solicit,
..Default::default()
});
let (tasks, _task_set) = crate::util::TaskSet::new();
Subscriber::new(
crate::time::Clock::tokio(),
session,
origin,
Control::new(None, false),
None,
peer_setup,
crate::Hop::new(1).unwrap(),
None,
version,
tasks,
Default::default(),
)
.unsolicited_is_a_violation(solicit)
}
#[tokio::test]
async fn an_announce_from_a_peer_that_implements_solicit_is_fatal() {
assert!(
unsolicited_is_a_violation(Some(true), Version::Draft17),
"a peer that requires solicitation itself"
);
assert!(
unsolicited_is_a_violation(Some(false), Version::Draft17),
"an explicit 0 declares support, so ours binds it too"
);
}
#[tokio::test]
async fn an_announce_from_a_peer_that_declared_nothing_is_fine() {
assert!(!unsolicited_is_a_violation(None, Version::Draft17));
}
#[tokio::test]
async fn a_legacy_announce_is_never_a_violation() {
for version in [Version::Draft14, Version::Draft15] {
assert!(
!unsolicited_is_a_violation(Some(true), version),
"{version:?} answers a subscription this way"
);
}
}
#[tokio::test]
async fn a_rooted_subscriber_asks_for_its_scope_not_its_root() {
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let scope = crate::Patterns::from(crate::Pattern::subtree("cam").unwrap());
let scoped = origin.scope("rootns", &scope).expect("scope the origin");
let gate = kio::Producer::new(true);
let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume());
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer::default());
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
scoped,
Control::new(None, false),
None,
peer_setup,
crate::Hop::new(1).unwrap(),
None,
Version::Draft16,
tasks,
Default::default(),
);
assert_eq!(
subscriber.subscribe_prefixes(),
vec![crate::Path::new("cam").to_owned()],
"one SUBSCRIBE_NAMESPACE per permitted prefix, relative to the root",
);
let stream = Stream::open(&mut session.clone(), Version::Draft16).await.unwrap();
let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::new("cam").to_owned()));
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(occurrences(&log, b"cam"), 1, "asked the peer for our scope");
assert_eq!(occurrences(&log, b"rootns"), 0, "asked the peer for our local root");
}
async fn namespace_response(version: Version, suffix: &str) -> 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::RequestOk::ID).await.unwrap();
writer.encode(&ietf::RequestOk { request_id: None }).await.unwrap();
writer.encode(&ietf::Namespace::ID).await.unwrap();
writer
.encode(&ietf::Namespace {
suffix: crate::Path::new(suffix),
cluster: None,
})
.await
.unwrap();
let writes = log.writes.lock().unwrap();
writes.clone()
}
#[tokio::test]
async fn a_rooted_subscriber_mounts_a_reply_under_its_root_once() {
const VERSION: Version = Version::Draft18;
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let scope = crate::Patterns::from(crate::Pattern::subtree("cam").unwrap());
let scoped = origin.scope("rootns", &scope).expect("scope the origin");
let session = crate::lite::test_transport::ScriptedSession::new(namespace_response(VERSION, "x.hang").await);
let (tasks, _task_set) = crate::util::TaskSet::new();
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer::default());
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
scoped,
Control::new(None, false),
None,
peer_setup,
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let prefix = subscriber.subscribe_prefixes().pop().expect("one prefix");
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, prefix));
for _ in 0..100 {
let _ = futures::poll!(run.as_mut());
if routed_now(&consumer, "rootns/cam/x.hang").is_some() {
break;
}
settle().await;
}
assert!(
routed_now(&consumer, "rootns/cam/x.hang").is_some(),
"the reply mounts under the root once",
);
assert!(
routed_now(&consumer, "rootns/rootns/cam/x.hang").is_none(),
"the root was applied twice",
);
}
#[test]
fn retiring_old_track_does_not_retire_reused_alias() {
let aliases = TrackAliases::default();
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
retire_track_alias(&aliases, 7, RequestId(13));
assert_eq!(aliases.read().map.get(&7), Some(&Alias::Active(RequestId(11))));
}
#[tokio::test(start_paused = true)]
async fn retired_alias_drops_late_groups_immediately() {
let aliases = TrackAliases::default();
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
retire_track_alias(&aliases, 7, RequestId(11));
let runtime = crate::time::Clock::tokio();
let resolve = resolve_track_alias(&runtime, aliases.consume(), 7);
tokio::pin!(resolve);
assert!(
matches!(poll!(&mut resolve), std::task::Poll::Ready(Err(Error::Cancel))),
"a retired alias must resolve without waiting on the timeout",
);
}
#[tokio::test(start_paused = true)]
async fn a_retired_alias_maps_to_the_cancelled_code() {
let aliases = TrackAliases::default();
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
retire_track_alias(&aliases, 7, RequestId(11));
let err = resolve_track_alias(&crate::time::Clock::tokio(), aliases.consume(), 7)
.await
.expect_err("a retired alias resolves to a cancellation");
assert_eq!(
crate::ietf::error::to_stream_code(&crate::StreamError::from(&err), Version::Draft20),
crate::ietf::error::CANCELLED,
"the code the dispatch loop maps this error onto",
);
}
#[test]
fn subscribe_ok_reclaims_a_retired_alias() {
let aliases = TrackAliases::default();
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
retire_track_alias(&aliases, 7, RequestId(11));
insert_track_alias(&aliases, 7, RequestId(13)).unwrap();
assert_eq!(aliases.read().map.get(&7), Some(&Alias::Active(RequestId(13))));
assert!(
aliases.read().retired.is_empty(),
"reclaiming an alias must drop its tombstone",
);
}
#[test]
fn active_alias_rejects_a_second_track() {
let aliases = TrackAliases::default();
insert_track_alias(&aliases, 7, RequestId(11)).unwrap();
assert!(matches!(
insert_track_alias(&aliases, 7, RequestId(13)),
Err(Error::Duplicate)
));
}
fn subscriber_with_tracks(
tracks: &[(RequestId, &str, &str)],
) -> Subscriber<crate::lite::test_transport::SinkSession> {
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let subscriber = Subscriber::new(
crate::time::Clock::tokio(),
crate::lite::test_transport::SinkSession::new(Default::default()),
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
Version::Draft19,
tasks,
Default::default(),
);
{
let mut state = subscriber.state.lock();
for (request_id, broadcast, name) in tracks {
state.subscribes.insert(
*request_id,
TrackState::new(
track::Producer::new(std::sync::Arc::new(crate::broadcast::Info::default()), *name, None),
Path::new(broadcast).to_owned(),
kio::Producer::new(Fill::Done),
None,
),
);
}
}
subscriber
}
#[test]
fn a_shared_alias_for_one_track_costs_only_that_subscription() {
let subscriber = subscriber_with_tracks(&[(RequestId(11), "cam", "video"), (RequestId(13), "cam", "video")]);
subscriber.register_alias(RequestId(11), 7).unwrap();
assert!(
matches!(subscriber.register_alias(RequestId(13), 7), Err(Error::Unsupported)),
"a shared alias must not be reported as the fatal collision",
);
}
#[test]
fn an_alias_reused_for_another_track_is_fatal() {
let subscriber = subscriber_with_tracks(&[(RequestId(11), "cam", "video"), (RequestId(13), "cam", "audio")]);
subscriber.register_alias(RequestId(11), 7).unwrap();
assert!(matches!(
subscriber.register_alias(RequestId(13), 7),
Err(Error::Duplicate)
));
}
#[test]
fn an_alias_reused_across_broadcasts_is_fatal() {
let subscriber = subscriber_with_tracks(&[(RequestId(11), "cam", "video"), (RequestId(13), "screen", "video")]);
subscriber.register_alias(RequestId(11), 7).unwrap();
assert!(matches!(
subscriber.register_alias(RequestId(13), 7),
Err(Error::Duplicate)
));
}
#[tokio::test(start_paused = true)]
async fn cancelling_a_subscription_stops_the_publisher() {
for version in [Version::Draft16, Version::Draft20] {
let log = cancel_a_subscription(version).await;
assert_eq!(
log.stops(),
vec![crate::ietf::error::CANCELLED],
"{version:?}: cancelling must STOP_SENDING the publisher's direction, not just FIN ours",
);
assert_ne!(
crate::ietf::error::CANCELLED,
crate::SessionError::Cancel.to_code(),
"the two error spaces disagree; that is why this code is mapped separately",
);
}
}
#[tokio::test(start_paused = true)]
async fn a_legacy_cancel_sends_unsubscribe() {
let log = cancel_a_subscription(Version::Draft16).await;
assert!(
occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0,
"draft-16 cancels with UNSUBSCRIBE",
);
let log = cancel_a_subscription(Version::Draft19).await;
assert_eq!(
occurrences(&log, &[ietf::Unsubscribe::ID as u8]),
0,
"draft-17+ has no UNSUBSCRIBE",
);
}
#[tokio::test(start_paused = true)]
async fn a_ready_rejection_beats_local_abandonment() {
const VERSION: Version = Version::Draft16;
let rejection = {
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: Some(RequestId(1)),
error_code: 0x10,
reason_phrase: "not found".into(),
retry_interval: 0,
})
.await
.unwrap();
log.writes.lock().unwrap().clone()
};
let session = crate::lite::test_transport::ScriptedSession::new(rejection);
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
drop(subscription);
drop(track);
drop(consumer);
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
tokio::time::timeout(std::time::Duration::from_secs(1), serving)
.await
.expect("run_subscribe did not finish")
.unwrap();
assert!(
!control_message_types(&log, VERSION).contains(&ietf::Unsubscribe::ID),
"a rejected request is already gone; cancelling it names a dead id at the peer",
);
}
#[tokio::test(start_paused = true)]
async fn abandoning_before_subscribe_ok_still_cancels() {
const VERSION: Version = Version::Draft16;
let session = crate::lite::test_transport::ScriptedSession::new(Vec::new());
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
settle().await;
drop(subscription);
drop(track);
drop(consumer);
tokio::time::timeout(std::time::Duration::from_secs(1), serving)
.await
.expect("run_subscribe parked waiting for a response that never came")
.unwrap();
assert!(
occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0,
"a subscribe abandoned before SUBSCRIBE_OK must still be cancelled",
);
assert_eq!(
log.stops(),
vec![crate::ietf::error::CANCELLED],
"and must stop the direction the publisher writes",
);
}
#[tokio::test(start_paused = true)]
async fn a_retraction_before_subscribe_ok_keeps_the_subscription() {
const VERSION: Version = Version::Draft16;
let session = crate::lite::test_transport::ScriptedSession::new(Vec::new());
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
settle().await;
producer.finish();
settle().await;
assert!(
!serving.is_finished(),
"a retraction ended a subscription still in flight"
);
assert_eq!(
occurrences(&log, &[ietf::Unsubscribe::ID as u8]),
0,
"a retraction must not cancel a subscription still in flight",
);
drop(subscription);
drop(track);
drop(consumer);
tokio::time::timeout(std::time::Duration::from_secs(1), serving)
.await
.expect("run_subscribe parked after its reader left")
.unwrap();
}
fn control_message_types(log: &crate::lite::test_transport::Log, version: Version) -> Vec<u64> {
use crate::coding::Decode;
let writes = log.writes.lock().unwrap().clone();
let mut buf = writes.as_slice();
let mut types = Vec::new();
while !buf.is_empty() {
let Ok(type_id) = u64::decode(&mut buf, version) else {
break;
};
let Ok(size) = u16::decode(&mut buf, version) else {
break;
};
if buf.len() < size as usize {
break;
}
buf = &buf[size as usize..];
types.push(type_id);
}
types
}
#[tokio::test(start_paused = true)]
async fn a_legacy_cancel_reaches_the_control_stream() {
const VERSION: Version = Version::Draft16;
let session = crate::lite::test_transport::ScriptedSession::new(Vec::new());
let log = session.log.clone();
let control = Control::new(None, false);
let adapter = super::super::adapter::ControlStreamAdapter::new(session.clone(), control.clone(), VERSION);
let control_stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
let running = adapter.clone();
let (_goaway_handle, goaway) = crate::goaway::Handle::new(true);
tokio::spawn(async move {
let _ = running.run(control_stream.reader, control_stream.writer, goaway).await;
});
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
adapter,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
control,
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
settle().await;
drop(subscription);
drop(track);
drop(consumer);
tokio::time::timeout(std::time::Duration::from_secs(1), serving)
.await
.expect("run_subscribe did not finish")
.unwrap();
settle().await;
let types = control_message_types(&log, VERSION);
assert!(
types.contains(&ietf::Subscribe::ID),
"the SUBSCRIBE reached the control stream: {types:?}"
);
assert!(
types.contains(&ietf::Unsubscribe::ID),
"the UNSUBSCRIBE must traverse the adapter to the control stream, not stop at the \
virtual writer: {types:?}"
);
}
async fn cancel_a_subscription(version: Version) -> crate::lite::test_transport::Log {
cancel_a_subscription_inner(version, false).await
}
#[tokio::test(start_paused = true)]
async fn a_legacy_shared_alias_is_unsubscribed() {
let log = cancel_a_subscription_inner(Version::Draft16, true).await;
assert!(
occurrences(&log, &[ietf::Unsubscribe::ID as u8]) > 0,
"abandoning a shared alias must still tell the publisher to stop",
);
}
#[tokio::test(start_paused = true)]
async fn cancelling_does_not_reset_away_the_unsubscribe() {
for version in [Version::Draft16, Version::Draft20] {
let log = cancel_a_subscription(version).await;
assert!(
log.resets().is_empty(),
"{version:?}: the send side must be closed, not reset out from under the cancellation",
);
}
}
async fn cancel_a_subscription_inner(version: Version, conflict: bool) -> crate::lite::test_transport::Log {
let subscribe_ok = {
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::SubscribeOk::ID).await.unwrap();
writer
.encode(&ietf::SubscribeOk {
request_id: match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(0)),
_ => None,
},
track_alias: 7,
largest: None,
properties: Default::default(),
})
.await
.unwrap();
log.writes.lock().unwrap().clone()
};
let session = crate::lite::test_transport::ScriptedSession::new(subscribe_ok);
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
version,
tasks,
Default::default(),
);
if conflict {
let holder = RequestId(999);
let mut state = subscriber.state.lock();
state.subscribes.insert(
holder,
TrackState {
alias: Some(7),
..TrackState::new(
track::Producer::new(std::sync::Arc::new(crate::broadcast::Info::default()), "video", None),
Path::new("broadcast").to_owned(),
kio::Producer::new(Fill::Done),
None,
)
},
);
insert_track_alias(&state.aliases, 7, holder).unwrap();
}
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
let probe = subscriber.clone();
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
settle().await;
assert!(
matches!(probe.state.lock().aliases.read().map.get(&7), Some(Alias::Active(_))),
"{version:?}: alias 7 must be bound before we cancel",
);
drop(subscription);
drop(track);
drop(consumer);
tokio::time::timeout(std::time::Duration::from_secs(1), serving)
.await
.expect("run_subscribe did not finish")
.unwrap();
log
}
async fn fill_after_subscribe_ok(largest: Option<ietf::Location>) -> bool {
let version = Version::Draft20;
let subscribe_ok = {
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::SubscribeOk::ID).await.unwrap();
writer
.encode(&ietf::SubscribeOk {
request_id: None,
track_alias: 7,
largest,
properties: Default::default(),
})
.await
.unwrap();
log.writes.lock().unwrap().clone()
};
let session = crate::lite::test_transport::ScriptedSession::new(subscribe_ok);
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
version,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let _subscription = track.subscribe(None);
let request = dynamic.requested_track().await.expect("no track requested");
let probe = subscriber.clone();
let serving = tokio::spawn(async move {
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
});
settle().await;
let outstanding = {
let state = probe.state.lock();
let track = state
.subscribes
.values()
.next()
.expect("the subscription is registered");
let fill = track.fill.read();
fill.outstanding()
};
serving.abort();
outstanding
}
#[tokio::test(start_paused = true)]
async fn an_empty_track_settles_the_fill() {
assert!(
!fill_after_subscribe_ok(None).await,
"no LARGEST_OBJECT means no content, so no fill is owed"
);
}
#[tokio::test(start_paused = true)]
async fn a_track_with_content_still_awaits_its_fill() {
assert!(
fill_after_subscribe_ok(Some(ietf::Location { group: 3, object: 4 })).await,
"a fetch stream is still owed"
);
}
#[test]
fn retired_aliases_are_capped() {
let aliases = TrackAliases::default();
for i in 0..(RETIRED_ALIAS_CAPACITY as u64 + 10) {
insert_track_alias(&aliases, i, RequestId(i)).unwrap();
retire_track_alias(&aliases, i, RequestId(i));
}
let table = aliases.read();
assert_eq!(table.retired.len(), RETIRED_ALIAS_CAPACITY);
assert_eq!(table.map.len(), RETIRED_ALIAS_CAPACITY);
assert!(!table.map.contains_key(&0), "the oldest tombstone is forgotten first");
}
#[tokio::test]
async fn assigned_peer_hop_attributes_announces() {
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let assigned = crate::Hop::new(777).unwrap();
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
origin,
Control::new(None, false),
Some(assigned),
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
Version::Draft14,
tasks,
Default::default(),
);
let advert = subscriber.route(None, &cluster::Peer::default()).expect("route");
subscriber
.start_announce(crate::Path::new("room/host").to_owned(), advert)
.unwrap();
let mut announced = consumer.announced();
let route = announced.assert_next_active("room/host");
let hops: Vec<_> = route.hops.iter().copied().collect();
assert_eq!(hops, vec![crate::Hop::UNKNOWN]);
assert!(route.is_anonymous());
let mut hidden = consumer.excluding(assigned).announced();
hidden.assert_next_wait();
}
#[tokio::test]
async fn reflected_announce_does_not_evict_the_source_we_publish() {
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let peer = crate::Hop::new(777).unwrap();
let self_origin = crate::Hop::new(1).unwrap();
let origin = crate::origin::Config::new(self_origin).produce();
let consumer = origin.consume();
let mut announced = consumer.announced();
let mut publishing = consumer.clone().excluding(peer).announced();
let upstream = crate::Hops::try_from(vec![crate::Hop::new(7).unwrap()]).unwrap();
let _source = origin
.announce("room/host", crate::origin::Route::default().with_hops(upstream.clone()))
.unwrap();
announced.assert_next_active("room/host");
let _advertised = publishing.assert_next_active("room/host");
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
origin,
Control::new(None, false),
Some(peer),
peer::PeerSetup::default(),
self_origin,
None,
Version::Draft14,
tasks,
Default::default(),
);
let advert = subscriber.route(None, &cluster::Peer::default()).expect("route");
subscriber
.start_announce(crate::Path::new("room/host").to_owned(), advert)
.unwrap();
announced.assert_next_wait();
publishing.assert_next_wait();
let route = routed_now(&consumer, "room/host").expect("still routed");
assert_eq!(route.hops, upstream);
}
#[tokio::test]
async fn reconnecting_peer_joins_the_front_it_replaces() {
let peer = crate::Hop::new(777).unwrap();
let self_origin = crate::Hop::new(1).unwrap();
let origin = crate::origin::Config::new(self_origin).produce();
let consumer = origin.consume();
let mut announced = consumer.announced();
let connect = || {
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
crate::lite::test_transport::SinkSession::new(Default::default()),
origin.clone(),
Control::new(None, false),
Some(peer),
peer::PeerSetup::default(),
self_origin,
None,
Version::Draft14,
tasks,
Default::default(),
);
let advert = subscriber.route(None, &cluster::Peer::default()).expect("route");
subscriber
.start_announce(crate::Path::new("room/host").to_owned(), advert)
.unwrap();
subscriber
};
let first = connect();
announced.assert_next_active("room/host");
let _second = connect();
announced.assert_next_wait();
drop(first);
announced.assert_next_wait();
assert!(routed_now(&consumer, "room/host").is_some());
}
fn cluster_subscriber(
self_origin: crate::Hop,
) -> (
Subscriber<crate::lite::test_transport::SinkSession>,
crate::origin::Producer,
) {
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let origin = crate::origin::Config::new(self_origin).produce();
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
origin.clone(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
self_origin,
None,
Version::Draft19,
tasks,
Default::default(),
);
(subscriber, origin)
}
fn routed_now(consumer: &crate::origin::Consumer, path: &str) -> Option<crate::origin::Route> {
use futures::FutureExt;
consumer.routed(path).now_or_never().flatten()
}
fn hop_path(ids: &[u64]) -> cluster::HopPath {
let hops = ids.iter().map(|&id| crate::Hop::new(id).unwrap()).collect::<Vec<_>>();
cluster::HopPath::new(crate::Hops::try_from(hops).unwrap())
}
#[tokio::test]
async fn cluster_advert_becomes_a_route_with_the_link_charged() {
let (subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: Some(3),
};
let advert = cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 4,
};
let advertised = subscriber.route(Some(&advert), &peer).expect("route");
assert_eq!(
advertised.route.cost.warm, 7,
"the link's price is added to the advertised cost"
);
assert_eq!(advertised.route.hops, hop_path(&[7, 9]).hops().clone());
let mut subscriber = subscriber;
subscriber
.start_announce(crate::Path::new("room/host").to_owned(), advertised)
.unwrap();
let route = routed_now(&consumer, "room/host").expect("routed");
let hops: Vec<_> = route.hops.iter().map(|h| h.id()).collect();
assert_eq!(hops, vec![7, 9]);
assert_eq!(route.cost.warm, 7);
}
#[test]
fn cluster_advert_loop_is_discarded() {
let (subscriber, _origin) = cluster_subscriber(crate::Hop::new(5).unwrap());
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: None,
};
let looped = cluster::Advert {
hops: hop_path(&[7, 5, 9]),
cost: 0,
};
assert!(subscriber.route(Some(&looped), &peer).is_none());
let clean = cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 0,
};
assert!(subscriber.route(Some(&clean), &peer).is_some());
}
#[test]
fn unpriced_link_costs_one() {
let (subscriber, _origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: None,
};
let advert = cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 2,
};
assert_eq!(subscriber.route(Some(&advert), &peer).unwrap().route.cost.warm, 3);
let free = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: Some(0),
};
assert_eq!(subscriber.route(Some(&advert), &free).unwrap().route.cost.warm, 2);
}
#[tokio::test(start_paused = true)]
async fn a_lost_namespace_stream_closes_the_broadcast() {
const VERSION: Version = Version::Draft18;
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let session = crate::lite::test_transport::ScriptedSession::eof(namespace_response(VERSION, "x.hang").await);
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer::default());
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer_setup,
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
subscriber
.run_subscribe_namespace(stream, crate::Path::new("").to_owned())
.await
.expect("a clean FIN is not an error");
settle().await;
assert!(
routed_now(&consumer, "x.hang").is_none(),
"an ended stream must retract the route, not leave a stale one",
);
}
#[tokio::test(start_paused = true)]
async fn a_re_sent_namespace_reprices_in_place() {
const VERSION: Version = Version::Draft19;
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let script = {
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::RequestOk::ID).await.unwrap();
writer.encode(&ietf::RequestOk { request_id: None }).await.unwrap();
for cost in [4, 0] {
writer.encode(&ietf::Namespace::ID).await.unwrap();
writer
.encode(&ietf::Namespace {
suffix: crate::Path::new("x.hang"),
cluster: Some(cluster::Advert {
hops: hop_path(&[7, 9]),
cost,
}),
})
.await
.unwrap();
}
log.writes.lock().unwrap().clone()
};
let session = crate::lite::test_transport::ScriptedSession::new(script);
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let peer_setup = peer::PeerSetup::default();
peer_setup.set(peer::Peer {
cluster: cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: Some(0),
},
..Default::default()
});
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer_setup,
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::new("").to_owned()));
for _ in 0..100 {
assert!(
futures::poll!(run.as_mut()).is_pending(),
"the stream stays open through a repeat"
);
if routed_now(&consumer, "x.hang").is_some_and(|route| route.cost.warm == 0) {
break;
}
settle().await;
}
let route = routed_now(&consumer, "x.hang").expect("still routed");
assert_eq!(route.cost.warm, 0, "the repeat repriced the route");
}
#[tokio::test(start_paused = true)]
async fn an_explicit_namespace_done_closes_the_broadcast() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let advert = subscriber.route(None, &cluster::Peer::default()).expect("route");
subscriber.start_announce(path.clone(), advert).unwrap();
settle().await;
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(
routed_now(&consumer, "room/host").is_none(),
"an explicit NAMESPACE_DONE must retract the route",
);
}
#[tokio::test(start_paused = true)]
async fn a_publish_namespace_done_retracts_without_faulting_the_session() {
const VERSION: Version = Version::Draft14;
let path = crate::Path::new("room/host").to_owned();
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_message(&ietf::PublishNamespaceDone {
track_namespace: path.borrow(),
request_id: RequestId(0),
})
.await
.unwrap();
let script = log.writes.lock().unwrap().clone();
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let session = crate::lite::test_transport::ScriptedSession::eof(script);
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
let msg = ietf::PublishNamespace {
request_id: RequestId(0),
track_namespace: path.borrow(),
cluster: None,
};
subscriber
.run_publish_namespace_stream(stream, msg, cluster::Peer::default(), None)
.await
.expect("a withdrawal is not a protocol violation");
settle().await;
assert!(
routed_now(&consumer, "room/host").is_none(),
"an explicit withdrawal must close the broadcast",
);
}
#[tokio::test(start_paused = true)]
async fn a_broken_publish_namespace_stream_closes_the_broadcast() {
const VERSION: Version = Version::Draft19;
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
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::NamespaceDone::ID).await.unwrap();
let script = log.writes.lock().unwrap().clone();
let session = crate::lite::test_transport::ScriptedSession::eof(script);
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks,
Default::default(),
);
let path = crate::Path::new("room/host").to_owned();
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
let msg = ietf::PublishNamespace {
request_id: RequestId(0),
track_namespace: path.borrow(),
cluster: None,
};
subscriber
.run_publish_namespace_stream(stream, msg, cluster::Peer::default(), None)
.await
.expect_err("an unexpected message ends the stream");
settle().await;
assert!(
routed_now(&consumer, "room/host").is_none(),
"a broken stream must close the broadcast, not leave a stale route",
);
}
#[tokio::test(start_paused = true)]
async fn the_last_owner_out_decides_the_detach() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let peer = cluster::Peer::default();
for _ in 0..2 {
let advert = subscriber.route(None, &peer).expect("route");
subscriber.start_announce(path.clone(), advert).unwrap();
}
settle().await;
subscriber.stop_announce(path.clone(), Detach::Abrupt).unwrap();
settle().await;
assert!(
routed_now(&consumer, "room/host").is_some(),
"the broadcast must survive while an owner remains",
);
subscriber.stop_announce(path, Detach::Graceful).unwrap();
settle().await;
assert!(
routed_now(&consumer, "room/host").is_none(),
"the last owner out must close the broadcast",
);
}
#[test]
fn a_pathless_advert_still_pays_for_its_link() {
let (unpriced, _origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let peer = cluster::Peer::default();
assert_eq!(
unpriced.route(None, &peer).unwrap().route.cost.warm,
cluster::DEFAULT_COST
);
let priced_peer = cluster::Peer {
hop: None,
cost: Some(4),
};
assert_eq!(unpriced.route(None, &priced_peer).unwrap().route.cost.warm, 4);
let (mut priced, _origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
priced.cost = Some(6);
assert_eq!(priced.route(None, &priced_peer).unwrap().route.cost.warm, 6);
}
#[tokio::test]
async fn cluster_update_replaces_in_place() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let first = Advertised {
route: crate::origin::Route::default()
.with_hops(hop_path(&[7, 9]).hops().clone())
.with_cost(4),
};
subscriber.start_announce(path.clone(), first).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
let rerouted = Advertised {
route: crate::origin::Route::default()
.with_hops(hop_path(&[7, 11]).hops().clone())
.with_cost(2),
};
subscriber.update_announce(path.clone(), rerouted).unwrap();
let route = routed_now(&consumer, "room/host").expect("routed");
let hops: Vec<_> = route.hops.iter().map(|h| h.id()).collect();
assert_eq!(hops, vec![7, 11]);
assert_eq!(route.cost.warm, 2);
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_none());
}
#[tokio::test(start_paused = true)]
async fn anonymous_publisher_survives_a_repricing_update() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: Some(0),
};
let hops = cluster::HopPath::new(
crate::Hops::try_from(vec![crate::Hop::UNKNOWN, crate::Hop::new(9).unwrap()]).unwrap(),
);
let advertised = subscriber
.route(
Some(&cluster::Advert {
hops: hops.clone(),
cost: 2,
}),
&peer,
)
.expect("route");
subscriber.start_announce(path.clone(), advertised).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
let repriced = subscriber
.route(Some(&cluster::Advert { hops, cost: 1 }), &peer)
.expect("route");
subscriber.update_announce(path.clone(), repriced).unwrap();
let route = routed_now(&consumer, "room/host").expect("still routed");
assert_eq!(
route.cost,
crate::origin::Cost {
warm: 1,
..crate::origin::Cost::UNKNOWN
},
"the repriced warm cost arrives; the Cluster extension has nowhere to carry a cold cost, so it stays unknown rather than reading as the publisher's own zero"
);
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_none());
}
#[tokio::test(start_paused = true)]
async fn separate_adverts_refcount_the_route() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: None,
};
let hops = cluster::HopPath::new(
crate::Hops::try_from(vec![crate::Hop::UNKNOWN, crate::Hop::new(9).unwrap()]).unwrap(),
);
let advert = cluster::Advert { hops, cost: 0 };
let first = subscriber.route(Some(&advert), &peer).expect("route");
subscriber.start_announce(path.clone(), first).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
let second = subscriber.route(Some(&advert), &peer).expect("route");
subscriber.start_announce(path.clone(), second).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
subscriber.stop_announce(path.clone(), Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_none());
}
#[tokio::test]
async fn pathless_adverts_never_replace_the_source() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let peer = cluster::Peer::default();
let first = subscriber.route(None, &peer).expect("route");
subscriber.start_announce(path.clone(), first).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
let second = subscriber.route(None, &peer).expect("route");
subscriber.start_announce(path.clone(), second).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
subscriber.stop_announce(path.clone(), Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(routed_now(&consumer, "room/host").is_none());
}
#[tokio::test]
async fn reflected_replacement_retracts_the_route() {
let self_origin = crate::Hop::new(5).unwrap();
let (mut subscriber, origin) = cluster_subscriber(self_origin);
let consumer = origin.consume();
let path = crate::Path::new("room/host").to_owned();
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: None,
};
let clean = cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 0,
};
let advert = subscriber.route(Some(&clean), &peer).expect("route");
subscriber.start_announce(path.clone(), advert).unwrap();
assert!(routed_now(&consumer, "room/host").is_some());
let looped = cluster::Advert {
hops: hop_path(&[7, 5, 9]),
cost: 0,
};
assert!(
subscriber.route(Some(&looped), &peer).is_none(),
"a path containing our own Hop ID is a loop"
);
subscriber.stop_announce(path, Detach::Graceful).unwrap();
assert!(
routed_now(&consumer, "room/host").is_none(),
"the superseded route must not stay attached"
);
}
async fn publish_namespace_updates(updates: &[cluster::Advert]) -> Vec<u8> {
const VERSION: Version = Version::Draft19;
let log = crate::lite::test_transport::Log::default();
let mut writer = crate::coding::Writer::new(crate::lite::test_transport::SinkSend::new(log.clone()), VERSION);
for (i, advert) in updates.iter().enumerate() {
writer.encode(&ietf::PublishNamespaceUpdate::ID).await.unwrap();
writer
.encode(&ietf::PublishNamespaceUpdate {
request_id: RequestId(3 + 2 * i as u64),
hops: Some(advert.hops.clone()),
cost: Some(advert.cost),
})
.await
.unwrap();
}
let writes = log.writes.lock().unwrap();
writes.clone()
}
async fn update_harness(
self_origin: crate::Hop,
peer: &cluster::Peer,
attached: &cluster::Advert,
script: Vec<u8>,
) -> (
Subscriber<crate::lite::test_transport::ScriptedSession>,
crate::origin::Consumer,
Stream<crate::lite::test_transport::ScriptedSession, Version>,
crate::origin::Driver,
) {
const VERSION: Version = Version::Draft19;
let session = crate::lite::test_transport::ScriptedSession::new(script);
let (origin, driver) = crate::origin::Producer::new(crate::origin::Config::new(self_origin));
let consumer = origin.consume();
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer::PeerSetup::default(),
self_origin,
None,
VERSION,
tasks,
Default::default(),
);
let path = crate::Path::new("room/host").to_owned();
let advert = subscriber.route(Some(attached), peer).expect("route");
subscriber.start_announce(path, advert).unwrap();
assert!(routed_now(&consumer, "room/host").is_some(), "attached to start with");
let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap();
(subscriber, consumer, stream, driver)
}
async fn reflected_harness(
self_origin: crate::Hop,
peer: &cluster::Peer,
attached: &cluster::Advert,
updates: &[cluster::Advert],
) -> (
Subscriber<crate::lite::test_transport::ScriptedSession>,
crate::origin::Consumer,
Stream<crate::lite::test_transport::ScriptedSession, Version>,
) {
let script = publish_namespace_updates(updates).await;
let (subscriber, consumer, stream, driver) = update_harness(self_origin, peer, attached, script).await;
std::mem::forget(driver);
(subscriber, consumer, stream)
}
fn replies(log: &crate::lite::test_transport::Log, type_id: u64) -> usize {
let writes = log.writes.lock().unwrap();
let mut count = 0;
let mut at = 0;
while at + 3 <= writes.len() {
if writes[at] as u64 == type_id {
count += 1;
}
let len = u16::from_be_bytes([writes[at + 1], writes[at + 2]]) as usize;
at += 3 + len;
}
count
}
fn peer_9() -> cluster::Peer {
cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: None,
}
}
fn clean_and_looped() -> (cluster::Advert, cluster::Advert) {
(
cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 0,
},
cluster::Advert {
hops: hop_path(&[7, 5, 9]),
cost: 0,
},
)
}
#[tokio::test]
async fn a_reflected_update_detaches_but_keeps_the_stream() {
let self_origin = crate::Hop::new(5).unwrap();
let peer = peer_9();
let (clean, looped) = clean_and_looped();
let (mut subscriber, consumer, mut stream) = reflected_harness(self_origin, &peer, &clean, &[looped]).await;
let log = subscriber.session.log.clone();
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
{
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(clean.clone()),
peer,
&mut attached,
));
for _ in 0..100 {
assert!(
futures::poll!(run.as_mut()).is_pending(),
"the stream must stay open after a reflected update"
);
if routed_now(&consumer, "room/host").is_none() {
break;
}
settle().await;
}
}
assert!(
routed_now(&consumer, "room/host").is_none(),
"an unusable path must not stay attached"
);
assert!(!attached, "the caller must not release it a second time");
assert_eq!(
replies(&log, ietf::RequestOk::ID),
1,
"the update was applied, so it is acknowledged"
);
}
#[tokio::test]
async fn a_clean_update_after_a_reflection_reattaches() {
let self_origin = crate::Hop::new(5).unwrap();
let peer = peer_9();
let (clean, looped) = clean_and_looped();
let (mut subscriber, consumer, mut stream) =
reflected_harness(self_origin, &peer, &clean, &[looped, clean.clone()]).await;
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
{
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(clean.clone()),
peer,
&mut attached,
));
for _ in 0..20 {
assert!(futures::poll!(run.as_mut()).is_pending());
settle().await;
}
}
assert!(attached, "the clean path must re-attach");
assert!(
routed_now(&consumer, "room/host").is_some(),
"the namespace is routable again",
);
}
#[tokio::test]
async fn an_explicit_zero_reprices_the_held_path() {
const VERSION: Version = Version::Draft19;
let self_origin = crate::Hop::new(5).unwrap();
let peer = cluster::Peer {
hop: Some(crate::Hop::new(9).unwrap()),
cost: Some(0),
};
let held = cluster::Advert {
hops: hop_path(&[7, 9]),
cost: 4,
};
let script = {
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::PublishNamespaceUpdate::ID).await.unwrap();
writer
.encode(&ietf::PublishNamespaceUpdate {
request_id: RequestId(3),
hops: None,
cost: Some(0),
})
.await
.unwrap();
let writes = log.writes.lock().unwrap();
writes.clone()
};
let (mut subscriber, consumer, mut stream, driver) = update_harness(self_origin, &peer, &held, script).await;
std::mem::forget(driver);
let log = subscriber.session.log.clone();
assert_eq!(routed_now(&consumer, "room/host").expect("routed").cost.warm, 4);
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
{
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(held.clone()),
peer,
&mut attached,
));
for _ in 0..20 {
assert!(futures::poll!(run.as_mut()).is_pending(), "the stream stays open");
settle().await;
}
}
let route = routed_now(&consumer, "room/host").expect("still routed");
assert_eq!(route.cost.warm, 0, "the explicit 0 replaced the held cost");
let hops: Vec<_> = route.hops.iter().map(|h| h.id()).collect();
assert_eq!(hops, vec![7, 9], "the omitted path kept its value");
assert!(attached, "a repricing is not a retraction");
assert_eq!(replies(&log, ietf::RequestOk::ID), 1);
assert_eq!(replies(&log, ietf::RequestError::ID), 0);
}
#[tokio::test]
async fn a_failed_update_withdraws_the_advertisement() {
let self_origin = crate::Hop::new(5).unwrap();
let peer = peer_9();
let (clean, _) = clean_and_looped();
let cheaper = cluster::Advert {
cost: 0,
..clean.clone()
};
let script = publish_namespace_updates(&[cheaper]).await;
let (mut subscriber, _consumer, mut stream, driver) = update_harness(self_origin, &peer, &clean, script).await;
let log = subscriber.session.log.clone();
drop(driver);
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
let mut result = None;
{
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(clean.clone()),
peer,
&mut attached,
));
for _ in 0..20 {
if let std::task::Poll::Ready(res) = futures::poll!(run.as_mut()) {
result = Some(res);
break;
}
settle().await;
}
}
assert!(
matches!(result, Some(Ok(()))),
"a refused update ends the stream cleanly, got {result:?}"
);
assert!(attached, "the caller releases the route it attached");
assert_eq!(replies(&log, ietf::RequestError::ID), 1, "REQUEST_ERROR went out");
assert_eq!(replies(&log, ietf::RequestOk::ID), 0);
}
#[tokio::test]
async fn an_update_that_changes_the_publisher_is_refused() {
let self_origin = crate::Hop::new(5).unwrap();
let peer = peer_9();
let (clean, _) = clean_and_looped();
let other_publisher = cluster::Advert {
hops: hop_path(&[8, 9]),
cost: 0,
};
let script = publish_namespace_updates(&[other_publisher]).await;
let (mut subscriber, consumer, mut stream, driver) = update_harness(self_origin, &peer, &clean, script).await;
std::mem::forget(driver);
let log = subscriber.session.log.clone();
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
let mut result = None;
{
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(clean.clone()),
peer,
&mut attached,
));
for _ in 0..20 {
if let std::task::Poll::Ready(res) = futures::poll!(run.as_mut()) {
result = Some(res);
break;
}
settle().await;
}
}
assert!(matches!(result, Some(Ok(()))), "refused cleanly, got {result:?}");
assert_eq!(replies(&log, ietf::RequestError::ID), 1, "REQUEST_ERROR went out");
assert_eq!(replies(&log, ietf::RequestOk::ID), 0);
let route = routed_now(&consumer, "room/host").expect("the caller releases the route");
let hops: Vec<_> = route.hops.iter().map(|h| h.id()).collect();
assert_eq!(hops, vec![7, 9], "the held path was not replaced");
}
#[tokio::test]
async fn a_repeated_publish_namespace_is_a_duplicate() {
const VERSION: Version = Version::Draft19;
let self_origin = crate::Hop::new(5).unwrap();
let peer = peer_9();
let (clean, _) = clean_and_looped();
let script = {
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::PublishNamespace::ID).await.unwrap();
writer
.encode(&ietf::PublishNamespace {
request_id: RequestId(1),
track_namespace: crate::Path::new("room/host"),
cluster: Some(cluster::Advert {
cost: 0,
..clean.clone()
}),
})
.await
.unwrap();
let writes = log.writes.lock().unwrap();
writes.clone()
};
let (mut subscriber, _consumer, mut stream, driver) = update_harness(self_origin, &peer, &clean, script).await;
std::mem::forget(driver);
let path = crate::Path::new("room/host").to_owned();
let mut attached = true;
let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates(
&mut stream,
&path,
Some(clean.clone()),
peer,
&mut attached,
));
let mut result = None;
for _ in 0..20 {
if let std::task::Poll::Ready(res) = futures::poll!(run.as_mut()) {
result = Some(res);
break;
}
settle().await;
}
let err = result.expect("the loop ends").expect_err("a repeat is refused");
assert!(is_protocol_violation(&err), "a duplicate request is fatal, got {err}");
}
#[tokio::test]
async fn namespace_stream_close_releases_live_paths() {
let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap());
let consumer = origin.consume();
let peer = cluster::Peer::default();
let mut live = std::collections::HashSet::new();
for path in ["room/a", "room/b"] {
let path = crate::Path::new(path).to_owned();
let advert = subscriber.route(None, &peer).expect("route");
subscriber.start_announce(path.clone(), advert).unwrap();
live.insert(path);
}
assert!(routed_now(&consumer, "room/a").is_some());
assert!(routed_now(&consumer, "room/b").is_some());
for path in live {
subscriber.stop_announce(path, Detach::Graceful).unwrap();
}
assert!(routed_now(&consumer, "room/a").is_none(), "room/a leaked a refcount");
assert!(routed_now(&consumer, "room/b").is_none(), "room/b leaked a refcount");
}
#[tokio::test]
async fn publish_is_rejected_without_announcing() {
for version in [Version::Draft14, Version::Draft19] {
let gate = kio::Producer::new(true);
let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume());
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let consumer = origin.consume();
let (tasks, task_set) = crate::util::TaskSet::new();
std::mem::forget(task_set);
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
version,
tasks,
Default::default(),
);
let stream = Stream::open(&mut session.clone(), version).await.unwrap();
let msg = ietf::Publish {
request_id: RequestId(1),
track_namespace: crate::Path::new("room/host"),
track_name: "video".into(),
track_alias: 7,
largest_location: None,
forward: true,
properties: ietf::Properties::default(),
};
subscriber.run_publish_stream(stream, msg).await.unwrap();
tokio::time::sleep(Duration::from_millis(1)).await;
assert!(
routed_now(&consumer, "room/host").is_none(),
"a rejected PUBLISH must not announce a broadcast"
);
let expected = {
const NOT_SUPPORTED: u64 = 0x3;
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::PublishError::ID).await.unwrap();
writer
.encode(&ietf::PublishError {
request_id: RequestId(1),
error_code: NOT_SUPPORTED,
reason_phrase: "PUBLISH is not supported".into(),
})
.await
.unwrap();
}
_ => {
writer.encode(&ietf::RequestError::ID).await.unwrap();
writer
.encode(&ietf::RequestError {
request_id: None,
error_code: NOT_SUPPORTED,
reason_phrase: "PUBLISH is not supported".into(),
retry_interval: 0,
})
.await
.unwrap();
}
}
log.writes.lock().unwrap().clone()
};
assert_eq!(
occurrences(&session.log, &expected),
1,
"{version} must decline the PUBLISH as NOT_SUPPORTED"
);
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
struct Join {
filter: Filter,
fill: Option<ietf::Fill>,
fetch: Option<JoiningFetch>,
}
fn subscribe_join(
start: Option<track::Position>,
end: Option<track::Position>,
version: Version,
) -> Result<Join, Error> {
if !Filter::is_draft20(version) {
if start.is_some_and(|start| start.frame != 0) || end.is_some() {
return Err(Error::Unsupported);
}
return Ok(Join {
filter: Filter::NextObject,
fill: None,
fetch: Some(match start {
None => JoiningFetch::Relative { group_offset: 0 },
Some(start) => JoiningFetch::Absolute { group_id: start.group },
}),
});
}
Ok(match start {
None => Join {
filter: Filter::NextObject,
fill: Some(ietf::Fill {
filter: Some(Filter::Relative(1)),
range_filters: false,
}),
fetch: None,
},
Some(start) if start == track::Position::group(0) && end.is_none() => Join {
filter: Filter::Unfiltered,
fill: None,
fetch: None,
},
Some(start) => Join {
filter: Filter::Absolute {
start: ietf::Location {
group: start.group,
object: start.frame,
},
end: end.and_then(|end| {
if end.frame == 0 {
Some(ietf::EndLocation {
group: end.group.checked_sub(1)?,
object: None,
})
} else {
Some(ietf::EndLocation {
group: end.group,
object: Some(end.frame - 1),
})
}
}),
},
fill: None,
fetch: None,
},
})
}
fn next_object_id(prior: Option<u64>, delta: u64, start: u64) -> Result<u64, Error> {
let object = match prior {
None => delta,
Some(prior) => prior
.checked_add(delta)
.and_then(|id| id.checked_add(1))
.ok_or(Error::Decode(crate::coding::DecodeError::BoundsExceeded))?,
};
let expected = prior.map_or(start, |prior| prior.saturating_add(1));
if object != expected {
tracing::warn!(
object,
expected,
"object IDs must start at the group's start and increment by 1"
);
return Err(Error::Unsupported);
}
Ok(object)
}
#[cfg(test)]
mod object_id_tests {
use super::*;
#[test]
fn accepts_sequential_ids_from_zero() {
let mut prior = None;
for expected in 0..4 {
let object = next_object_id(prior, 0, 0).expect("sequential");
assert_eq!(object, expected);
prior = Some(object);
}
}
#[test]
fn rejects_a_group_that_does_not_start_at_zero() {
assert!(matches!(next_object_id(None, 6, 0), Err(Error::Unsupported)));
}
#[test]
fn accepts_a_tail_that_starts_where_the_fill_stopped() {
assert_eq!(next_object_id(None, 6, 6).expect("the fill's next object"), 6);
assert_eq!(next_object_id(Some(6), 0, 6).expect("then sequential"), 7);
assert!(matches!(next_object_id(None, 5, 6), Err(Error::Unsupported)));
assert!(matches!(next_object_id(None, 7, 6), Err(Error::Unsupported)));
}
#[test]
fn rejects_a_gap() {
assert!(matches!(next_object_id(Some(0), 1, 0), Err(Error::Unsupported)));
assert!(matches!(next_object_id(Some(3), 9, 0), Err(Error::Unsupported)));
}
#[test]
fn rejects_an_overflow() {
assert!(next_object_id(Some(u64::MAX), 0, 0).is_err());
}
}
#[cfg(test)]
mod filter_tests {
use super::*;
#[test]
fn live_joins_the_current_group_with_a_fill() {
assert_eq!(
subscribe_join(None, None, Version::Draft20).unwrap(),
Join {
filter: Filter::NextObject,
fill: Some(ietf::Fill {
filter: Some(Filter::Relative(1)),
range_filters: false,
}),
fetch: None,
}
);
}
#[test]
fn a_past_start_is_absolute() {
assert_eq!(
subscribe_join(
Some(track::Position::group(7)),
track::Position::after_group(9),
Version::Draft20,
)
.unwrap(),
Join {
filter: Filter::Absolute {
start: ietf::Location { group: 7, object: 0 },
end: Some(ietf::EndLocation { group: 9, object: None }),
},
fill: None,
fetch: None,
}
);
}
#[test]
fn the_whole_track_is_unfiltered() {
assert_eq!(
subscribe_join(Some(track::Position::group(0)), None, Version::Draft20).unwrap(),
Join {
filter: Filter::Unfiltered,
fill: None,
fetch: None,
}
);
}
const JOINING_DRAFTS: [Version; 6] = [
Version::Draft14,
Version::Draft15,
Version::Draft16,
Version::Draft17,
Version::Draft18,
Version::Draft19,
];
#[test]
fn older_drafts_live_join_with_a_relative_fetch() {
for version in JOINING_DRAFTS {
assert_eq!(
subscribe_join(None, None, version).unwrap(),
Join {
filter: Filter::NextObject,
fill: None,
fetch: Some(JoiningFetch::Relative { group_offset: 0 }),
},
"{version}"
);
}
}
#[test]
fn older_drafts_absolute_join_at_the_start_group() {
for version in JOINING_DRAFTS {
assert_eq!(
subscribe_join(Some(track::Position::group(7)), None, version).unwrap(),
Join {
filter: Filter::NextObject,
fill: None,
fetch: Some(JoiningFetch::Absolute { group_id: 7 }),
},
"{version}"
);
}
}
#[test]
fn older_drafts_refuse_a_frame_level_start() {
for version in JOINING_DRAFTS {
assert!(
matches!(
subscribe_join(Some(track::Position { group: 7, frame: 1 }), None, version),
Err(Error::Unsupported)
),
"{version}"
);
}
}
#[test]
fn older_drafts_refuse_a_bounded_end() {
for version in JOINING_DRAFTS {
assert!(
matches!(
subscribe_join(
Some(track::Position::group(7)),
track::Position::after_group(9),
version
),
Err(Error::Unsupported)
),
"{version}"
);
}
}
}
#[cfg(test)]
mod stitch_tests {
use bytes::BufMut as _;
use super::*;
use crate::{
Timestamp,
coding::Encode as _,
lite::test_transport::ScriptedSession,
model::ProduceTest,
transport::poll::Session as _,
util::{TaskSet, Tasks},
};
const VERSION: Version = Version::Draft20;
const ALIAS: u64 = 7;
const REQUEST: RequestId = RequestId(1);
const SEQUENCE: u64 = 4;
fn timestamp(index: usize) -> Timestamp {
Timestamp::from_micros(1000 + index as u64).expect("in range")
}
fn fill_stream(sequence: u64, payloads: &[&[u8]]) -> Vec<u8> {
fill_stream_for(REQUEST, &[(sequence, payloads)], true)
}
fn fill_stream_for<B: AsRef<[u8]>>(request_id: RequestId, groups: &[(u64, &[B])], timed: bool) -> Vec<u8> {
let mut buf = bytes::BytesMut::new();
ietf::FetchHeader::TYPE.encode(&mut buf, VERSION).unwrap();
ietf::FetchHeader { request_id }.encode(&mut buf, VERSION).unwrap();
let mut object_index = 0usize;
let mut prev_group = None;
for &(sequence, payloads) in groups {
for (index, payload) in payloads.iter().enumerate() {
let payload = payload.as_ref();
let properties = timed.then(|| {
let mut properties = bytes::BytesMut::new();
ietf::encode_object_time(&mut properties, timestamp(object_index), Timescale::MICRO, VERSION)
.unwrap();
properties.to_vec()
});
let first = index == 0;
let group = match (first, prev_group) {
(false, _) => None,
(true, None) => Some(sequence),
(true, Some(prev)) => Some(sequence.checked_sub(prev + 1).expect("ascending groups")),
};
ietf::FetchObject::Object {
subgroup: ietf::FetchSubgroup::Zero,
group,
object: first.then_some(0),
priority: first.then_some(0),
properties,
}
.encode(&mut buf, VERSION)
.unwrap();
(payload.len() as u64).encode(&mut buf, VERSION).unwrap();
buf.put_slice(payload);
object_index += 1;
}
prev_group = Some(sequence);
}
buf.to_vec()
}
fn tail_stream(sequence: u64, start: u64, payloads: &[&[u8]]) -> Vec<u8> {
let mut buf = bytes::BytesMut::new();
ietf::GroupHeader {
track_alias: ALIAS,
group_id: sequence,
sub_group_id: 0,
publisher_priority: 0,
flags: ietf::GroupFlags {
first_object: start == 0,
..Default::default()
},
}
.encode(&mut buf, VERSION)
.unwrap();
for (index, payload) in payloads.iter().enumerate() {
let delta = match index {
0 => start,
_ => 0,
};
delta.encode(&mut buf, VERSION).unwrap();
(payload.len() as u64).encode(&mut buf, VERSION).unwrap();
buf.put_slice(payload);
}
buf.to_vec()
}
struct Harness {
subscriber: Subscriber<ScriptedSession>,
session: ScriptedSession,
track: track::Producer,
fill: kio::Producer<Fill>,
_tasks: (Tasks, TaskSet),
}
impl Harness {
fn new(fill: Fill, scripts: Vec<Vec<u8>>) -> Self {
let session = ScriptedSession::per_stream_eof(scripts);
let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce();
let tasks = TaskSet::new();
let subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
origin,
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
VERSION,
tasks.0.clone(),
Default::default(),
);
let track = track::Producer::new(
std::sync::Arc::new(crate::broadcast::Info::default()),
"video",
track::Info::default().with_timescale(Timescale::MICRO),
);
let fill = kio::Producer::new(fill);
{
let mut state = subscriber.state.lock();
state.subscribes.insert(
REQUEST,
TrackState {
alias: Some(ALIAS),
timescale: Some(Timescale::MICRO),
..TrackState::new(track.clone(), Path::new("broadcast").to_owned(), fill.clone(), None)
},
);
insert_track_alias(&state.aliases, ALIAS, REQUEST).unwrap();
}
Self {
subscriber,
session,
track,
fill,
_tasks: tasks,
}
}
fn with_joining(self, joining: JoiningFetch, fetch_id: RequestId, largest: ietf::Location) -> Self {
{
let mut state = self.subscriber.state.lock();
state.fetches.insert(fetch_id, REQUEST);
if let Some(track) = state.subscribes.get_mut(&REQUEST) {
track.fetch_id = Some(fetch_id);
track.joining = Some(joining);
track.largest = Some(largest);
}
}
self
}
async fn stream(&self) -> Reader<<ScriptedSession as web_transport_trait::poll::Session>::RecvStream, Version> {
let mut session = self.session.clone();
let (_, recv) = session.open_bi().await.unwrap();
Reader::new(recv, VERSION)
}
}
async fn read_group(subscriber: &mut track::Subscriber) -> (u64, Vec<(Timestamp, Vec<u8>)>) {
let mut group = subscriber
.recv_group()
.await
.expect("track aborted")
.expect("track finished");
let sequence = group.sequence;
let mut frames = Vec::new();
while let Some(frame) = group.read_frame().await.expect("group aborted") {
frames.push((frame.timestamp, frame.payload.to_vec()));
}
(sequence, frames)
}
#[tokio::test]
async fn a_fill_and_its_tail_stitch_into_one_group() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream(SEQUENCE, &[b"head-0", b"head-1"]),
tail_stream(SEQUENCE, 2, &[b"tail-2"]),
],
);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut tail = h.stream().await;
let mut serve_tail = h.subscriber.clone();
let mut serve_fill = h.subscriber.clone();
let (tail, head) = futures::join!(serve_tail.recv_group(&mut tail), serve_fill.recv_fill(&mut fill));
head.expect("fill");
tail.expect("tail");
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(
frames,
vec![
(timestamp(0), b"head-0".to_vec()),
(timestamp(1), b"head-1".to_vec()),
(frames[2].0, b"tail-2".to_vec()),
]
);
assert!(matches!(*h.fill.read(), Fill::Done), "the head was claimed");
}
#[tokio::test]
async fn an_empty_tail_finishes_the_filled_group() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream(SEQUENCE, &[b"head-0", b"head-1"]),
tail_stream(SEQUENCE, 2, &[]),
],
);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut tail = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("fill");
h.subscriber.clone().recv_group(&mut tail).await.expect("tail");
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 2, "the head is the whole group");
}
#[tokio::test]
async fn a_head_finishing_after_teardown_is_published_not_installed() {
let track = track::Producer::new(
std::sync::Arc::new(crate::broadcast::Info::default()),
"video",
track::Info::default().with_timescale(Timescale::MICRO),
);
let mut consumer = track.subscribe(None);
let mut producer = track.create_group(group::Info { sequence: SEQUENCE }).unwrap();
producer.write_frame(timestamp(0), b"head-0".as_slice()).unwrap();
let mut fill = Fill::Done;
fill.install(Fill::Ready {
sequence: SEQUENCE,
next: 1,
producer,
});
assert!(matches!(fill, Fill::Done), "Done is terminal");
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 1, "published rather than left unfinished");
}
#[tokio::test]
async fn a_whole_group_for_a_headed_sequence_is_refused() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream(SEQUENCE, &[b"head-0", b"head-1"]),
tail_stream(SEQUENCE, 0, &[b"again-0"]),
],
);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut again = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("fill");
assert!(matches!(
h.subscriber.clone().recv_group(&mut again).await,
Err(Error::Unsupported)
));
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 2, "the head is published once, not twice");
}
#[tokio::test]
async fn a_cancelled_session_aborts_a_waiting_fill_head() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![fill_stream(SEQUENCE, &[b"head-0"])],
);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("fill");
let mut group = consumer
.recv_group()
.await
.expect("track aborted")
.expect("track finished");
drop(h);
assert!(
matches!(group.read_frame().await, Err(Error::Cancel)),
"a waiting fill head must be cancelled, not left parked"
);
}
#[tokio::test]
async fn a_whole_group_that_precedes_the_head_wins_the_sequence() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
tail_stream(SEQUENCE, 0, &[b"whole-0"]),
fill_stream(SEQUENCE, &[b"head-0", b"head-1"]),
],
);
let mut consumer = h.track.subscribe(None);
let mut whole = h.stream().await;
let mut fill = h.stream().await;
h.subscriber
.clone()
.recv_group(&mut whole)
.await
.expect("the whole group");
assert!(
h.subscriber.clone().recv_fill(&mut fill).await.is_err(),
"the fill cannot create a second producer for a live sequence"
);
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 1, "the group is whatever one producer wrote, not both");
}
#[tokio::test]
async fn a_tail_without_a_fill_is_dropped() {
let h = Harness::new(Fill::Done, vec![tail_stream(SEQUENCE, 2, &[b"tail-2"])]);
let mut consumer = h.track.subscribe(None);
let mut tail = h.stream().await;
assert!(matches!(
h.subscriber.clone().recv_group(&mut tail).await,
Err(Error::Unsupported)
));
let delivered = tokio::time::timeout(Duration::from_millis(50), async {
let mut group = consumer.recv_group().await.ok().flatten()?;
group.read_frame().await.ok().flatten()
})
.await;
assert!(matches!(delivered, Err(_) | Ok(None)), "no frame is delivered");
}
#[tokio::test]
async fn a_head_that_misses_the_tail_is_refused() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream(SEQUENCE, &[b"head-0", b"head-1"]),
tail_stream(SEQUENCE, 5, &[b"tail-5"]),
],
);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut tail = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("fill");
assert!(matches!(
h.subscriber.clone().recv_group(&mut tail).await,
Err(Error::Unsupported)
));
let (_, frames) = read_group(&mut consumer).await;
assert_eq!(frames.len(), 2, "the head is published as the prefix it is");
}
#[tokio::test]
async fn an_unsolicited_fill_is_refused() {
let h = Harness::new(Fill::Done, vec![fill_stream(SEQUENCE, &[b"head-0"])]);
let mut fill = h.stream().await;
assert!(matches!(
h.subscriber.clone().recv_fill(&mut fill).await,
Err(Error::Unsupported)
));
}
const FETCH: RequestId = RequestId(3);
const LIVE: ietf::Location = ietf::Location {
group: SEQUENCE,
object: 1,
};
#[tokio::test]
async fn a_joining_fetch_stitches_a_mid_group_tail() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream_for(FETCH, &[(SEQUENCE, &[b"head-0", b"head-1"])], true),
tail_stream(SEQUENCE, 2, &[b"tail-2"]),
],
)
.with_joining(JoiningFetch::Relative { group_offset: 0 }, FETCH, LIVE);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut tail = h.stream().await;
let mut serve_tail = h.subscriber.clone();
let mut serve_fill = h.subscriber.clone();
let (tail, head) = futures::join!(serve_tail.recv_group(&mut tail), serve_fill.recv_fill(&mut fill));
head.expect("joining fetch");
tail.expect("tail");
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 3);
assert_eq!(frames[0].1, b"head-0");
assert_eq!(frames[1].1, b"head-1");
assert_eq!(frames[2].1, b"tail-2");
}
#[tokio::test]
async fn a_whole_group_stream_discards_the_joining_fetch() {
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
tail_stream(SEQUENCE, 0, &[b"whole-0"]),
fill_stream_for(FETCH, &[(SEQUENCE, &[b"head-0", b"head-1"])], true),
],
)
.with_joining(JoiningFetch::Relative { group_offset: 0 }, FETCH, LIVE);
let mut consumer = h.track.subscribe(None);
let mut whole = h.stream().await;
let mut fill = h.stream().await;
h.subscriber
.clone()
.recv_group(&mut whole)
.await
.expect("the whole group");
assert!(
h.subscriber.clone().recv_fill(&mut fill).await.is_err(),
"the fetch cannot create a second producer for a live sequence"
);
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, SEQUENCE);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].1, b"whole-0");
}
#[test]
fn a_later_fetch_group_field_is_a_delta() {
assert_eq!(
resolve_fetch_group(Version::Draft18, Some(7), Some(0)).unwrap(),
Some(8)
);
assert_eq!(
resolve_fetch_group(Version::Draft20, Some(7), Some(0)).unwrap(),
Some(8)
);
assert_eq!(
resolve_fetch_group(Version::Draft17, Some(7), Some(8)).unwrap(),
Some(8)
);
}
#[tokio::test]
async fn an_absolute_fetch_stitches_into_the_live_tail() {
const START: u64 = 7;
const LIVE_GROUP: u64 = 10;
let largest = ietf::Location {
group: LIVE_GROUP,
object: 1,
};
let groups: &[(u64, &[&[u8]])] = &[
(START, &[b"g7-0"]),
(8, &[b"g8-0"]),
(9, &[b"g9-0"]),
(LIVE_GROUP, &[b"g10-0", b"g10-1"]),
];
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream_for(FETCH, groups, false),
tail_stream(LIVE_GROUP, 2, &[b"g10-2"]),
],
)
.with_joining(JoiningFetch::Absolute { group_id: START }, FETCH, largest);
let mut consumer = h
.track
.subscribe(track::Subscription::default().with_max_age(Duration::from_secs(60)));
let mut fill = h.stream().await;
let mut tail = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("absolute fetch");
h.subscriber.clone().recv_group(&mut tail).await.expect("tail");
let mut groups = Vec::new();
for _ in 0..4 {
groups.push(read_group(&mut consumer).await);
}
assert_eq!(
groups
.iter()
.map(|(seq, frames)| (*seq, frames.iter().map(|(_, p)| p.as_slice()).collect::<Vec<_>>()))
.collect::<Vec<_>>(),
vec![
(7, vec![b"g7-0".as_slice()]),
(8, vec![b"g8-0".as_slice()]),
(9, vec![b"g9-0".as_slice()]),
(10, vec![b"g10-0".as_slice(), b"g10-1".as_slice(), b"g10-2".as_slice()]),
]
);
}
#[tokio::test]
async fn a_short_fetch_delivers_its_prefix() {
const START: u64 = 7;
let largest = ietf::Location { group: 10, object: 1 };
let h = Harness::new(
Fill::Serving(Some(Timescale::MICRO)),
vec![
fill_stream_for(FETCH, &[(START, &[b"g7-0", b"g7-1"])], true),
tail_stream(10, 2, &[b"g10-2"]),
],
)
.with_joining(JoiningFetch::Absolute { group_id: START }, FETCH, largest);
let mut consumer = h.track.subscribe(None);
let mut fill = h.stream().await;
let mut tail = h.stream().await;
h.subscriber.clone().recv_fill(&mut fill).await.expect("short fetch");
assert!(matches!(
h.subscriber.clone().recv_group(&mut tail).await,
Err(Error::Unsupported)
));
let (sequence, frames) = read_group(&mut consumer).await;
assert_eq!(sequence, START);
assert_eq!(frames.len(), 2, "the prefix that arrived is published");
assert_eq!(frames[0].1, b"g7-0");
assert_eq!(frames[1].1, b"g7-1");
}
}
#[cfg(test)]
mod joining_fetch_tests {
use super::*;
use crate::{
coding::Encode as _,
lite::test_transport::ScriptedSession,
model::ProduceTest,
util::{TaskSet, Tasks},
};
const JOINING_DRAFTS: [Version; 6] = [
Version::Draft14,
Version::Draft15,
Version::Draft16,
Version::Draft17,
Version::Draft18,
Version::Draft19,
];
async fn settle() {
tokio::time::sleep(Duration::from_millis(1)).await;
}
fn message_bytes<M: Message>(id: u64, msg: &M, version: Version) -> Vec<u8> {
let mut buf = Vec::new();
id.encode(&mut buf, version).unwrap();
msg.encode(&mut buf, version).unwrap();
buf
}
fn subscribe_ok(version: Version, largest: Option<ietf::Location>) -> Vec<u8> {
message_bytes(
ietf::SubscribeOk::ID,
&ietf::SubscribeOk {
request_id: match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(1)),
_ => None,
},
track_alias: 7,
largest,
properties: Default::default(),
},
version,
)
}
fn fetch_ok(version: Version) -> Vec<u8> {
message_bytes(
ietf::FetchOk::ID,
&ietf::FetchOk {
request_id: match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(RequestId(3)),
_ => None,
},
group_order: GroupOrder::Ascending,
end_of_track: false,
end_location: ietf::Location { group: 4, object: 2 },
},
version,
)
}
fn fetch_error(version: Version) -> Vec<u8> {
match version {
Version::Draft14 => message_bytes(
ietf::FetchError::ID,
&ietf::FetchError {
request_id: RequestId(3),
error_code: 1,
reason_phrase: "refused".into(),
},
version,
),
Version::Draft15 | Version::Draft16 => message_bytes(
ietf::RequestError::ID,
&ietf::RequestError {
request_id: Some(RequestId(3)),
error_code: 1,
reason_phrase: "refused".into(),
retry_interval: 0,
},
version,
),
_ => message_bytes(
ietf::RequestError::ID,
&ietf::RequestError {
request_id: None,
error_code: 1,
reason_phrase: "refused".into(),
retry_interval: 0,
},
version,
),
}
}
fn decode_messages(log: &crate::lite::test_transport::Log, version: Version) -> Vec<(u64, bytes::Bytes)> {
use crate::coding::Decode;
let writes = log.writes.lock().unwrap().clone();
let mut buf = writes.as_slice();
let mut messages = Vec::new();
while !buf.is_empty() {
let Ok(type_id) = u64::decode(&mut buf, version) else {
break;
};
let Ok(size) = u16::decode(&mut buf, version) else {
break;
};
if buf.len() < size as usize {
break;
}
let (body, rest) = buf.split_at(size as usize);
messages.push((type_id, bytes::Bytes::copy_from_slice(body)));
buf = rest;
}
messages
}
struct JoinRun {
subscriber: Subscriber<ScriptedSession>,
session: ScriptedSession,
_hold: (
crate::broadcast::Producer,
track::Consumer,
kio::Pending<track::Subscribing>,
),
_tasks: (Tasks, TaskSet),
serving: tokio::task::JoinHandle<()>,
}
impl JoinRun {
async fn start(version: Version, start: Option<track::Position>, ok: Vec<u8>, fetch: Vec<u8>) -> Self {
let session = ScriptedSession::per_stream(vec![ok, fetch]);
let (tasks, _task_set) = crate::util::TaskSet::new();
let subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session.clone(),
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
version,
tasks.clone(),
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let subscription = match start {
None => track.subscribe(None),
Some(start) => track.subscribe(track::Subscription::default().with_start(start)),
};
let request = dynamic.requested_track().await.expect("no track requested");
let mut serving_subscriber = subscriber.clone();
let serving = tokio::spawn(async move {
serving_subscriber
.run_subscribe(Path::new("broadcast"), dynamic, request)
.await;
});
settle().await;
Self {
subscriber,
session,
_hold: (producer, track, subscription),
_tasks: (tasks, _task_set),
serving,
}
}
fn fill_outstanding(&self) -> bool {
let state = self.subscriber.state.lock();
let track = state
.subscribes
.values()
.next()
.expect("the subscription is registered");
track.fill.read().outstanding()
}
}
impl Drop for JoinRun {
fn drop(&mut self) {
self.serving.abort();
}
}
#[tokio::test(start_paused = true)]
async fn a_live_join_is_spelled_as_largest_object_plus_relative_fetch() {
let largest = Some(ietf::Location { group: 4, object: 1 });
for version in JOINING_DRAFTS {
let run = JoinRun::start(version, None, subscribe_ok(version, largest), fetch_ok(version)).await;
let messages = decode_messages(&run.session.log, version);
let subscribe = messages
.iter()
.find(|(id, _)| *id == ietf::Subscribe::ID)
.expect("SUBSCRIBE");
let mut body = subscribe.1.clone();
let msg = ietf::Subscribe::decode_msg(&mut body, version).unwrap();
assert_eq!(msg.filter, Filter::NextObject, "{version}");
assert!(msg.fill.is_none(), "{version}");
let fetch = messages.iter().find(|(id, _)| *id == ietf::Fetch::ID).expect("FETCH");
let mut body = fetch.1.clone();
let msg = ietf::Fetch::decode_msg(&mut body, version).unwrap();
assert_eq!(
msg.fetch_type,
FetchType::RelativeJoining {
subscriber_request_id: RequestId(1),
group_offset: 0,
},
"{version}"
);
}
}
#[tokio::test(start_paused = true)]
async fn an_absolute_join_is_spelled_at_the_start_group() {
let largest = Some(ietf::Location { group: 9, object: 0 });
for version in JOINING_DRAFTS {
let run = JoinRun::start(
version,
Some(track::Position::group(7)),
subscribe_ok(version, largest),
fetch_ok(version),
)
.await;
let messages = decode_messages(&run.session.log, version);
let fetch = messages.iter().find(|(id, _)| *id == ietf::Fetch::ID).expect("FETCH");
let mut body = fetch.1.clone();
let msg = ietf::Fetch::decode_msg(&mut body, version).unwrap();
assert_eq!(
msg.fetch_type,
FetchType::AbsoluteJoining {
subscriber_request_id: RequestId(1),
group_id: 7,
},
"{version}"
);
}
}
#[tokio::test(start_paused = true)]
async fn a_refused_fetch_continues_live() {
let largest = Some(ietf::Location { group: 4, object: 1 });
for version in JOINING_DRAFTS {
let run = JoinRun::start(version, None, subscribe_ok(version, largest), fetch_error(version)).await;
assert!(
!run.fill_outstanding(),
"{version}: a refused FETCH must not leave the fill waiting"
);
assert!(
run.subscriber.state.lock().subscribes.values().next().is_some(),
"{version}: the subscription continues live"
);
}
}
#[tokio::test(start_paused = true)]
async fn a_frame_level_start_never_reaches_the_wire() {
let version = Version::Draft19;
let session = ScriptedSession::new(Vec::new());
let log = session.log.clone();
let (tasks, _task_set) = crate::util::TaskSet::new();
let mut subscriber = Subscriber::new(
crate::time::Clock::tokio(),
session,
crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(),
Control::new(None, false),
None,
peer::PeerSetup::default(),
crate::Hop::new(1).unwrap(),
None,
version,
tasks,
Default::default(),
);
let producer = crate::broadcast::Info::default().produce();
let mut dynamic = producer.dynamic();
let consumer = producer.consume();
let track = consumer.track("video").unwrap();
let _subscription =
track.subscribe(track::Subscription::default().with_start(track::Position { group: 7, frame: 1 }));
let request = dynamic.requested_track().await.expect("no track requested");
subscriber.run_subscribe(Path::new("broadcast"), dynamic, request).await;
let writes = log.writes.lock().unwrap().clone();
assert!(writes.is_empty(), "a refused join must not write SUBSCRIBE");
}
}