use crate::{group, origin, track};
use std::{collections::HashMap, task::Poll};
use futures::{FutureExt, StreamExt, stream::FuturesUnordered};
use web_transport_trait::SendStream;
use crate::{
AsPath, Error, Timescale,
coding::{Stream, Writer},
ietf::{self, Control, FetchHeader, FetchType, FilterType, GroupOrder, Location, RequestId},
track::Subscription,
util::{MaybeBoxedExt, MaybeSendBox},
};
use super::{Message, Version, cluster};
struct Watched {
broadcast: crate::broadcast::Consumer,
demand: crate::broadcast::Demand,
sent: Advert,
idle_at: Option<web_async::time::Instant>,
dead: bool,
}
impl Watched {
fn new(broadcast: crate::broadcast::Consumer) -> Self {
Self {
demand: broadcast.demand(),
broadcast,
sent: Advert::None,
idle_at: None,
dead: false,
}
}
fn set_sent(&mut self, sent: Advert) {
if !sent.discounted() {
self.idle_at = None;
}
self.sent = sent;
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
enum Advert {
#[default]
None,
Plain,
Cluster(cluster::Advert),
}
impl Advert {
fn wanted(&self) -> bool {
!matches!(self, Self::None)
}
fn params(&self) -> Option<cluster::Advert> {
match self {
Self::Cluster(advert) => Some(advert.clone()),
_ => None,
}
}
fn discounted(&self) -> bool {
matches!(self, Self::Cluster(advert) if advert.cost == 0)
}
}
enum Watch {
Changed(crate::PathOwned),
Idle(crate::PathOwned),
}
enum NamespaceEvent {
Closed(Result<(), Error>),
Update(Option<crate::announce::Update>),
Routes(crate::PathOwned),
Idle(crate::PathOwned),
Linger,
}
#[derive(Clone)]
pub(super) struct Publisher<S: web_transport_trait::Session> {
session: S,
origin: origin::Consumer,
control: Control,
self_origin: crate::Origin,
peer_origin: Option<crate::Origin>,
peer_setup: cluster::PeerSetup,
version: Version,
}
impl<S: web_transport_trait::Session> Publisher<S> {
pub fn new(
session: S,
origin: origin::Consumer,
control: Control,
peer_origin: Option<crate::Origin>,
peer_setup: cluster::PeerSetup,
version: Version,
) -> Self {
Self {
session,
self_origin: *origin,
origin,
control,
peer_origin,
peer_setup,
version,
}
}
async fn peer(&self) -> cluster::Peer {
match cluster::supported(self.version) {
true => self.peer_setup.get().await,
false => cluster::Peer::default(),
}
}
async fn serving_origin(&self) -> origin::Consumer {
let peer = self.peer().await;
match self.exclude(&peer) {
crate::Origin::UNKNOWN => self.origin.clone(),
exclude => self.origin.clone().excluding(exclude),
}
}
fn exclude(&self, peer: &cluster::Peer) -> crate::Origin {
match peer.negotiated() {
true => peer.exclude(),
false => self.peer_origin.unwrap_or(crate::Origin::UNKNOWN),
}
}
fn select(&self, watch: &Watched, peer: &cluster::Peer) -> Advert {
let routes = watch.broadcast.routes();
let exclude = self.exclude(peer);
for (route, serving) in crate::broadcast::advertisable_routes(&routes, self.self_origin, exclude) {
if !peer.negotiated() {
return Advert::Plain;
}
let cost = crate::broadcast::outgoing_cost(&watch.demand, route, serving);
match cluster::Advert::forward(&route.hops, cost, self.self_origin) {
Ok(advert) => return Advert::Cluster(advert),
Err(_) => continue,
}
}
Advert::None
}
fn poll_watched(
watched: &mut HashMap<crate::PathOwned, Watched>,
fired: Option<web_async::time::Instant>,
waiter: &kio::Waiter,
) -> Poll<Watch> {
for (path, watch) in watched.iter_mut() {
if watch.dead {
continue;
}
match watch.broadcast.poll_routes_changed(waiter) {
Poll::Ready(Ok(())) => return Poll::Ready(Watch::Changed(path.clone())),
Poll::Ready(Err(_)) => {
watch.dead = true;
continue;
}
Poll::Pending => {}
}
if !matches!(watch.sent, Advert::Cluster(_)) {
continue;
}
if !watch.sent.discounted() {
if let Poll::Ready(Ok(())) = watch.demand.poll_used(waiter) {
return Poll::Ready(Watch::Changed(path.clone()));
}
continue;
}
match watch.idle_at {
Some(_) if watch.demand.is_used() => watch.idle_at = None,
Some(at) if fired.is_some_and(|now| now >= at + crate::broadcast::COST_LINGER) => {
watch.idle_at = None;
return Poll::Ready(Watch::Changed(path.clone()));
}
Some(_) => {
let _ = watch.demand.poll_used(waiter);
continue;
}
None => {}
}
if let Poll::Ready(Ok(())) = watch.demand.poll_unused(waiter) {
return Poll::Ready(Watch::Idle(path.clone()));
}
}
Poll::Pending
}
fn linger_deadline(watched: &HashMap<crate::PathOwned, Watched>) -> Option<web_async::time::Instant> {
watched
.values()
.filter_map(|watch| watch.idle_at)
.min()
.map(|at| at + crate::broadcast::COST_LINGER)
}
pub fn handle_stream(
&self,
id: u64,
mut data: bytes::Bytes,
stream: Stream<S, Version>,
) -> Result<MaybeSendBox<'static, ()>, Error> {
let this = self.clone();
let task = match id {
ietf::Subscribe::ID => {
let msg = ietf::Subscribe::decode_msg(&mut data, this.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received subscribe");
async move {
if let Err(err) = this.run_subscribe_stream(stream, msg).await {
tracing::debug!(%err, "subscribe stream error");
}
}
.maybe_boxed()
}
ietf::Fetch::ID => {
let msg = ietf::Fetch::decode_msg(&mut data, this.version)?;
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received fetch");
async move {
if let Err(err) = this.run_fetch_stream(stream, msg).await {
tracing::debug!(%err, "fetch stream error");
}
}
.maybe_boxed()
}
ietf::SubscribeNamespace::ID | ietf::SubscribeNamespaceLegacy::ID => {
let msg = if id == ietf::SubscribeNamespace::ID {
ietf::SubscribeNamespace::decode_msg(&mut data, this.version)?
} else {
let legacy = ietf::SubscribeNamespaceLegacy::decode_msg(&mut data, this.version)?;
ietf::SubscribeNamespace {
request_id: legacy.request_id,
namespace: legacy.namespace,
}
};
if !data.is_empty() {
return Err(Error::WrongSize);
}
tracing::debug!(message = ?msg, "received subscribe_namespace");
async move {
if let Err(err) = this.run_subscribe_namespace_stream(stream, msg).await {
tracing::debug!(%err, "subscribe_namespace stream error");
}
}
.maybe_boxed()
}
ietf::TrackStatus::ID => {
tracing::warn!("TrackStatus not supported");
async {}.maybe_boxed()
}
_ => {
tracing::warn!(id, "unexpected bidi stream type for publisher");
return Err(Error::UnexpectedStream);
}
};
Ok(task)
}
async fn run_subscribe_stream(self, mut stream: Stream<S, Version>, msg: ietf::Subscribe<'_>) -> Result<(), Error> {
match msg.filter_type {
FilterType::AbsoluteStart | FilterType::AbsoluteRange => {
tracing::warn!(?msg, "absolute subscribe not supported, ignoring");
}
FilterType::NextGroup => {
tracing::warn!(?msg, "next group subscribe not supported, ignoring");
}
FilterType::LargestObject => {}
};
let request_id = msg.request_id;
let track_name = msg.track_name.clone();
let absolute = self.origin.absolute(&msg.track_namespace).to_owned();
tracing::info!(id = %request_id, broadcast = %absolute, track = %track_name, "subscribe started");
let broadcast = match self
.serving_origin()
.await
.request_broadcast(&msg.track_namespace)
.await
{
Ok(broadcast) => broadcast,
Err(_) => {
self.write_subscribe_error(&mut stream.writer, request_id, 404, "Broadcast not found")
.await?;
return Ok(());
}
};
let subscription = Subscription {
priority: msg.subscriber_priority,
..Default::default()
};
let track = match async { broadcast.track(&msg.track_name)?.subscribe(subscription).await }.await {
Ok(track) => track,
Err(err) => {
self.write_subscribe_error(&mut stream.writer, request_id, 404, &err.to_string())
.await?;
return Ok(());
}
};
stream.writer.encode(&ietf::SubscribeOk::ID).await?;
stream
.writer
.encode(&ietf::SubscribeOk {
request_id: match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id),
_ => None,
},
track_alias: request_id.0,
timescale: Some(track.info().timescale),
})
.await?;
let res = {
let mut serve = std::pin::pin!(self.run_track(track, request_id));
let mut reader_closed = std::pin::pin!(stream.reader.closed());
let mut session_closed = std::pin::pin!(self.session.closed());
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(serve.as_mut()) {
return Poll::Ready(res);
}
if waiter.poll_future(reader_closed.as_mut()).is_ready()
|| waiter.poll_future(session_closed.as_mut()).is_ready()
{
return Poll::Ready(Ok(()));
}
Poll::Pending
})
.await
};
let (status_code, reason) = match &res {
Ok(()) => (200, "OK"),
Err(_) => (500, "error"),
};
let _ = stream.writer.encode(&ietf::PublishDone::ID).await;
let _ = stream
.writer
.encode(&ietf::PublishDone {
request_id: match self.version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => Some(request_id),
_ => None,
},
status_code,
stream_count: 0,
reason_phrase: reason.into(),
})
.await;
stream.writer.finish().ok();
res
}
async fn write_subscribe_error(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::SubscribeError::ID).await?;
writer
.encode(&ietf::SubscribeError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
async fn run_track(&self, mut track: track::Subscriber, request_id: RequestId) -> Result<(), Error> {
let mut tasks = FuturesUnordered::new();
loop {
let group = {
kio::wait(|waiter| {
let mut cx = std::task::Context::from_waker(waiter.waker());
while let std::task::Poll::Ready(Some(())) = tasks.poll_next_unpin(&mut cx) {}
track.poll_recv_group(waiter)
})
.await
};
let Some(group) = group? else {
while tasks.next().await.is_some() {}
return Ok(());
};
let sequence = group.sequence;
tracing::debug!(subscribe = %request_id, track = %track.name(), sequence, "serving group");
let msg = ietf::GroupHeader {
track_alias: request_id.0,
group_id: sequence,
sub_group_id: 0,
publisher_priority: 0,
flags: ietf::GroupFlags {
has_extensions: true,
..Default::default()
},
};
let priority = track.subscription().priority;
let timescale = track.info().timescale;
tasks
.push(Self::run_group(self.session.clone(), msg, priority, group, timescale, self.version).map(|_| ()));
}
}
async fn run_group(
session: S,
msg: ietf::GroupHeader,
priority: u8,
mut group: group::Consumer,
timescale: Timescale,
version: Version,
) -> Result<(), Error> {
let mut stream = session.open_uni().await.map_err(Error::from_transport)?;
stream.set_priority(priority);
let mut stream = Writer::new(stream, version);
stream.encode(&msg).await?;
loop {
let frame = {
let mut closed = std::pin::pin!(stream.closed());
kio::wait(|waiter| {
if waiter.poll_future(closed.as_mut()).is_ready() {
return Poll::Ready(Err(Error::Cancel));
}
group.poll_next_frame(waiter)
})
.await
};
let mut frame = match frame? {
Some(frame) => frame,
None => break,
};
stream.encode(&0u64).await?;
if msg.flags.has_extensions {
let mut ext = bytes::BytesMut::new();
ietf::encode_object_time(&mut ext, frame.timestamp, timescale, version)?;
stream.encode(&(ext.len() as u64)).await?;
stream.write_chunk(ext.freeze()).await?;
}
stream.encode(&frame.size).await?;
if frame.size == 0 {
stream.encode(&0u8).await?;
} else {
loop {
let chunk = {
let mut closed = std::pin::pin!(stream.closed());
kio::wait(|waiter| {
if waiter.poll_future(closed.as_mut()).is_ready() {
return Poll::Ready(Err(Error::Cancel));
}
frame.poll_read_chunk(waiter)
})
.await
};
match chunk? {
Some(chunk) => {
stream.write_chunk(chunk).await?;
}
None => break,
}
}
}
}
stream.finish()?;
stream.closed().await?;
tracing::debug!(sequence = %msg.group_id, "finished group");
Ok(())
}
async fn run_fetch_stream(self, mut stream: Stream<S, Version>, msg: ietf::Fetch<'_>) -> Result<(), Error> {
let _subscribe_id = match msg.fetch_type {
FetchType::Standalone { .. } => {
self.write_fetch_error(&mut stream.writer, msg.request_id, 500, "not supported")
.await?;
return Ok(());
}
FetchType::RelativeJoining {
subscriber_request_id,
group_offset,
} => {
if group_offset != 0 {
self.write_fetch_error(&mut stream.writer, msg.request_id, 500, "not supported")
.await?;
return Ok(());
}
subscriber_request_id
}
FetchType::AbsoluteJoining { .. } => {
self.write_fetch_error(&mut stream.writer, msg.request_id, 500, "not supported")
.await?;
return Ok(());
}
};
self.write_fetch_ok(&mut stream.writer, msg.request_id).await?;
let uni = self.session.open_uni().await.map_err(Error::from_transport)?;
let mut writer = Writer::new(uni, self.version);
writer.encode(&FetchHeader::TYPE).await?;
writer
.encode(&FetchHeader {
request_id: msg.request_id,
})
.await?;
writer.finish()?;
writer.closed().await?;
Ok(())
}
async fn write_fetch_ok(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::FetchOk::ID).await?;
writer
.encode(&ietf::FetchOk {
request_id: Some(request_id),
group_order: GroupOrder::Descending,
end_of_track: false,
end_location: Location { group: 0, object: 0 },
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestOk::ID).await?;
writer
.encode(&ietf::RequestOk {
request_id: Some(request_id),
})
.await?;
}
_ => {
writer.encode(&ietf::RequestOk::ID).await?;
writer.encode(&ietf::RequestOk { request_id: None }).await?;
}
}
Ok(())
}
async fn write_fetch_error(
&self,
writer: &mut Writer<S::SendStream, Version>,
request_id: RequestId,
error_code: u64,
reason: &str,
) -> Result<(), Error> {
match self.version {
Version::Draft14 => {
writer.encode(&ietf::FetchError::ID).await?;
writer
.encode(&ietf::FetchError {
request_id,
error_code,
reason_phrase: reason.into(),
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: Some(request_id),
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
_ => {
writer.encode(&ietf::RequestError::ID).await?;
writer
.encode(&ietf::RequestError {
request_id: None,
error_code,
reason_phrase: reason.into(),
retry_interval: 0,
})
.await?;
}
}
Ok(())
}
async fn sync_namespace(
&self,
suffix: &crate::PathOwned,
path: &crate::PathOwned,
peer: &cluster::Peer,
watched: &mut HashMap<crate::PathOwned, Watched>,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
stream: &mut Stream<S, Version>,
) -> Result<(), Error> {
let Some(watch) = watched.get(suffix) else {
return Ok(());
};
let advert = self.select(watch, peer);
if advert == watch.sent {
return Ok(());
}
let held = watch.sent.wanted();
let absolute = self.origin.absolute(path).to_owned();
let sent = match self.version {
Version::Draft14 | Version::Draft15 => {
match (advert.wanted(), requests.get_mut(suffix)) {
(false, _) => {
if held {
tracing::debug!(broadcast = %absolute, "namespace_done");
}
self.withdraw_namespace(stream, requests, suffix.clone()).await?;
}
(true, Some(request)) => {
tracing::debug!(broadcast = %absolute, "announce update");
request.stream.writer.encode(&ietf::PublishNamespace::ID).await?;
request
.stream
.writer
.encode(&ietf::PublishNamespace {
request_id: request.request_id,
track_namespace: request.path.as_path(),
cluster: advert.params(),
})
.await?;
}
(true, None) => {
tracing::debug!(broadcast = %absolute, "namespace");
self.advertise_namespace(requests, path, suffix.clone(), advert.params())
.await?;
}
}
match requests.contains_key(suffix) {
true => advert,
false => Advert::None,
}
}
_ => {
match (advert.wanted(), held) {
(true, _) => {
tracing::debug!(broadcast = %absolute, "namespace");
stream.writer.encode(&ietf::Namespace::ID).await?;
stream
.writer
.encode(&ietf::Namespace {
suffix: suffix.as_path(),
cluster: advert.params(),
})
.await?;
}
(false, true) => {
tracing::debug!(broadcast = %absolute, "namespace_done");
stream.writer.encode(&ietf::NamespaceDone::ID).await?;
stream
.writer
.encode(&ietf::NamespaceDone {
suffix: suffix.as_path(),
})
.await?;
}
(false, false) => {}
}
advert
}
};
if let Some(watch) = watched.get_mut(suffix) {
watch.set_sent(sent);
}
Ok(())
}
async fn advertise_namespace(
&self,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
path: &crate::PathOwned,
suffix: crate::PathOwned,
cluster: Option<cluster::Advert>,
) -> Result<(), Error> {
let request_id = self.control.next_request_id().await?;
let mut request = Stream::open(&self.session, self.version).await?;
request.writer.encode(&ietf::PublishNamespace::ID).await?;
request
.writer
.encode(&ietf::PublishNamespace {
request_id,
track_namespace: path.as_path(),
cluster,
})
.await?;
let type_id: u64 = request.reader.decode().await?;
let size: u16 = request.reader.decode().await?;
let mut data = request.reader.read_exact(size as usize).await?;
match (self.version, type_id) {
(Version::Draft14, ietf::PublishNamespaceOk::ID) => {
let msg = ietf::PublishNamespaceOk::decode_msg(&mut data, self.version)?;
tracing::debug!(message = ?msg, "publish namespace ok");
}
(Version::Draft14, ietf::PublishNamespaceError::ID) => {
let msg = ietf::PublishNamespaceError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "publish namespace error");
return Ok(());
}
(_, ietf::RequestOk::ID) => {
let msg = ietf::RequestOk::decode_msg(&mut data, self.version)?;
tracing::debug!(message = ?msg, "publish namespace ok");
}
(_, ietf::RequestError::ID) => {
let msg = ietf::RequestError::decode_msg(&mut data, self.version)?;
tracing::warn!(message = ?msg, "publish namespace error");
return Ok(());
}
_ => return Err(Error::UnexpectedMessage),
}
requests.insert(
suffix,
NamespaceRequest {
path: path.clone(),
request_id,
stream: request,
},
);
Ok(())
}
async fn withdraw_namespace(
&self,
stream: &mut Stream<S, Version>,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
suffix: crate::PathOwned,
) -> Result<(), Error> {
match self.version {
Version::Draft14 | Version::Draft15 => {
if let Some(mut request) = requests.remove(&suffix) {
let _ = request
.stream
.writer
.encode_message(&ietf::PublishNamespaceDone {
track_namespace: request.path.as_path(),
request_id: request.request_id,
})
.await;
request.stream.writer.finish().ok();
}
}
_ => {
stream.writer.encode(&ietf::NamespaceDone::ID).await?;
stream
.writer
.encode(&ietf::NamespaceDone {
suffix: suffix.as_path(),
})
.await?;
}
}
Ok(())
}
async fn withdraw_requests(
&self,
stream: &mut Stream<S, Version>,
requests: &mut HashMap<crate::PathOwned, NamespaceRequest<S>>,
) {
let suffixes: Vec<crate::PathOwned> = requests.keys().cloned().collect();
for suffix in suffixes {
let _ = self.withdraw_namespace(stream, requests, suffix).await;
}
}
async fn run_subscribe_namespace_stream(
self,
mut stream: Stream<S, Version>,
msg: ietf::SubscribeNamespace<'_>,
) -> Result<(), Error> {
let prefix = msg.namespace.to_owned();
tracing::debug!(prefix = %self.origin.absolute(&prefix), "subscribe_namespace stream");
let origin = self
.origin
.scope(&[prefix.as_path()])
.unwrap_or_else(|| self.origin.empty());
match self.version {
Version::Draft14 => {
stream.writer.encode(&ietf::SubscribeNamespaceOk::ID).await?;
stream
.writer
.encode(&ietf::SubscribeNamespaceOk {
request_id: msg.request_id,
})
.await?;
}
Version::Draft15 | Version::Draft16 => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream
.writer
.encode(&ietf::RequestOk {
request_id: Some(msg.request_id),
})
.await?;
}
_ => {
stream.writer.encode(&ietf::RequestOk::ID).await?;
stream.writer.encode(&ietf::RequestOk { request_id: None }).await?;
}
}
let mut announced = origin.announced();
let mut watched: HashMap<crate::PathOwned, Watched> = HashMap::new();
let mut requests: HashMap<crate::PathOwned, NamespaceRequest<S>> = HashMap::new();
let peer = self.peer().await;
let mut linger = kio::time::Deadline::new();
let res = loop {
linger.set(Self::linger_deadline(&watched));
let event = {
let mut closed = std::pin::pin!(stream.reader.closed());
kio::wait(|waiter| {
if let Poll::Ready(res) = waiter.poll_future(closed.as_mut()) {
return Poll::Ready(NamespaceEvent::Closed(res));
}
if let Poll::Ready(update) = announced.poll_next(waiter) {
return Poll::Ready(NamespaceEvent::Update(update));
}
let fired = linger.poll(waiter).is_ready().then(web_async::time::Instant::now);
match Self::poll_watched(&mut watched, fired, waiter) {
Poll::Ready(Watch::Changed(path)) => return Poll::Ready(NamespaceEvent::Routes(path)),
Poll::Ready(Watch::Idle(path)) => return Poll::Ready(NamespaceEvent::Idle(path)),
Poll::Pending => {}
}
match fired.is_some() {
true => Poll::Ready(NamespaceEvent::Linger),
false => Poll::Pending,
}
})
.await
};
match event {
NamespaceEvent::Closed(res) => break res,
NamespaceEvent::Linger => continue,
NamespaceEvent::Update(None) => {
self.withdraw_requests(&mut stream, &mut requests).await;
stream.writer.finish()?;
return stream.writer.closed().await;
}
NamespaceEvent::Update(Some(crate::announce::Update { path, broadcast })) => {
let suffix = path
.strip_prefix(&prefix)
.expect("origin returned invalid path")
.to_owned();
let path = path.to_owned();
match broadcast {
Some(broadcast) => {
watched.insert(suffix.clone(), Watched::new(broadcast));
self.sync_namespace(&suffix, &path, &peer, &mut watched, &mut requests, &mut stream)
.await?;
}
None => {
let held = watched.remove(&suffix).is_some_and(|watch| watch.sent.wanted());
if held {
tracing::debug!(broadcast = %self.origin.absolute(&path), "namespace_done");
self.withdraw_namespace(&mut stream, &mut requests, suffix).await?;
}
}
}
}
NamespaceEvent::Routes(suffix) => {
let path = prefix.join(&suffix);
self.sync_namespace(&suffix, &path, &peer, &mut watched, &mut requests, &mut stream)
.await?;
}
NamespaceEvent::Idle(suffix) => {
if let Some(watch) = watched.get_mut(&suffix) {
watch.idle_at = Some(web_async::time::Instant::now());
}
}
}
};
self.withdraw_requests(&mut stream, &mut requests).await;
res
}
}
struct NamespaceRequest<S: web_transport_trait::Session> {
path: crate::PathOwned,
request_id: RequestId,
stream: Stream<S, Version>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lite::test_transport::SinkSession;
async fn settle() {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
fn occurrences(log: &crate::lite::test_transport::Log, needle: &[u8]) -> usize {
let writes = log.writes.lock().unwrap();
writes.windows(needle.len()).filter(|window| *window == needle).count()
}
#[tokio::test]
async fn assigned_peer_origin_filters_echoed_announces() {
let assigned = crate::Origin::new(777).unwrap();
let other = crate::Origin::new(778).unwrap();
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let consumer = origin.consume();
let session = crate::lite::test_transport::SinkSession::new(Default::default());
let publisher = Publisher::new(
session,
origin.consume(),
Control::new(None, false),
Some(assigned),
cluster::PeerSetup::default(),
Version::Draft16,
);
let mut echoed_hops = crate::OriginList::new();
echoed_hops.push(assigned).unwrap();
let _echoed = origin
.create_broadcast(
"from/peer",
crate::broadcast::Route::new()
.with_hops(echoed_hops)
.with_announce(true),
)
.unwrap();
let mut local_hops = crate::OriginList::new();
local_hops.push(other).unwrap();
let _local = origin
.create_broadcast(
"from/us",
crate::broadcast::Route::new().with_hops(local_hops).with_announce(true),
)
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
let peer = cluster::Peer::default();
let echoed = consumer.get_broadcast("from/peer").unwrap();
assert!(!publisher.select(&Watched::new(echoed), &peer).wanted());
let local = consumer.get_broadcast("from/us").unwrap();
assert_eq!(publisher.select(&Watched::new(local), &peer), Advert::Plain);
}
#[tokio::test]
async fn linger_clears_when_the_advert_stops_being_discounted() {
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let broadcast = origin
.clone()
.create_broadcast("cam", crate::broadcast::Route::announced())
.unwrap();
let mut watch = Watched::new(broadcast.consume());
let hops = crate::OriginList::try_from(vec![crate::Origin::new(7).unwrap()]).unwrap();
watch.set_sent(Advert::Cluster(cluster::Advert {
hops: cluster::HopPath::new(hops.clone()),
cost: 0,
}));
watch.idle_at = Some(web_async::time::Instant::now());
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert!(Publisher::<SinkSession>::linger_deadline(&watched).is_some());
let mut watch = watched.into_values().next().unwrap();
watch.set_sent(Advert::Cluster(cluster::Advert {
hops: cluster::HopPath::new(hops),
cost: 9,
}));
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert_eq!(
Publisher::<SinkSession>::linger_deadline(&watched),
None,
"a non-discounted advert must not leave a deadline behind"
);
let mut watch = watched.into_values().next().unwrap();
watch.idle_at = Some(web_async::time::Instant::now());
watch.set_sent(Advert::None);
let watched = HashMap::from([(crate::Path::new("cam").to_owned(), watch)]);
assert_eq!(Publisher::<SinkSession>::linger_deadline(&watched), None);
}
#[tokio::test]
async fn namespace_follows_route_eligibility_changes() {
let assigned = crate::Origin::new(777).unwrap();
let clean_publisher = crate::Origin::new(778).unwrap();
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let gate = kio::Producer::new(true);
let session = SinkSession::gated_bi(gate.consume());
let log = session.log.clone();
let publisher = Publisher::new(
session.clone(),
origin.consume(),
Control::new(None, false),
Some(assigned),
cluster::PeerSetup::default(),
Version::Draft16,
);
let mut tainted_hops = crate::OriginList::new();
tainted_hops.push(assigned).unwrap();
let _tainted = origin
.create_broadcast(
"route-flip-cam",
crate::broadcast::Route::new()
.with_hops(tainted_hops)
.with_announce(true),
)
.unwrap();
settle().await;
let stream = Stream::open(&session, Version::Draft16).await.unwrap();
let msg = ietf::SubscribeNamespace {
request_id: RequestId(1),
namespace: crate::Path::new(""),
};
let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg));
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(occurrences(&log, b"route-flip-cam"), 0);
let mut clean_hops = crate::OriginList::new();
clean_hops.push(clean_publisher).unwrap();
let clean = origin
.create_broadcast(
"route-flip-cam",
crate::broadcast::Route::new().with_hops(clean_hops).with_announce(true),
)
.unwrap();
settle().await;
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(
occurrences(&log, b"route-flip-cam"),
1,
"NAMESPACE after a clean route joins"
);
drop(clean);
settle().await;
assert!(futures::poll!(run.as_mut()).is_pending());
assert_eq!(
occurrences(&log, b"route-flip-cam"),
2,
"NAMESPACE_DONE after the last clean route detaches"
);
}
async fn publish_namespace_ok(version: Version) -> Vec<u8> {
let log = crate::lite::test_transport::Log::default();
let mut writer = crate::coding::Writer::new(crate::lite::test_transport::SinkSend::new(log.clone()), version);
match version {
Version::Draft14 => {
writer.encode(&ietf::PublishNamespaceOk::ID).await.unwrap();
writer
.encode(&ietf::PublishNamespaceOk {
request_id: RequestId(1),
})
.await
.unwrap();
}
_ => {
writer.encode(&ietf::RequestOk::ID).await.unwrap();
writer
.encode(&ietf::RequestOk {
request_id: Some(RequestId(1)),
})
.await
.unwrap();
}
}
let writes = log.writes.lock().unwrap();
writes.clone()
}
#[tokio::test]
async fn v14_subscribe_namespace_is_answered_with_publish_namespace() {
const VERSION: Version = Version::Draft14;
let origin = crate::origin::Info::new(crate::Origin::new(1).unwrap()).produce();
let consumer = origin.consume();
let early = origin
.create_broadcast("early-cam", crate::broadcast::Route::new().with_announce(true))
.unwrap();
settle().await;
let ok = publish_namespace_ok(VERSION).await;
let session = crate::lite::test_transport::ScriptedSession::per_stream(vec![Vec::new(), ok.clone(), ok]);
let log = session.log.clone();
let publisher = Publisher::new(
session.clone(),
consumer,
Control::new(None, false),
None,
cluster::PeerSetup::default(),
VERSION,
);
let stream = Stream::open(&session, VERSION).await.unwrap();
let msg = ietf::SubscribeNamespace {
request_id: RequestId(1),
namespace: crate::Path::new(""),
};
let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg));
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"early-cam") >= 1 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"early-cam"),
1,
"PUBLISH_NAMESPACE after subscribing"
);
let _late = origin
.create_broadcast("late-cam", crate::broadcast::Route::new().with_announce(true))
.unwrap();
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"late-cam") >= 1 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"late-cam"),
1,
"PUBLISH_NAMESPACE for a live announce"
);
drop(early);
for _ in 0..100 {
assert!(futures::poll!(run.as_mut()).is_pending());
if occurrences(&log, b"early-cam") >= 2 {
break;
}
settle().await;
}
assert_eq!(
occurrences(&log, b"early-cam"),
2,
"PUBLISH_NAMESPACE_DONE on unannounce"
);
assert_eq!(log.bi_opens(), 3, "no extra stream for the withdrawal");
}
}