use crate::{broadcast, cache, stats, track};
use kio::Pollable;
use std::{
cmp::Reverse,
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
fmt,
sync::Arc,
sync::atomic::{AtomicU64, Ordering},
task::{Poll, ready},
time::Duration,
};
use rand::RngExt;
use super::{
Requests, WeakCache, WeakEntry,
front::{Action, Candidate, Event, Front, Pin, Refusal},
};
use crate::{
AsPath, Error, InvalidPattern, Path, PathOwned, Pattern, Patterns,
coding::{BoundsExceeded, Decode, DecodeError, Encode, EncodeError},
runtime::{Instant, Timers},
time::Clock,
util::{Keepalive, TaskSet, Tasks, TasksWeak},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hop {
id: u64,
}
impl Hop {
pub const UNKNOWN: Self = Self { id: 0 };
pub fn new(id: u64) -> Result<Self, InvalidHop> {
if id == 0 || id >= 1u64 << 62 {
return Err(InvalidHop::Range);
}
Ok(Self { id })
}
pub fn random() -> Self {
let mut rng = rand::rng();
let id = rng.random_range(1..(1u64 << 53));
Self { id }
}
pub fn id(self) -> u64 {
self.id
}
pub(crate) fn from_wire(id: u64) -> Result<Self, DecodeError> {
if id >= 1u64 << 62 {
return Err(DecodeError::InvalidValue);
}
Ok(Self { id })
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub hop: Hop,
pub pool: cache::Pool,
pub cache_duration: Duration,
pub default_max_age: Duration,
}
impl Default for Config {
fn default() -> Self {
let pool = cache::Pool::new(cache::Config::default().with_expiry(cache::DEFAULT_EXPIRY));
Self {
hop: Hop::random(),
pool,
cache_duration: Duration::MAX,
default_max_age: track::DEFAULT_MAX_AGE,
}
}
}
impl Config {
pub fn new(hop: Hop) -> Self {
Self { hop, ..Self::default() }
}
}
impl From<Hop> for Config {
fn from(hop: Hop) -> Self {
Self::new(hop)
}
}
impl TryFrom<u64> for Hop {
type Error = InvalidHop;
fn try_from(id: u64) -> Result<Self, Self::Error> {
Self::new(id)
}
}
impl fmt::Display for Hop {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.id.fmt(f)
}
}
impl<V: Copy> Encode<V> for Hop
where
u64: Encode<V>,
{
fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
self.id.encode(w, version)
}
}
impl<V: Copy> Decode<V> for Hop
where
u64: Decode<V>,
{
fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
Self::from_wire(u64::decode(r, version)?)
}
}
pub(crate) const MAX_HOPS: usize = 32;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Hops(Vec<Hop>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidHop {
Range,
TooMany,
Duplicate,
}
impl fmt::Display for InvalidHop {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Range => write!(f, "local hop id must be non-zero and below 2^62"),
Self::TooMany => write!(f, "too many hops (max {MAX_HOPS})"),
Self::Duplicate => write!(f, "hop already in the chain"),
}
}
}
impl std::error::Error for InvalidHop {}
impl From<InvalidHop> for DecodeError {
fn from(err: InvalidHop) -> Self {
match err {
InvalidHop::TooMany => DecodeError::BoundsExceeded,
InvalidHop::Range | InvalidHop::Duplicate => DecodeError::InvalidValue,
}
}
}
impl Hops {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, hop: Hop) -> Result<(), InvalidHop> {
if self.0.len() >= MAX_HOPS {
return Err(InvalidHop::TooMany);
}
if hop != Hop::UNKNOWN && self.0.contains(&hop) {
return Err(InvalidHop::Duplicate);
}
self.0.push(hop);
Ok(())
}
pub fn contains(&self, hop: &Hop) -> bool {
self.0.contains(hop)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, Hop> {
self.0.iter()
}
pub fn as_slice(&self) -> &[Hop] {
&self.0
}
}
impl TryFrom<Vec<Hop>> for Hops {
type Error = InvalidHop;
fn try_from(v: Vec<Hop>) -> Result<Self, Self::Error> {
if v.len() > MAX_HOPS {
return Err(InvalidHop::TooMany);
}
for (i, hop) in v.iter().enumerate() {
if *hop != Hop::UNKNOWN && v[i + 1..].contains(hop) {
return Err(InvalidHop::Duplicate);
}
}
Ok(Self(v))
}
}
impl<'a> IntoIterator for &'a Hops {
type Item = &'a Hop;
type IntoIter = std::slice::Iter<'a, Hop>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<V: Copy> Encode<V> for Hops
where
u64: Encode<V>,
Hop: Encode<V>,
{
fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
(self.0.len() as u64).encode(w, version)?;
for origin in &self.0 {
origin.encode(w, version)?;
}
Ok(())
}
}
impl<V: Copy> Decode<V> for Hops
where
u64: Decode<V>,
Hop: Decode<V>,
{
fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
let count = u64::decode(r, version)? as usize;
if count > MAX_HOPS {
return Err(DecodeError::BoundsExceeded);
}
let mut list = Self(Vec::with_capacity(count));
for _ in 0..count {
list.push(Hop::decode(r, version)?)?;
}
Ok(list)
}
}
const MAX_COST: u64 = (1 << 62) - 1;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Cost {
pub warm: u64,
pub cold: u64,
}
impl Cost {
pub const fn new(cost: u64) -> Self {
Self { warm: cost, cold: cost }
}
pub const MAX: Self = Self::new(MAX_COST);
pub const DRAIN: Self = Self::MAX;
pub(crate) const UNKNOWN: Self = Self {
warm: 0,
cold: MAX_COST,
};
pub(crate) fn charged(self, link_cost: u64) -> Self {
Self {
warm: self.warm.saturating_add(link_cost).min(MAX_COST),
cold: self.cold.saturating_add(link_cost).min(MAX_COST),
}
}
pub(crate) fn clamped(self) -> Self {
Self {
warm: self.warm.min(MAX_COST),
cold: self.cold.min(MAX_COST),
}
}
}
impl From<u64> for Cost {
fn from(cost: u64) -> Self {
Self::new(cost)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Route {
pub hops: Hops,
pub cost: Cost,
pub(crate) via: Hop,
pub(crate) source: Source,
}
impl Default for Route {
fn default() -> Self {
Self {
hops: Hops::new(),
cost: Cost::default(),
via: Hop::UNKNOWN,
source: Source::Local,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Source {
#[default]
Local,
Peer(Hop),
}
impl Route {
pub fn with_hops(mut self, hops: Hops) -> Self {
self.hops = hops;
self
}
pub fn with_cost(mut self, cost: impl Into<Cost>) -> Self {
self.cost = cost.into();
self
}
pub(crate) fn with_via(mut self, via: Hop) -> Self {
self.via = via;
self
}
pub fn is_anonymous(&self) -> bool {
self.hops.iter().any(|hop| *hop == Hop::UNKNOWN)
}
pub fn source(&self) -> Source {
self.source
}
}
static NEXT_CONSUMER_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct ConsumerId(u64);
impl ConsumerId {
fn new() -> Self {
Self(NEXT_CONSUMER_ID.fetch_add(1, Ordering::Relaxed))
}
}
fn fnv_key(name: &str, origins: impl IntoIterator<Item = Hop>) -> u64 {
const SEED: u64 = 0x420C0DECB00B; const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = SEED;
for &byte in name.as_bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME);
}
for origin in origins {
for &byte in &origin.id().to_le_bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME);
}
}
hash
}
fn route_order(prefix: &Path, entry: &RouteEntry) -> (bool, Cost, bool, usize, u64, Reverse<u64>) {
(
entry.is_anonymous(),
entry.cost,
!entry.local,
entry.hops.len(),
fnv_key(prefix.as_str(), entry.hops.iter().copied()),
Reverse(entry.id),
)
}
type RouteMeta = (Hops, Cost, Source);
type AnnounceMeta = (RouteMeta, Option<Vec<Pattern>>);
enum PendingUpdate {
Announce(AnnounceMeta),
Unannounce(AnnounceMeta),
UnannounceAnnounce { old: AnnounceMeta, new: AnnounceMeta },
}
#[derive(Default)]
struct OriginConsumerState {
pending: BTreeMap<PathOwned, PendingUpdate>,
delivered: BTreeSet<PathOwned>,
ended: bool,
}
impl OriginConsumerState {
fn apply_announce(&mut self, prefix: PathOwned, meta: RouteMeta, captures: Option<Vec<Pattern>>) {
let meta = (meta, captures);
let new = match self.pending.remove(&prefix) {
None | Some(PendingUpdate::Announce(_)) => PendingUpdate::Announce(meta),
Some(PendingUpdate::Unannounce(old) | PendingUpdate::UnannounceAnnounce { old, .. }) => {
PendingUpdate::UnannounceAnnounce { old, new: meta }
}
};
self.pending.insert(prefix, new);
}
fn apply_unannounce(&mut self, prefix: PathOwned, last: RouteMeta, captures: Option<Vec<Pattern>>) {
let last = (last, captures);
match self.pending.remove(&prefix) {
Some(PendingUpdate::Announce(_)) if !self.delivered.contains(&prefix) => {}
None | Some(PendingUpdate::Announce(_) | PendingUpdate::Unannounce(_)) => {
self.pending.insert(prefix, PendingUpdate::Unannounce(last));
}
Some(PendingUpdate::UnannounceAnnounce { old, .. }) => {
self.pending.insert(prefix, PendingUpdate::Unannounce(old));
}
}
}
fn take(&mut self) -> Option<AnnounceUpdate> {
let prefix = self.pending.keys().next()?.clone();
let ((meta, captures), kind) = match self.pending.remove(&prefix).unwrap() {
PendingUpdate::Announce(meta) => {
let kind = match self.delivered.insert(prefix.clone()) {
true => AnnounceKind::Announced,
false => AnnounceKind::Updated,
};
(meta, kind)
}
PendingUpdate::Unannounce(meta) => {
self.delivered.remove(&prefix);
(meta, AnnounceKind::Retracted)
}
PendingUpdate::UnannounceAnnounce { old, new } => {
self.delivered.remove(&prefix);
self.pending.insert(prefix.clone(), PendingUpdate::Announce(new));
(old, AnnounceKind::Retracted)
}
};
Some(AnnounceUpdate {
prefix,
captures,
route: Route {
hops: meta.0,
cost: meta.1,
via: Hop::UNKNOWN,
source: meta.2,
},
kind,
})
}
}
struct RouteEntry {
id: u64,
prefix: PathOwned,
scope: Patterns,
hops: Hops,
cost: Cost,
via: Hop,
local: bool,
peer: bool,
server: Option<kio::Shared<ServeState>>,
source: Option<broadcast::Consumer>,
advertised: bool,
claim: Pattern,
}
impl RouteEntry {
fn is_anonymous(&self) -> bool {
self.hops.iter().any(|hop| *hop == Hop::UNKNOWN)
}
fn entered(&self) -> Source {
match self.peer {
true => Source::Peer(self.via),
false => Source::Local,
}
}
fn serves(&self, path: &Path) -> bool {
self.server.is_some() || (self.source.is_some() && self.prefix == *path)
}
fn qualifies(&self, pin: Pin) -> bool {
match pin {
Pin::Any => true,
Pin::Local => self.local,
Pin::Publisher(first) => self.hops.iter().next() == Some(&first),
Pin::Route(id) => self.id == id,
}
}
fn visible_to(&self, exclude: Option<Hop>) -> bool {
match exclude {
Some(peer) if peer != Hop::UNKNOWN => self.via != peer && !self.hops.contains(&peer),
_ => true,
}
}
fn overlaps(&self, allowed: &Patterns) -> bool {
self.scope.iter().any(|scope| {
scope
.intersect(&self.claim)
.is_ok_and(|scoped| scoped.iter().any(|restriction| allowed.overlaps(restriction)))
})
}
}
fn prefix_claim(prefix: &Path) -> Result<Pattern, InvalidPattern> {
if prefix.parts().count() == Path::MAX_PARTS {
Pattern::literal(prefix.as_str())
} else {
Pattern::subtree(prefix.as_str())
}
}
#[derive(Default)]
struct ServeState {
requests: Requests<PathOwned, kio::Producer<PendingBroadcast>>,
served: WeakCache<PathOwned, broadcast::WeakConsumer>,
closed: bool,
}
type FrontKey = (PathOwned, Horizon);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
struct Horizon {
exclude: Option<Hop>,
local: bool,
}
impl Horizon {
fn admits(&self, entry: &RouteEntry) -> bool {
!(self.local && entry.peer) && entry.visible_to(self.exclude)
}
}
#[derive(Clone)]
struct RemoteFront {
request: kio::Producer<PendingBroadcast>,
broadcast: broadcast::WeakConsumer,
pin: kio::Lock<Pin>,
}
type CursorRoute = (u64, RouteMeta, bool, Option<Vec<Pattern>>);
impl WeakEntry for RemoteFront {
fn is_closed(&self) -> bool {
self.broadcast.is_closed()
}
fn same_channel(&self, other: &Self) -> bool {
self.broadcast.same_channel(&other.broadcast)
}
}
struct TableCursor {
root: PathOwned,
allowed: Patterns,
heads: Vec<PathOwned>,
horizon: Horizon,
hidden: Hidden,
state: kio::Producer<OriginConsumerState>,
current: HashMap<PathOwned, CursorRoute>,
}
impl TableCursor {
fn presented(&self, prefix: &Path, claim: &Pattern) -> Option<PathOwned> {
if !self.allowed.overlaps(claim) {
return None;
}
if let Some(relative) = prefix.strip_prefix(&self.root) {
return Some(relative.to_owned());
}
self.root.has_prefix(prefix).then(PathOwned::default)
}
fn captures(&self, prefix: &Path) -> Option<Vec<Pattern>> {
let literal = Pattern::literal(prefix.as_str()).ok()?;
self.allowed
.iter()
.filter_map(|allowed| {
allowed
.captures(&literal)
.map(|captures| (allowed.specificity(), captures))
})
.max_by_key(|(specificity, _)| *specificity)
.map(|(_, captures)| captures)
}
fn visible(&self, entry: &RouteEntry) -> bool {
entry.advertised && self.horizon.admits(entry) && entry.overlaps(&self.allowed) && self.discovers(&entry.prefix)
}
fn discovers(&self, prefix: &Path) -> bool {
(self.hidden.include || !hides(&self.heads, prefix))
&& self.hidden.beyond.as_ref().is_none_or(|outer| hides(outer, prefix))
}
}
#[derive(Clone)]
struct OriginScope {
allowed: Patterns,
}
impl OriginScope {
fn empty() -> Self {
Self {
allowed: Patterns::new(),
}
}
fn narrow(&self, patterns: &Patterns) -> Option<Self> {
let allowed = self.allowed.intersect(patterns).ok()?;
if allowed.is_empty() {
None
} else {
Some(Self { allowed })
}
}
fn permits(&self, path: &Path) -> bool {
self.allowed.matches(path.as_str())
}
fn relative(&self, root: &Path) -> Patterns {
self.allowed.rebase(root.as_str())
}
}
impl Default for OriginScope {
fn default() -> Self {
Self {
allowed: Patterns::from(Pattern::all()),
}
}
}
pub(crate) fn interest_prefixes(allowed: &Patterns) -> Vec<PathOwned> {
let mut heads: Vec<PathOwned> = allowed
.iter()
.map(|pattern| Path::new(pattern.head()).to_owned())
.collect();
heads.sort();
heads.dedup();
let covered = heads.clone();
heads.retain(|head| !covered.iter().any(|other| other != head && head.has_prefix(other)));
heads
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Hidden {
include: bool,
beyond: Option<Vec<PathOwned>>,
}
fn hides(heads: &[PathOwned], prefix: &Path) -> bool {
heads
.iter()
.any(|head| prefix.strip_prefix(head).is_some_and(|below| below.is_hidden()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnnounceKind {
Announced,
Updated,
Retracted,
}
impl AnnounceKind {
pub fn is_active(self) -> bool {
!matches!(self, Self::Retracted)
}
}
#[derive(Clone, Debug)]
pub struct AnnounceUpdate {
pub prefix: PathOwned,
pub captures: Option<Vec<Pattern>>,
pub route: Route,
pub kind: AnnounceKind,
}
#[derive(Clone)]
pub struct Producer {
hop: Hop,
scope: OriginScope,
root: PathOwned,
shared: kio::Shared<OriginState>,
pool: cache::Pool,
cache_duration: Duration,
default_max_age: Duration,
stats: stats::Session,
peer: bool,
tasks: Tasks,
timers: Clock,
}
impl Producer {
pub fn new(config: Config) -> (Self, Driver) {
let (tasks, set) = TaskSet::new();
let scope = OriginScope::default();
let shared = kio::Shared::<OriginState>::default();
let timers = Clock::default();
let pool = config.pool.clone();
let producer = Self {
hop: config.hop,
scope: scope.clone(),
root: PathOwned::default(),
shared: shared.clone(),
pool: config.pool,
cache_duration: config.cache_duration,
default_max_age: config.default_max_age,
stats: stats::Session::default(),
peer: false,
tasks,
timers: timers.clone(),
};
let driver = Driver {
state: DriverState {
set,
shared,
done: false,
},
timers,
pool,
};
(producer, driver)
}
pub fn with_stats(mut self, session: stats::Session) -> Self {
self.stats = session;
self
}
pub fn peer(mut self) -> Self {
self.peer = true;
self
}
pub fn config(&self) -> Config {
Config {
hop: self.hop,
pool: self.pool.clone(),
cache_duration: self.cache_duration,
default_max_age: self.default_max_age,
}
}
pub fn hop(&self) -> Hop {
self.hop
}
pub(crate) fn default_max_age(&self) -> Duration {
self.default_max_age
}
pub(crate) fn empty(hop: Hop) -> Self {
let (tasks, _) = TaskSet::new();
Self {
hop,
scope: OriginScope::empty(),
root: PathOwned::default(),
shared: kio::Shared::default(),
pool: cache::Pool::default(),
cache_duration: Duration::MAX,
default_max_age: track::DEFAULT_MAX_AGE,
stats: stats::Session::default(),
peer: false,
tasks,
timers: Clock::default(),
}
}
pub fn create_broadcast(&self, path: impl AsPath) -> Result<broadcast::Producer, Error> {
let path = path.as_path();
let full = self.root.join(&path).to_owned();
if !self.scope.permits(&full) {
return Err(Error::Unauthorized);
}
if full.parts().count() > Path::MAX_PARTS {
return Err(BoundsExceeded.into());
}
let claim = prefix_claim(&full)?;
let ingress = self.stats.ingress(&full);
let announcing = Announcing {
hop: self.hop,
shared: self.shared.clone(),
requested: full.clone(),
prefixes: vec![(full.clone(), claim)],
scope: self.scope.allowed.clone(),
local: true,
peer: self.peer,
stats: self.stats.clone(),
};
let info = broadcast::Info {
pool: self.pool.clone(),
cache_duration: self.cache_duration,
path: full,
};
let source = info.produce().with_stats(ingress.clone());
let entry = announcing.announce(
Route::default(),
Serving {
server: None,
source: Some(source.consume()),
advertised: false,
},
)?;
Ok(source.with_announcer(Announcer {
entry,
ingress,
_keepalive: self.tasks.keepalive(),
}))
}
pub fn publish(&self, path: impl AsPath, route: Route) -> Result<broadcast::Producer, Error> {
let broadcast = self.create_broadcast(path)?;
broadcast.announce(route)?;
Ok(broadcast)
}
pub(crate) fn create_source(&self, path: impl AsPath) -> broadcast::Producer {
let path = path.as_path();
let full = self.root.join(&path).to_owned();
let ingress = self.stats.ingress(&full);
broadcast::Info {
pool: self.pool.clone(),
cache_duration: self.cache_duration,
path: full,
}
.produce()
.with_stats(ingress)
}
#[cfg(test)]
pub(crate) fn announce(&self, prefix: impl AsPath, route: Route) -> Result<AnnounceProducer, Error> {
Announcing::new(self, prefix)?.announce(
route,
Serving {
server: None,
source: None,
advertised: true,
},
)
}
pub fn dynamic(&self, prefix: impl AsPath, route: Route) -> Result<Dynamic, Error> {
let announcing = Announcing::new(self, prefix)?;
let serve = kio::Shared::<ServeState>::default();
serve.lock().requests.add_handler();
let announcement = announcing.announce(
route,
Serving {
server: Some(serve.clone()),
source: None,
advertised: true,
},
)?;
Ok(Dynamic {
announcement,
state: serve,
})
}
pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result<Producer, Error> {
let root = self.root.join(root).to_owned();
let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?;
let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?;
Ok(Producer {
hop: self.hop,
scope,
root,
shared: self.shared.clone(),
pool: self.pool.clone(),
cache_duration: self.cache_duration,
default_max_age: self.default_max_age,
stats: self.stats.clone(),
peer: self.peer,
tasks: self.tasks.clone(),
timers: self.timers.clone(),
})
}
pub fn consume(&self) -> Consumer {
Consumer::from_producer(self, stats::Session::default())
}
pub fn root(&self) -> &Path<'_> {
&self.root
}
pub fn allowed(&self) -> Patterns {
self.scope.relative(&self.root)
}
pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
self.root.join(path)
}
}
struct Announcing {
hop: Hop,
shared: kio::Shared<OriginState>,
requested: PathOwned,
prefixes: Vec<(PathOwned, Pattern)>,
scope: Patterns,
local: bool,
peer: bool,
stats: stats::Session,
}
impl Announcing {
fn new(producer: &Producer, prefix: impl AsPath) -> Result<Self, Error> {
let requested = producer.root.join(prefix.as_path()).to_owned();
if requested.parts().count() > Path::MAX_PARTS {
return Err(BoundsExceeded.into());
}
let claim = prefix_claim(&requested)?;
if !producer.scope.allowed.overlaps(&claim) {
return Err(Error::Unauthorized);
}
Ok(Self {
hop: producer.hop,
shared: producer.shared.clone(),
requested: requested.clone(),
prefixes: vec![(requested, claim)],
scope: producer.scope.allowed.clone(),
local: false,
peer: producer.peer,
stats: producer.stats.clone(),
})
}
fn announce(&self, route: Route, serving: Serving) -> Result<AnnounceProducer, Error> {
debug_assert!(
!route.hops.contains(&self.hop),
"announce called with a looping hop chain",
);
let via = route.via;
let mut shared = self.shared.lock();
if shared.closed {
return Err(Error::Closed);
}
let mut entries = Vec::with_capacity(self.prefixes.len());
for (prefix, claim) in &self.prefixes {
let id = shared.next_route;
shared.next_route += 1;
shared.routes.insert(RouteEntry {
id,
prefix: prefix.clone(),
scope: self.scope.clone(),
hops: route.hops.clone(),
cost: route.cost,
via,
local: self.local,
peer: self.peer,
server: serving.server.clone(),
source: serving.source.clone(),
advertised: serving.advertised,
claim: claim.clone(),
});
shared.sync_route(prefix, claim);
entries.push((prefix.clone(), id));
}
drop(shared);
let guard = serving
.advertised
.then(|| self.stats.ingress(&self.requested).announce());
Ok(AnnounceProducer {
shared: self.shared.clone(),
entries,
guard,
})
}
}
struct Serving {
server: Option<kio::Shared<ServeState>>,
source: Option<broadcast::Consumer>,
advertised: bool,
}
pub(crate) struct Announcer {
entry: AnnounceProducer,
ingress: stats::Scope,
_keepalive: Keepalive,
}
impl Announcer {
pub(crate) fn announce(&mut self, route: Route) -> Result<(), Error> {
self.entry.update(route)?;
if self.entry.guard.is_none() {
self.entry.guard = Some(self.ingress.announce());
}
Ok(())
}
pub(crate) fn withdraw(&mut self) {
self.entry.withdraw();
self.entry.guard = None;
}
}
#[must_use = "dropping an announcement retracts the route"]
pub(crate) struct AnnounceProducer {
shared: kio::Shared<OriginState>,
entries: Vec<(PathOwned, u64)>,
guard: Option<stats::Announce>,
}
impl AnnounceProducer {
pub fn update(&self, route: Route) -> Result<(), Error> {
let mut shared = self.shared.lock();
if shared.closed {
return Err(Error::Closed);
}
for (prefix, id) in &self.entries {
let Some(entry) = shared.routes.entry_mut(prefix, *id) else {
return Err(Error::Closed);
};
entry.hops = route.hops.clone();
entry.cost = route.cost;
entry.via = route.via;
entry.advertised = true;
let claim = entry.claim.clone();
shared.sync_route(prefix, &claim);
}
Ok(())
}
fn withdraw(&self) {
let mut shared = self.shared.lock();
for (prefix, id) in &self.entries {
let Some(entry) = shared.routes.entry_mut(prefix, *id) else {
continue;
};
if !entry.advertised {
continue;
}
entry.advertised = false;
let claim = entry.claim.clone();
shared.sync_route(prefix, &claim);
}
}
fn retract(&self) {
let mut shared = self.shared.lock();
for (prefix, id) in &self.entries {
let Some(entry) = shared.routes.remove(prefix, *id) else {
continue;
};
if let Some(server) = &entry.server {
let mut server = server.lock();
server.closed = true;
for producer in server.requests.drain_all() {
if let Ok(mut request) = producer.write() {
request.resolved.get_or_insert(Err(Error::Unroutable));
}
}
}
shared.sync_route(&entry.prefix, &entry.claim);
}
}
}
impl Drop for AnnounceProducer {
fn drop(&mut self) {
self.retract();
}
}
#[must_use = "poll the driver or the origin makes no progress"]
pub struct Driver {
state: DriverState,
timers: Clock,
pool: cache::Pool,
}
struct DriverState {
set: TaskSet,
shared: kio::Shared<OriginState>,
done: bool,
}
impl Driver {
pub fn poll(&mut self, now: Instant, waiter: &kio::Waiter) -> Result<Option<Instant>, Error> {
self.timers.advance(now);
let result = self.state.poll(waiter);
let gc = self.pool.gc(now);
if result.is_ready() {
return Err(Error::Closed);
}
Ok(self.timers.timeout().into_iter().chain(gc).min())
}
}
impl crate::time::Driver for Driver {
fn poll(&mut self, now: Instant, waiter: &kio::Waiter) -> Result<Option<Instant>, Error> {
self.poll(now, waiter)
}
}
impl DriverState {
fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
if !self.done {
ready!(self.set.poll(waiter));
self.done = true;
}
Poll::Ready(())
}
fn teardown(&mut self) {
drop(std::mem::replace(&mut self.set, TaskSet::owned()));
let (servers, cursors, fronts) = {
let mut shared = self.shared.lock();
shared.closed = true;
shared.routes.poke_all();
let servers: Vec<_> = shared
.routes
.entries()
.filter_map(|entry| entry.server.clone())
.collect();
let cursors: Vec<_> = shared.cursors.values().map(|cursor| cursor.state.clone()).collect();
let fronts: Vec<_> = shared.fronts.values().map(|front| front.request.clone()).collect();
(servers, cursors, fronts)
};
for producer in fronts {
if let Ok(mut request) = producer.write() {
request.resolved.get_or_insert(Err(Error::Dropped));
}
}
for server in servers {
let mut server = server.lock();
server.closed = true;
for producer in server.requests.drain_all() {
if let Ok(mut request) = producer.write() {
request.resolved.get_or_insert(Err(Error::Dropped));
}
}
}
for state in cursors {
if let Ok(mut state) = state.write() {
state.ended = true;
}
}
}
}
impl Drop for DriverState {
fn drop(&mut self) {
self.teardown();
}
}
const TRACK_IDLE_LINGER: Duration = Duration::from_secs(30);
struct WarmCopy {
track: track::Producer,
_dynamic: track::Dynamic,
}
impl Drop for WarmCopy {
fn drop(&mut self) {
let _ = self.track.finish();
}
}
fn warm_copy(source: &track::Consumer) -> Option<WarmCopy> {
let info = source.cached_info()?;
let mut track = track::Producer::new(Arc::new(source.broadcast().clone()), source.name(), info);
for (group, visible) in source.cached_groups() {
if group.is_finished() {
let _ = track.adopt_group(group, visible);
}
}
let dynamic = track.dynamic();
Some(WarmCopy {
track,
_dynamic: dynamic,
})
}
struct FrontTask {
shared: kio::Shared<OriginState>,
broadcast: broadcast::Producer,
path: PathOwned,
horizon: Horizon,
watch: Watch,
request: kio::Producer<PendingBroadcast>,
pin: kio::Lock<Pin>,
timers: Clock,
}
struct TrackIo {
resume: super::resume::Producer,
staged: Option<(u64, track::Consumer)>,
query: Option<(u64, track::Consumer, track::Querying)>,
copy: Option<(u64, track::Consumer)>,
edge: Option<track::Position>,
warm: Option<WarmCopy>,
used: bool,
}
async fn run_front(task: FrontTask) {
let FrontTask {
shared,
broadcast,
path,
horizon,
watch,
request,
pin,
timers,
} = task;
enum Step {
Assigned(Arc<str>, super::resume::Producer),
Resolved(u64, Result<broadcast::Consumer, Error>),
SourceClosed(u64),
Info(Arc<str>, u64, Result<track::Info, Error>),
Ended(Arc<str>, u64, Result<(), Error>),
Demand(Arc<str>),
Deadline,
Table,
}
let mut front = Front::new(TRACK_IDLE_LINGER);
let mut sources: HashMap<u64, broadcast::Consumer> = HashMap::new();
let mut next_source = 0u64;
let mut upstream: Option<(u64, kio::Consumer<PendingBroadcast>)> = None;
let mut tracks: HashMap<Arc<str>, TrackIo> = HashMap::new();
let mut deadline = crate::runtime::Deadline::new(&timers);
let mut seen = 0;
let mut events: VecDeque<Event> = VecDeque::new();
let select = |front: &mut Front, sources: &HashMap<u64, broadcast::Consumer>, seen: &mut u64| -> Event {
let table = shared.read();
if table.closed {
return Event::Closed;
}
*seen = watch.seen();
front.retain_routes(|route| table.routes.covers(&path.as_path(), route));
let best = table
.best_route(&path.as_path(), horizon, front.pin(), front.refused_routes())
.map(|entry| Candidate {
route: entry.id,
first: entry.hops.iter().next().copied(),
local: entry.local,
});
let serving_closing = front
.serving()
.and_then(|id| sources.get(&id))
.is_some_and(|source| source.is_closing());
Event::Selected { best, serving_closing }
};
events.push_back(select(&mut front, &sources, &mut seen));
loop {
while let Some(event) = events.pop_front() {
for action in front.step(event) {
match action {
Action::Reselect => events.push_back(select(&mut front, &sources, &mut seen)),
Action::Request { route } => {
let found = {
let table = shared.read();
table
.routes
.covering(&path.as_path())
.find(|entry| entry.id == route)
.map(|entry| {
(
Candidate {
route,
first: entry.hops.iter().next().copied(),
local: entry.local,
},
entry.source.clone(),
entry.server.clone(),
)
})
};
let Some((candidate, source, server)) = found else {
events.push_back(Event::Resolved {
route,
result: Err(Refusal {
err: Error::Unroutable,
standing: false,
}),
});
continue;
};
front.identify(candidate);
*pin.lock() = front.pin();
if let Some(source) = source {
let id = next_source;
next_source += 1;
sources.insert(id, source);
events.push_back(Event::Resolved { route, result: Ok(id) });
continue;
}
let Some(server) = server else {
events.push_back(Event::Resolved {
route,
result: Err(Refusal {
err: Error::Unroutable,
standing: true,
}),
});
continue;
};
let mut serve = server.lock();
if serve.closed {
drop(serve);
events.push_back(Event::Resolved {
route,
result: Err(Refusal {
err: Error::Unroutable,
standing: true,
}),
});
continue;
}
if let Some(weak) = serve.served.get(&path) {
drop(serve);
let id = next_source;
next_source += 1;
sources.insert(id, weak.consume());
events.push_back(Event::Resolved { route, result: Ok(id) });
continue;
}
let pending = match serve.requests.join(&path) {
Some(producer) => producer.consume(),
None => {
let producer = kio::Producer::<PendingBroadcast>::default();
let consumer = producer.consume();
match serve.requests.insert(path.clone(), producer) {
Ok(()) => consumer,
Err(_) => {
drop(serve);
events.push_back(Event::Resolved {
route,
result: Err(Refusal {
err: Error::Unroutable,
standing: true,
}),
});
continue;
}
}
}
};
upstream = Some((route, pending));
}
Action::Detach { source } => {
sources.remove(&source);
for io in tracks.values_mut() {
if io.copy.as_ref().is_some_and(|(s, _)| *s == source) {
io.copy = None;
}
if io.query.as_ref().is_some_and(|(s, ..)| *s == source) {
io.query = None;
}
if io.staged.as_ref().is_some_and(|(s, _)| *s == source) {
io.staged = None;
}
}
}
Action::Resolve => {
if let Ok(mut pending) = request.write() {
pending.resolved.get_or_insert(Ok(broadcast.consume()));
}
}
Action::Query { track: name, source } => {
let Some(io) = tracks.get_mut(&name) else { continue };
let closing = sources.get(&source).is_some_and(|s| s.is_closing());
match sources.get(&source).map(|s| s.track(&name)) {
Some(Ok(copy)) => {
let query = copy.query().into_inner();
io.query = Some((source, copy, query));
}
Some(Err(err)) => events.push_back(Event::TrackInfo {
track: name,
source,
closing,
result: Err(err),
}),
None => {}
}
}
Action::Splice { track: name, source } => {
let Some(io) = tracks.get_mut(&name) else { continue };
let Some((staged, copy)) = io.staged.take() else {
continue;
};
if staged != source {
continue;
}
if let Err(err) = io.resume.takeover(©) {
let _ = io.resume.abort(err);
tracks.remove(&name);
continue;
}
io.warm = None;
io.edge = io.resume.resume_position();
io.copy = Some((source, copy));
}
Action::Park { track: name } => {
let Some(io) = tracks.get_mut(&name) else { continue };
let Some((_, copy)) = io.copy.take() else { continue };
let warm = warm_copy(©);
drop(copy);
if io.resume.release().is_err() {
tracks.remove(&name);
continue;
}
if let Some(warm) = warm {
if let Err(err) = io.resume.takeover(&warm.track) {
let _ = io.resume.abort(err);
tracks.remove(&name);
continue;
}
io.warm = Some(warm);
}
}
Action::Release { track: name } => {
let Some(io) = tracks.get_mut(&name) else { continue };
io.warm = None;
if io.resume.release().is_err() {
tracks.remove(&name);
}
}
Action::Finish { track: name } => {
if let Some(mut io) = tracks.remove(&name) {
let _ = io.resume.finish();
}
}
Action::Abort { track: name, err } => {
if let Some(mut io) = tracks.remove(&name) {
tracing::debug!(name = %name, %err, "aborting track");
let _ = io.resume.abort(err);
}
}
Action::Arm { at } => deadline.set(at),
Action::End { err } => {
if let Ok(mut pending) = request.write() {
pending.resolved.get_or_insert(Err(err.clone()));
}
broadcast.finish();
broadcast.release_spliced(err.clone());
for (_, mut io) in tracks.drain() {
let waiting = io.staged.take().map(|(_, copy)| copy);
let waiting = waiting.or_else(|| io.query.take().map(|(_, copy, _)| copy));
if let Some(copy) = waiting
&& io.resume.is_used()
{
if io.resume.takeover(©).is_err() {
continue;
}
io.warm = None;
}
if !io.resume.is_used() || !io.resume.is_spliced() || io.warm.is_some() {
let _ = io.resume.abort(err.clone());
}
}
return;
}
}
}
}
let step = kio::wait(|waiter| {
if let Poll::Ready((name, resume)) = broadcast.poll_spliced_assigned(waiter) {
return Poll::Ready(Step::Assigned(name, resume));
}
if let Some((route, pending)) = &upstream
&& let Poll::Ready(result) = pending.poll(waiter, |p| match &p.resolved {
Some(result) => Poll::Ready(result.clone()),
None => Poll::Pending,
}) {
return Poll::Ready(Step::Resolved(
*route,
match result {
Ok(resolved) => resolved,
Err(_closed) => Err(Error::Unroutable),
},
));
}
if let Some(id) = front.serving()
&& let Some(source) = sources.get(&id)
&& source.poll_closed(waiter).is_ready()
{
return Poll::Ready(Step::SourceClosed(id));
}
for (name, io) in &tracks {
if let Some((source, _, query)) = &io.query
&& let Poll::Ready(result) = query.poll(waiter)
{
return Poll::Ready(Step::Info(name.clone(), *source, result));
}
if let Some((source, copy)) = &io.copy
&& let Poll::Ready(result) = copy.poll_complete(waiter)
{
return Poll::Ready(Step::Ended(name.clone(), *source, result));
}
let edge = match io.used {
true => io.resume.poll_unused(waiter),
false => io.resume.poll_used(waiter),
};
if edge.is_ready() {
return Poll::Ready(Step::Demand(name.clone()));
}
}
if deadline.poll(waiter).is_ready() {
return Poll::Ready(Step::Deadline);
}
watch.poll_changed(waiter, seen).map(|()| Step::Table)
})
.await;
let event = match step {
Step::Assigned(name, resume) => {
tracks.insert(
name.clone(),
TrackIo {
resume,
staged: None,
query: None,
copy: None,
edge: None,
warm: None,
used: false,
},
);
Event::TrackAssigned { track: name }
}
Step::Resolved(route, result) => {
upstream = None;
match result {
Ok(source) => {
let id = next_source;
next_source += 1;
sources.insert(id, source);
Event::Resolved { route, result: Ok(id) }
}
Err(err) => {
let standing =
!matches!(err, Error::Unroutable) || shared.read().routes.covers(&path.as_path(), route);
Event::Resolved {
route,
result: Err(Refusal { err, standing }),
}
}
}
}
Step::SourceClosed(source) => Event::SourceClosed { source },
Step::Info(name, source, result) => {
let closing = sources.get(&source).is_some_and(|s| s.is_closing());
let Some(io) = tracks.get_mut(&name) else { continue };
let Some((_, copy, _)) = io.query.take() else { continue };
let result = match result {
Ok(info) => match copy.poll_complete(&kio::Waiter::noop()) {
Poll::Ready(Err(err)) => Err(err),
_ => Ok(info),
},
Err(err) => Err(err),
};
if result.is_ok() && io.used {
io.staged = Some((source, copy));
}
Event::TrackInfo {
track: name,
source,
closing,
result,
}
}
Step::Ended(name, source, result) => {
let closing = sources.get(&source).is_some_and(|s| s.is_closing());
let Some(io) = tracks.get_mut(&name) else { continue };
io.copy = None;
let delivered = io.resume.resume_position() != io.edge;
Event::TrackEnded {
track: name,
source,
closing,
result,
delivered,
}
}
Step::Demand(name) => {
let Some(io) = tracks.get_mut(&name) else { continue };
io.used = io.resume.is_used();
if !io.used {
io.query = None;
io.staged = None;
}
match io.used {
true => Event::Used { track: name },
false => Event::Unused {
track: name,
now: timers.now(),
},
}
}
Step::Deadline => {
deadline.set(None);
Event::Deadline { now: timers.now() }
}
Step::Table => select(&mut front, &sources, &mut seen),
};
events.push_back(event);
}
}
#[derive(Default)]
struct RouteTable {
root: RouteNode,
}
#[derive(Default)]
struct RouteNode {
entries: Vec<RouteEntry>,
cursors: Vec<ConsumerId>,
cursors_below: usize,
watches: Vec<(u64, kio::Producer<Watched>)>,
watches_below: usize,
children: HashMap<String, RouteNode>,
}
#[derive(Default)]
struct Watched {
generation: u64,
}
struct Watch {
shared: kio::Shared<OriginState>,
path: PathOwned,
id: u64,
signal: kio::Consumer<Watched>,
}
impl Watch {
fn seen(&self) -> u64 {
self.signal.read().generation
}
fn poll_changed(&self, waiter: &kio::Waiter, seen: u64) -> Poll<()> {
self.signal
.poll(waiter, |watched| match watched.generation != seen {
true => Poll::Ready(()),
false => Poll::Pending,
})
.map(|_| ())
}
}
impl Drop for Watch {
fn drop(&mut self) {
self.shared.lock().routes.remove_watch(&self.path, self.id);
}
}
#[derive(Clone, Copy)]
struct Below {
cursors: usize,
watches: usize,
}
impl Below {
const NONE: Self = Self { cursors: 0, watches: 0 };
const CURSOR: Self = Self { cursors: 1, watches: 0 };
const WATCH: Self = Self { cursors: 0, watches: 1 };
}
impl RouteNode {
fn is_empty(&self) -> bool {
self.entries.is_empty() && self.cursors.is_empty() && self.watches.is_empty() && self.children.is_empty()
}
fn find<'a>(&self, mut parts: impl Iterator<Item = &'a str>) -> Option<&Self> {
match parts.next() {
None => Some(self),
Some(part) => self.children.get(part)?.find(parts),
}
}
fn reach<'a>(&mut self, mut parts: impl Iterator<Item = &'a str>, below: Below) -> &mut Self {
self.cursors_below += below.cursors;
self.watches_below += below.watches;
match parts.next() {
None => self,
Some(part) => self.children.entry(part.to_string()).or_default().reach(parts, below),
}
}
fn edit<'a, R>(
&mut self,
mut parts: impl Iterator<Item = &'a str>,
below: Below,
f: impl FnOnce(&mut Self) -> R,
) -> Option<R> {
let result = match parts.next() {
None => f(self),
Some(part) => {
let child = self.children.get_mut(part)?;
let result = child.edit(parts, below, f)?;
if child.is_empty() {
self.children.remove(part);
}
result
}
};
self.cursors_below -= below.cursors;
self.watches_below -= below.watches;
Some(result)
}
fn poke(&self) {
for (_, watch) in &self.watches {
if let Ok(mut watched) = watch.write() {
watched.generation += 1;
}
}
}
fn poke_below(&self) {
if self.watches_below == 0 {
return;
}
self.poke();
for child in self.children.values() {
child.poke_below();
}
}
fn walk<'a>(&'a self, visit: &mut impl FnMut(&'a Self)) {
visit(self);
for child in self.children.values() {
child.walk(visit);
}
}
fn collect_cursors(&self, out: &mut Vec<ConsumerId>) {
if self.cursors_below == 0 {
return;
}
out.extend(&self.cursors);
for child in self.children.values() {
child.collect_cursors(out);
}
}
}
impl RouteTable {
fn split(&self, path: &Path) -> (Vec<&RouteNode>, Option<&RouteNode>) {
let mut above = Vec::new();
let mut node = &self.root;
for part in path.parts() {
above.push(node);
match node.children.get(part) {
Some(child) => node = child,
None => return (above, None),
}
}
(above, Some(node))
}
fn covering(&self, path: &Path) -> impl Iterator<Item = &RouteEntry> {
let (above, at) = self.split(path);
above.into_iter().chain(at).flat_map(|node| node.entries.iter())
}
fn covers(&self, path: &Path, id: u64) -> bool {
self.covering(path).any(|entry| entry.id == id)
}
fn at(&self, prefix: &Path) -> impl Iterator<Item = &RouteEntry> {
self.root
.find(prefix.parts())
.into_iter()
.flat_map(|node| node.entries.iter())
}
fn entries(&self) -> impl Iterator<Item = &RouteEntry> {
let mut nodes = Vec::new();
self.root.walk(&mut |node| nodes.push(node));
nodes.into_iter().flat_map(|node| node.entries.iter())
}
fn insert(&mut self, entry: RouteEntry) {
let node = self.root.reach(entry.prefix.parts(), Below::NONE);
node.entries.push(entry);
}
fn entry_mut(&mut self, prefix: &Path, id: u64) -> Option<&mut RouteEntry> {
let mut node = &mut self.root;
for part in prefix.parts() {
node = node.children.get_mut(part)?;
}
node.entries.iter_mut().find(|entry| entry.id == id)
}
fn remove(&mut self, prefix: &Path, id: u64) -> Option<RouteEntry> {
self.root
.edit(prefix.parts(), Below::NONE, |node| {
let index = node.entries.iter().position(|entry| entry.id == id)?;
Some(node.entries.swap_remove(index))
})
.flatten()
}
fn add_cursor(&mut self, head: &Path, id: ConsumerId) {
self.root.reach(head.parts(), Below::CURSOR).cursors.push(id);
}
fn remove_cursor(&mut self, head: &Path, id: ConsumerId) {
self.root.edit(head.parts(), Below::CURSOR, |node| {
node.cursors.retain(|cursor| *cursor != id)
});
}
fn add_watch(&mut self, path: &Path, id: u64) -> kio::Consumer<Watched> {
let producer = kio::Producer::<Watched>::default();
let consumer = producer.consume();
self.root.reach(path.parts(), Below::WATCH).watches.push((id, producer));
consumer
}
fn remove_watch(&mut self, path: &Path, id: u64) {
self.root.edit(path.parts(), Below::WATCH, |node| {
node.watches.retain(|(watch, _)| *watch != id)
});
}
fn poke_below(&self, prefix: &Path) {
if let (_, Some(node)) = self.split(prefix) {
node.poke_below();
}
}
fn poke_all(&self) {
self.root.walk(&mut |node| node.poke());
}
fn cursors_touching(&self, prefix: &Path) -> Vec<ConsumerId> {
let (above, at) = self.split(prefix);
let mut cursors: Vec<ConsumerId> = above.iter().flat_map(|node| node.cursors.iter().copied()).collect();
if let Some(node) = at {
node.collect_cursors(&mut cursors);
}
cursors.sort_unstable();
cursors.dedup();
cursors
}
}
#[derive(Default)]
struct OriginState {
routes: RouteTable,
next_route: u64,
next_watch: u64,
cursors: HashMap<ConsumerId, TableCursor>,
fronts: WeakCache<FrontKey, RemoteFront>,
closed: bool,
}
impl OriginState {
fn sync_route(&mut self, prefix: &Path, claim: &Pattern) {
let routes = &self.routes;
for id in routes.cursors_touching(prefix) {
let Some(cursor) = self.cursors.get_mut(&id) else {
continue;
};
if let Some(presented) = cursor.presented(prefix, claim) {
Self::sync_cursor(routes, cursor, &presented);
}
}
routes.poke_below(prefix);
}
fn watch(&mut self, shared: &kio::Shared<OriginState>, path: &Path) -> Watch {
let id = self.next_watch;
self.next_watch += 1;
let signal = self.routes.add_watch(path, id);
Watch {
shared: shared.clone(),
path: path.to_owned(),
id,
signal,
}
}
fn sync_cursor(routes: &RouteTable, cursor: &mut TableCursor, presented: &PathOwned) {
let candidates: Vec<&RouteEntry> = match presented.is_empty() {
true => routes
.covering(&cursor.root)
.filter(|entry| cursor.visible(entry))
.collect(),
false => {
let absolute = cursor.root.join(presented);
routes.at(&absolute).filter(|entry| cursor.visible(entry)).collect()
}
};
let most = candidates.iter().map(|entry| entry.prefix.len()).max();
let best = most.and_then(|most| {
candidates
.into_iter()
.filter(|entry| entry.prefix.len() == most)
.min_by_key(|entry| route_order(&entry.prefix, entry))
});
match best {
Some(entry) => {
let meta = (entry.hops.clone(), entry.cost, entry.entered());
let served = entry.server.is_some();
let captures = cursor.captures(&entry.prefix);
let previous = cursor
.current
.insert(presented.clone(), (entry.id, meta.clone(), served, captures.clone()));
match previous {
Some((_, prev, prev_served, prev_captures))
if prev == meta && prev_served == served && prev_captures == captures => {}
Some((_, prev, _, prev_captures)) if prev_captures != captures => {
if let Ok(mut state) = cursor.state.write() {
state.apply_unannounce(presented.clone(), prev, prev_captures);
state.apply_announce(presented.clone(), meta, captures);
}
}
_ => {
if let Ok(mut state) = cursor.state.write() {
state.apply_announce(presented.clone(), meta, captures);
}
}
}
}
None => {
if let Some((_, last, _, captures)) = cursor.current.remove(presented)
&& let Ok(mut state) = cursor.state.write()
{
state.apply_unannounce(presented.clone(), last, captures);
}
}
}
}
fn register_cursor(&mut self, id: ConsumerId, mut cursor: TableCursor) {
let mut presented: BTreeSet<PathOwned> = BTreeSet::new();
for head in &cursor.heads {
let (above, at) = self.routes.split(head);
let mut nodes = above;
if let Some(node) = at {
node.walk(&mut |node| nodes.push(node));
}
for entry in nodes.into_iter().flat_map(|node| node.entries.iter()) {
if let Some(p) = cursor.presented(&entry.prefix, &entry.claim) {
presented.insert(p);
}
}
}
for p in &presented {
Self::sync_cursor(&self.routes, &mut cursor, p);
}
for head in &cursor.heads {
self.routes.add_cursor(head, id);
}
self.cursors.insert(id, cursor);
}
fn best_route(&self, path: &Path, horizon: Horizon, pin: Pin, refused: &HashSet<u64>) -> Option<&RouteEntry> {
let (above, at) = self.routes.split(path);
let mut best = None;
for node in above.into_iter().chain(at) {
let mut candidates = node
.entries
.iter()
.filter(|entry| entry.advertised)
.filter(|entry| entry.scope.matches(path.as_str()))
.filter(|entry| horizon.admits(entry))
.filter(|entry| entry.qualifies(pin))
.filter(|entry| !refused.contains(&entry.id))
.peekable();
if candidates.peek().is_some() {
best = candidates
.filter(|entry| entry.serves(path))
.min_by_key(|entry| route_order(&entry.prefix, entry));
}
}
best
}
}
#[derive(Default)]
struct PendingBroadcast {
resolved: Option<Result<broadcast::Consumer, Error>>,
}
#[must_use = "dropping an origin::Dynamic retracts the route"]
pub struct Dynamic {
announcement: AnnounceProducer,
state: kio::Shared<ServeState>,
}
impl Dynamic {
pub fn update(&self, route: Route) -> Result<(), Error> {
self.announcement.update(route)
}
pub fn poll_requested_broadcast(&self, waiter: &kio::Waiter) -> Poll<Result<Request, Error>> {
let mut state = ready!(self.state.poll(waiter, |state| {
if state.closed || state.requests.has_queued() {
Poll::Ready(())
} else {
Poll::Pending
}
}));
if state.closed {
return Poll::Ready(Err(Error::Closed));
}
let path = state.requests.pop().expect("predicate guaranteed a request");
let producer = state.requests.get(&path).expect("popped key must be pending").clone();
Poll::Ready(Ok(Request {
path,
producer,
home: self.state.clone(),
}))
}
pub async fn requested_broadcast(&self) -> Result<Request, Error> {
kio::wait(|waiter| self.poll_requested_broadcast(waiter)).await
}
}
impl ServeState {
fn resolve(
shared: &kio::Shared<Self>,
path: &PathOwned,
producer: &kio::Producer<PendingBroadcast>,
result: Result<broadcast::Consumer, Error>,
) {
let mut state = shared.lock();
if state.closed {
return;
}
let resolved = match result {
Ok(broadcast) => {
let existing = state.served.insert(path.clone(), broadcast.weak());
Ok(existing.map(|weak| weak.consume()).unwrap_or(broadcast))
}
Err(err) => Err(err),
};
state.requests.remove_if(path, |p| p.same_channel(producer));
if let Ok(mut pending) = producer.write() {
pending.resolved.get_or_insert(resolved);
drop(state);
}
}
fn forget(shared: &kio::Shared<Self>, path: &PathOwned, producer: &kio::Producer<PendingBroadcast>) {
shared.lock().requests.remove_if(path, |p| p.same_channel(producer));
}
}
pub struct Request {
path: PathOwned,
producer: kio::Producer<PendingBroadcast>,
home: kio::Shared<ServeState>,
}
impl Request {
pub fn path(&self) -> &Path<'_> {
&self.path
}
pub fn accept(self, broadcast: impl Consume<broadcast::Consumer>) {
let broadcast = broadcast.consume();
ServeState::resolve(&self.home, &self.path, &self.producer, Ok(broadcast));
}
pub fn reject(self, err: Error) {
ServeState::resolve(&self.home, &self.path, &self.producer, Err(err));
}
}
impl Drop for Request {
fn drop(&mut self) {
ServeState::forget(&self.home, &self.path, &self.producer);
}
}
pub struct Requesting {
inner: RequestState,
path: PathOwned,
stats: stats::Scope,
}
enum RequestState {
Failed(Error),
Pending(kio::Consumer<PendingBroadcast>),
}
impl Requesting {
fn failed(error: Error) -> Self {
Self::new(RequestState::Failed(error))
}
fn queued(consumer: kio::Consumer<PendingBroadcast>) -> Self {
Self::new(RequestState::Pending(consumer))
}
pub fn is_queued(&self) -> bool {
matches!(self.inner, RequestState::Pending(_))
}
fn new(inner: RequestState) -> Self {
Self {
inner,
path: PathOwned::default(),
stats: stats::Scope::default(),
}
}
fn with_path(mut self, path: PathOwned) -> Self {
self.path = path;
self
}
fn with_stats(mut self, scope: stats::Scope) -> Self {
self.stats = scope;
self
}
fn hand_out(&self, broadcast: broadcast::Consumer) -> broadcast::Consumer {
broadcast.with_path(self.path.clone()).with_stats(self.stats.clone())
}
pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll<Result<broadcast::Consumer, Error>> {
match &self.inner {
RequestState::Failed(error) => Poll::Ready(Err(error.clone())),
RequestState::Pending(consumer) => Poll::Ready(
match ready!(consumer.poll(waiter, |state| match &state.resolved {
Some(result) => Poll::Ready(result.clone()),
None => Poll::Pending,
})) {
Ok(result) => result.map(|broadcast| self.hand_out(broadcast)),
Err(_closed) => Err(Error::Unroutable),
},
),
}
}
}
impl kio::Pollable for Requesting {
type Output = Result<broadcast::Consumer, Error>;
fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
self.poll_ok(waiter)
}
}
pub trait Consume<T> {
fn consume(&self) -> T;
}
impl<T, U: Consume<T>> Consume<T> for &U {
fn consume(&self) -> T {
(**self).consume()
}
}
impl Consume<Consumer> for Producer {
fn consume(&self) -> Consumer {
Consumer::from_producer(self, stats::Session::default())
}
}
impl Consume<Consumer> for Consumer {
fn consume(&self) -> Consumer {
self.clone()
}
}
impl Consume<broadcast::Consumer> for broadcast::Producer {
fn consume(&self) -> broadcast::Consumer {
self.consume()
}
}
impl Consume<broadcast::Consumer> for broadcast::Consumer {
fn consume(&self) -> broadcast::Consumer {
self.clone()
}
}
impl Consume<track::Consumer> for track::Producer {
fn consume(&self) -> track::Consumer {
self.consume()
}
}
impl Consume<track::Consumer> for track::Consumer {
fn consume(&self) -> track::Consumer {
self.clone()
}
}
#[derive(Clone)]
pub struct Consumer {
hop: Hop,
scope: OriginScope,
root: PathOwned,
shared: kio::Shared<OriginState>,
stats: stats::Session,
horizon: Horizon,
hidden: Hidden,
pool: cache::Pool,
cache_duration: Duration,
tasks: TasksWeak,
timers: Clock,
}
impl Consumer {
fn from_producer(producer: &Producer, stats: stats::Session) -> Self {
Self {
hop: producer.hop,
scope: producer.scope.clone(),
root: producer.root.clone(),
shared: producer.shared.clone(),
stats,
horizon: Horizon::default(),
hidden: Hidden::default(),
pool: producer.pool.clone(),
cache_duration: producer.cache_duration,
tasks: producer.tasks.downgrade(),
timers: producer.timers.clone(),
}
}
pub fn hop(&self) -> Hop {
self.hop
}
pub(crate) fn excluding(mut self, peer: Hop) -> Self {
self.horizon.exclude = Some(peer);
self
}
pub fn local(mut self) -> Self {
self.horizon.local = true;
self
}
pub fn with_hidden(mut self, hidden: bool) -> Self {
self.hidden.include = hidden;
self
}
pub(crate) fn beyond(mut self, outer: &Consumer) -> Self {
self.hidden.beyond = Some(interest_prefixes(&outer.scope.allowed));
self
}
pub(crate) fn includes_hidden(&self) -> bool {
self.hidden.include
}
pub fn with_stats(mut self, session: stats::Session) -> Self {
self.stats = session;
self
}
fn untagged(&self) -> Self {
Self {
stats: stats::Session::default(),
..self.clone()
}
}
pub(crate) fn empty(&self) -> Self {
Self {
scope: OriginScope::empty(),
..self.clone()
}
}
pub fn announced(&self) -> AnnounceConsumer {
AnnounceConsumer::new(
self.root.clone(),
self.scope.allowed.clone(),
self.stats.clone(),
self.horizon,
self.hidden.clone(),
&self.shared,
)
}
pub fn consume(&self) -> Self {
self.clone()
}
#[cfg(test)]
pub(crate) fn get_broadcast(&self, path: impl AsPath) -> Option<broadcast::Consumer> {
let full = self.root.join(path).to_owned();
if !self.scope.permits(&full) {
return None;
}
let table = self.shared.lock();
table
.routes
.at(&full)
.filter(|entry| entry.local)
.min_by_key(|entry| route_order(&entry.prefix, entry))
.and_then(|entry| entry.source.clone())
}
pub async fn routed(&self, path: impl AsPath) -> Option<Route> {
let path = path.as_path();
let consumer = match Pattern::subtree(path.as_str()) {
Ok(subtree) => self.scope("", &Patterns::from(subtree)).ok()?,
Err(InvalidPattern::TooManySegments) => self.clone(),
Err(_) => return None,
};
if !consumer.allowed().matches(path.as_str()) {
return None;
}
let mut announced = consumer.untagged().with_hidden(true).announced();
loop {
let update = announced.next().await?;
if update.kind.is_active() && path.has_prefix(&update.prefix) {
return Some(update.route);
}
}
}
pub async fn routed_broadcast(&self, path: impl AsPath) -> Result<broadcast::Consumer, Error> {
let path = path.as_path();
if !self.allowed().matches(path.as_str()) {
return Err(Error::Unauthorized);
}
loop {
let (watch, seen) = {
let mut table = self.shared.lock();
if table.closed {
return Err(Error::Closed);
}
let watch = table.watch(&self.shared, &self.root.join(&path));
let seen = watch.seen();
(watch, seen)
};
match self.request_broadcast(&path).await {
Ok(broadcast) => return Ok(broadcast),
Err(Error::Unroutable) => {
kio::wait(|waiter| watch.poll_changed(waiter, seen)).await;
}
Err(Error::Dropped) if self.shared.lock().closed => return Err(Error::Closed),
Err(err) => return Err(err),
}
}
}
pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result<Consumer, Error> {
let root = self.root.join(root).to_owned();
let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?;
let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?;
Ok(Consumer {
scope,
root,
..self.clone()
})
}
pub fn request_broadcast(&self, path: impl AsPath) -> kio::Pending<Requesting> {
let path = path.as_path();
let absolute = self.root.join(&path).to_owned();
let scope = self.stats.egress(&absolute);
let requested = path.to_owned();
if !self.scope.permits(&absolute) {
return kio::Pending::new(Requesting::failed(Error::Unauthorized));
}
let mut state = self.shared.lock();
if state.closed {
return kio::Pending::new(Requesting::failed(Error::Closed));
}
if state
.best_route(&absolute.as_path(), self.horizon, Pin::Any, &HashSet::new())
.is_none()
{
return kio::Pending::new(Requesting::failed(Error::Unroutable));
}
let key = (absolute.clone(), self.horizon);
if let Some(front) = state.fronts.get(&key) {
let pin = *front.pin.lock();
let current = state
.best_route(&absolute.as_path(), self.horizon, Pin::Any, &HashSet::new())
.is_some_and(|entry| entry.qualifies(pin));
if current {
let pending = Requesting::queued(front.request.consume())
.with_path(requested)
.with_stats(scope);
return kio::Pending::new(pending);
}
state.fronts.remove(&key);
}
let broadcast = broadcast::Producer::new_spliced(broadcast::Info {
pool: self.pool.clone(),
cache_duration: self.cache_duration,
path: absolute.clone(),
});
let request = kio::Producer::<PendingBroadcast>::default();
let consumer = request.consume();
let watch = state.watch(&self.shared, &absolute);
let pin = kio::Lock::new(Pin::Any);
state.fronts.insert(
key,
RemoteFront {
request: request.clone(),
broadcast: broadcast.consume().weak(),
pin: pin.clone(),
},
);
drop(state);
self.tasks.push(run_front(FrontTask {
shared: self.shared.clone(),
broadcast,
path: absolute,
horizon: self.horizon,
watch,
request,
pin,
timers: self.timers.clone(),
}));
kio::Pending::new(Requesting::queued(consumer).with_path(requested).with_stats(scope))
}
pub fn root(&self) -> &Path<'_> {
&self.root
}
pub fn allowed(&self) -> Patterns {
self.scope.relative(&self.root)
}
pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
self.root.join(path)
}
}
pub struct AnnounceConsumer {
id: ConsumerId,
shared: kio::Shared<OriginState>,
root: PathOwned,
state: kio::Producer<OriginConsumerState>,
stats: stats::Session,
guards: HashMap<PathOwned, stats::Announce>,
park: kio::Park,
}
impl AnnounceConsumer {
fn new(
root: PathOwned,
allowed: Patterns,
stats: stats::Session,
horizon: Horizon,
hidden: Hidden,
shared: &kio::Shared<OriginState>,
) -> Self {
let state = kio::Producer::<OriginConsumerState>::default();
let id = ConsumerId::new();
{
let mut table = shared.lock();
if table.closed {
if let Ok(mut state) = state.write() {
state.ended = true;
}
} else {
table.register_cursor(
id,
TableCursor {
root: root.clone(),
heads: interest_prefixes(&allowed),
allowed,
horizon,
hidden,
state: state.clone(),
current: HashMap::new(),
},
);
}
}
Self {
id,
shared: shared.clone(),
root,
state,
stats,
guards: HashMap::new(),
park: kio::Park::default(),
}
}
fn hand_out(&mut self, update: AnnounceUpdate) -> AnnounceUpdate {
let absolute = self.root.join(&update.prefix).to_owned();
if update.kind.is_active() {
let scope = self.stats.egress(&absolute);
self.guards
.entry(update.prefix.clone())
.or_insert_with(|| scope.announce());
} else {
self.guards.remove(&update.prefix);
}
update
}
pub async fn next(&mut self) -> Option<AnnounceUpdate> {
kio::wait(|waiter| self.poll_next(waiter)).await
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Option<AnnounceUpdate>> {
let update = {
let mut state = match ready!(self.state.poll(waiter, |state| {
if state.pending.is_empty() && !state.ended {
Poll::Pending
} else {
Poll::Ready(())
}
})) {
Ok(state) => state,
Err(_) => return Poll::Ready(None),
};
match state.take() {
Some(update) => update,
None => {
state.close();
return Poll::Ready(None);
}
}
};
Poll::Ready(Some(self.hand_out(update)))
}
pub fn try_next(&mut self) -> Option<AnnounceUpdate> {
let update = self.state.write().ok()?.take()?;
Some(self.hand_out(update))
}
pub fn is_closed(&self) -> bool {
let state = self.state.read();
state.is_closed() || state.ended
}
pub fn root(&self) -> &Path<'_> {
&self.root
}
pub fn absolute(&self, prefix: impl AsPath) -> Path<'_> {
self.root.join(prefix)
}
}
impl futures::Stream for AnnounceConsumer {
type Item = AnnounceUpdate;
fn poll_next(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let waiter = this.park.hold(cx).clone();
this.poll_next(&waiter)
}
}
impl Drop for AnnounceConsumer {
fn drop(&mut self) {
let mut shared = self.shared.lock();
if let Some(cursor) = shared.cursors.remove(&self.id) {
for head in &cursor.heads {
shared.routes.remove_cursor(head, self.id);
}
}
}
}
#[cfg(test)]
use futures::FutureExt;
#[cfg(test)]
#[allow(missing_docs)] impl AnnounceConsumer {
pub fn assert_next_active(&mut self, expected: impl AsPath) -> Route {
let expected = expected.as_path();
let update = self.next().now_or_never().expect("next blocked").expect("no next");
assert_eq!(update.prefix, expected, "wrong prefix");
assert!(update.kind.is_active(), "should be an active route");
update.route
}
pub fn assert_try_next_active(&mut self, expected: impl AsPath) -> Route {
let expected = expected.as_path();
let update = self.try_next().expect("no next");
assert_eq!(update.prefix, expected, "wrong prefix");
assert!(update.kind.is_active(), "should be an active route");
update.route
}
pub fn assert_next_ended(&mut self, expected: impl AsPath) {
let expected = expected.as_path();
let update = self.next().now_or_never().expect("next blocked").expect("no next");
assert_eq!(update.prefix, expected, "wrong prefix");
assert_eq!(update.kind, AnnounceKind::Retracted, "should be a retraction");
}
pub fn assert_next_wait(&mut self) {
if let Some(res) = self.next().now_or_never() {
panic!("next should block: got {:?}", res.map(|u| u.prefix));
}
}
}
#[cfg(test)]
pub(crate) trait ProduceTest {
fn produce(self) -> Producer;
}
#[cfg(test)]
impl ProduceTest for Config {
fn produce(self) -> Producer {
let (producer, driver) = Producer::new(self);
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(crate::time::run(driver));
} else {
std::mem::forget(driver);
}
producer
}
}
#[cfg(test)]
impl ProduceTest for Hop {
fn produce(self) -> Producer {
Config::new(self).produce()
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt;
fn origin(id: u64) -> Hop {
Hop::new(id).unwrap()
}
fn hops(ids: &[u64]) -> Hops {
let mut list = Hops::new();
for &id in ids {
list.push(if id == 0 { Hop::UNKNOWN } else { origin(id) }).unwrap();
}
list
}
fn scopes(prefixes: &[&str]) -> Patterns {
prefixes
.iter()
.map(|prefix| Pattern::subtree(prefix).unwrap())
.collect()
}
#[test]
fn default_config_mints_a_real_hop() {
let config = Config::default();
assert_ne!(config.hop, Hop::UNKNOWN);
let (producer, _driver) = Producer::new(config.clone());
assert_eq!(producer.hop(), config.hop);
assert_eq!(producer.consume().hop(), config.hop);
}
#[test]
fn random_hops_fit_legacy_lite_clients() {
for _ in 0..32 {
assert!(Hop::random().id() < 1u64 << 53);
}
}
async fn settle(mut check: impl FnMut() -> bool) {
for _ in 0..100 {
if check() {
return;
}
tokio::task::yield_now().await;
}
panic!("condition never settled");
}
async fn queued(server: &Dynamic) -> Request {
let mut request = None;
settle(|| match server.poll_requested_broadcast(&kio::Waiter::noop()) {
Poll::Ready(Ok(popped)) => {
request = Some(popped);
true
}
_ => false,
})
.await;
request.unwrap()
}
async fn next_group(subscription: &mut crate::track::Subscriber) -> Result<Option<crate::group::Consumer>, Error> {
let mut next = None;
settle(|| match subscription.poll_recv_group(&kio::Waiter::noop()) {
Poll::Ready(result) => {
next = Some(result);
true
}
Poll::Pending => false,
})
.await;
next.unwrap()
}
#[tokio::test]
async fn announce_and_retract() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
announced.assert_next_wait();
let announcement = producer.announce("room/alice", Route::default()).unwrap();
let route = announced.assert_next_active("room/alice");
assert!(route.hops.is_empty());
assert_eq!(route.cost, Cost::default());
announced.assert_next_wait();
drop(announcement);
announced.assert_next_ended("room/alice");
announced.assert_next_wait();
}
#[tokio::test]
async fn hidden_routes_need_an_opt_in() {
let producer = origin(1).produce();
let consumer = producer.consume();
let _visible = producer.announce("room/alice", Route::default()).unwrap();
let _stats = producer.announce(".stats/node", Route::default()).unwrap();
let _nested = producer.announce("room/.internal", Route::default()).unwrap();
let _suffix = producer.announce("room/catalog.pro", Route::default()).unwrap();
let mut announced = consumer.announced();
announced.assert_next_active("room/alice");
announced.assert_next_active("room/catalog.pro");
announced.assert_next_wait();
let mut announced = consumer.clone().with_hidden(true).announced();
announced.assert_next_active(".stats/node");
announced.assert_next_active("room/.internal");
announced.assert_next_active("room/alice");
announced.assert_next_active("room/catalog.pro");
announced.assert_next_wait();
let mut announced = consumer
.scope(".stats", &Patterns::from(Pattern::all()))
.unwrap()
.announced();
announced.assert_next_active("node");
announced.assert_next_wait();
let mut announced = consumer.scope("", &scopes(&["room/.internal"])).unwrap().announced();
announced.assert_next_active("room/.internal");
announced.assert_next_wait();
let mut announced = consumer.clone().with_hidden(true).beyond(&consumer).announced();
announced.assert_next_active(".stats/node");
announced.assert_next_active("room/.internal");
announced.assert_next_wait();
let room = consumer.scope("", &scopes(&["room"])).unwrap().beyond(&consumer);
let mut announced = room.announced();
announced.assert_next_wait();
let stats = consumer.scope("", &scopes(&[".stats"])).unwrap().beyond(&consumer);
let mut announced = stats.announced();
announced.assert_next_active(".stats/node");
announced.assert_next_wait();
}
#[tokio::test]
async fn hidden_broadcast_resolves_by_path() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.create_broadcast(".stats/node").unwrap();
broadcast.announce(Route::default()).unwrap();
consumer.announced().assert_next_wait();
let resolved = consumer.request_broadcast(".stats/node").await.expect("resolves");
assert_eq!(resolved.info().path.as_str(), ".stats/node");
}
#[tokio::test]
async fn hidden_route_announced_later_stays_hidden() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let mut opted = consumer.clone().with_hidden(true).announced();
let hidden = producer.announce(".stats/node", Route::default()).unwrap();
announced.assert_next_wait();
opted.assert_next_active(".stats/node");
drop(hidden);
announced.assert_next_wait();
opted.assert_next_ended(".stats/node");
}
#[tokio::test]
async fn broadcast_announces_its_own_path() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let mut peer = consumer.clone().excluding(Hop::UNKNOWN).announced();
let broadcast = producer.create_broadcast("room/alice").unwrap();
announced.assert_next_wait();
peer.assert_next_wait();
broadcast.announce(Route::default().with_cost(3)).unwrap();
assert_eq!(announced.assert_next_active("room/alice").cost, Cost::new(3));
assert_eq!(peer.assert_next_active("room/alice").cost, Cost::new(3));
broadcast.announce(Route::default().with_cost(1)).unwrap();
assert_eq!(announced.assert_next_active("room/alice").cost, Cost::new(1));
assert_eq!(peer.assert_next_active("room/alice").cost, Cost::new(1));
broadcast.unannounce();
announced.assert_next_ended("room/alice");
peer.assert_next_ended("room/alice");
broadcast.unannounce();
announced.assert_next_wait();
let err = consumer.request_broadcast("room/alice").await.err().unwrap();
assert!(matches!(err, Error::Unroutable));
broadcast.announce(Route::default()).unwrap();
announced.assert_next_active("room/alice");
peer.assert_next_active("room/alice");
broadcast.finish();
announced.assert_next_ended("room/alice");
peer.assert_next_ended("room/alice");
assert!(matches!(broadcast.announce(Route::default()), Err(Error::Closed)));
announced.assert_next_wait();
}
#[tokio::test]
async fn broadcast_announcement_retracts_with_the_last_producer() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let broadcast = producer.create_broadcast("room/alice").unwrap();
let clone = broadcast.clone();
broadcast.announce(Route::default()).unwrap();
announced.assert_next_active("room/alice");
drop(broadcast);
announced.assert_next_wait();
drop(clone);
announced.assert_next_ended("room/alice");
}
#[tokio::test]
async fn publish_creates_and_announces_together() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let _broadcast = producer.publish("room/alice", Route::default()).unwrap();
announced.assert_next_active("room/alice");
}
#[tokio::test]
async fn standalone_broadcast_cannot_announce() {
let broadcast = broadcast::Info::new().produce();
assert!(matches!(broadcast.announce(Route::default()), Err(Error::Closed)));
broadcast.unannounce();
}
#[tokio::test]
async fn announce_replays_to_late_cursor() {
let producer = origin(1).produce();
let _a = producer.announce("room/alice", Route::default()).unwrap();
let _b = producer.announce("room/bob", Route::default()).unwrap();
let mut announced = producer.consume().announced();
announced.assert_next_active("room/alice");
announced.assert_next_active("room/bob");
announced.assert_next_wait();
}
#[tokio::test]
async fn announce_keeps_its_prefix_under_a_producer_scope() {
let producer = origin(1).produce();
let scoped = producer.scope("", &scopes(&["room"])).unwrap();
let _a = scoped.announce("", Route::default()).unwrap();
let mut announced = producer.consume().announced();
announced.assert_next_active("");
assert!(matches!(
scoped.announce("other", Route::default()),
Err(Error::Unauthorized)
));
}
#[tokio::test]
async fn cursor_keeps_an_overlapping_prefix_above_its_scope() {
let producer = origin(1).produce();
let _a = producer.announce("", Route::default()).unwrap();
let consumer = producer.consume().scope("", &scopes(&["room"])).unwrap();
let mut announced = consumer.announced();
announced.assert_next_active("");
}
#[tokio::test]
async fn cursor_root_strips_prefix() {
let producer = origin(1).produce();
let _a = producer.announce("room/alice", Route::default()).unwrap();
let consumer = producer
.consume()
.scope("room", &Patterns::from(Pattern::all()))
.unwrap();
let mut announced = consumer.announced();
announced.assert_next_active("alice");
}
#[tokio::test]
async fn best_route_wins_and_fails_over() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let expensive = producer
.announce("room", Route::default().with_hops(hops(&[10])).with_cost(5))
.unwrap();
let route = announced.assert_next_active("room");
assert_eq!(route.cost, Cost::new(5));
let cheap = producer
.announce("room", Route::default().with_hops(hops(&[20])).with_cost(1))
.unwrap();
let route = announced.assert_next_active("room");
assert_eq!(route.cost, Cost::new(1));
drop(cheap);
let route = announced.assert_next_active("room");
assert_eq!(route.cost, Cost::new(5));
drop(expensive);
announced.assert_next_ended("room");
}
#[tokio::test]
async fn identical_reannounce_is_invisible() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let old = producer
.announce("room", Route::default().with_hops(hops(&[10])))
.unwrap();
let first = announced.assert_next_active("room");
assert_eq!(first.hops.as_slice(), hops(&[10]).as_slice());
let _new = producer
.announce("room", Route::default().with_hops(hops(&[10])))
.unwrap();
announced.assert_next_wait();
drop(old);
announced.assert_next_wait();
}
#[tokio::test]
async fn exclude_hides_routes_through_the_peer() {
let producer = origin(1).produce();
let _a = producer
.announce("room", Route::default().with_hops(hops(&[7])))
.unwrap();
let mut hidden = producer.consume().excluding(origin(7)).announced();
hidden.assert_next_wait();
let mut visible = producer.consume().excluding(origin(8)).announced();
visible.assert_next_active("room");
}
#[tokio::test]
async fn exclude_matches_via_when_the_chain_is_anonymous() {
let producer = origin(1).produce();
let assigned = origin(777);
let _echoed = producer
.announce("echoed", Route::default().with_hops(hops(&[0])).with_via(assigned))
.unwrap();
let _local = producer
.announce("local", Route::default().with_hops(hops(&[10])))
.unwrap();
let mut hidden = producer.consume().excluding(assigned).announced();
hidden.assert_next_active("local");
hidden.assert_next_wait();
}
#[tokio::test]
async fn anonymous_route_loses_to_identified_at_any_cost() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let _anonymous = producer
.announce("room", Route::default().with_hops(hops(&[0])).with_cost(1))
.unwrap();
let route = announced.assert_next_active("room");
assert!(route.is_anonymous());
assert_eq!(route.cost, Cost::new(1));
let _identified = producer
.announce("room", Route::default().with_hops(hops(&[10])).with_cost(5))
.unwrap();
let route = announced.assert_next_active("room");
assert!(!route.is_anonymous());
assert_eq!(route.cost, Cost::new(5));
}
#[tokio::test]
async fn anonymous_routes_order_by_cost() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let expensive = producer
.announce("room", Route::default().with_hops(hops(&[0])).with_cost(5))
.unwrap();
let route = announced.assert_next_active("room");
assert_eq!(route.cost, Cost::new(5));
let _cheap = producer
.announce("room", Route::default().with_hops(hops(&[0, 7])).with_cost(1))
.unwrap();
let route = announced.assert_next_active("room");
assert!(route.is_anonymous());
assert_eq!(route.cost, Cost::new(1));
drop(expensive);
announced.assert_next_wait();
}
#[tokio::test]
async fn anonymous_chain_from_identified_peer_still_ranks_last() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let _anonymous = producer
.announce(
"room",
Route::default()
.with_hops(hops(&[0, 7]))
.with_cost(1)
.with_via(origin(7)),
)
.unwrap();
announced.assert_next_active("room");
let _identified = producer
.announce("room", Route::default().with_hops(hops(&[10, 20])).with_cost(5))
.unwrap();
let route = announced.assert_next_active("room");
assert!(!route.is_anonymous());
assert_eq!(route.cost, Cost::new(5));
}
#[tokio::test]
async fn request_prefers_identified_over_cheaper_anonymous() {
let producer = origin(1).produce();
let consumer = producer.consume();
let anonymous = producer
.dynamic("room", Route::default().with_hops(hops(&[0])).with_cost(1))
.unwrap();
let identified = producer
.dynamic("room", Route::default().with_hops(hops(&[10])).with_cost(5))
.unwrap();
let _pending = consumer.request_broadcast("room/alice");
let request = queued(&identified).await;
assert_eq!(request.path().as_str(), "room/alice");
assert!(
anonymous.poll_requested_broadcast(&kio::Waiter::noop()).is_pending(),
"the cheaper anonymous route must not serve"
);
}
#[tokio::test]
async fn update_reprices_in_place() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let announcement = producer.announce("room", Route::default()).unwrap();
announced.assert_next_active("room");
announcement.update(Route::default().with_cost(9)).unwrap();
let route = announced.assert_next_active("room");
assert_eq!(route.cost, Cost::new(9));
}
#[tokio::test]
async fn retract_after_undelivered_reprice_still_delivered() {
let producer = origin(1).produce();
let mut announced = producer.consume().announced();
let announcement = producer.announce("room", Route::default()).unwrap();
announced.assert_next_active("room");
announcement.update(Route::default().with_cost(9)).unwrap();
drop(announcement);
announced.assert_next_ended("room");
announced.assert_next_wait();
}
#[tokio::test]
async fn scoped_cursor_advertises_most_specific_covering_route() {
let producer = origin(1).produce();
let _broad = producer.announce("room", Route::default().with_cost(1)).unwrap();
let _narrow = producer.announce("room/alice", Route::default().with_cost(9)).unwrap();
let consumer = producer
.consume()
.scope("room/alice", &Patterns::from(Pattern::all()))
.unwrap();
let mut announced = consumer.announced();
let route = announced.assert_next_active("");
assert_eq!(route.cost, Cost::new(9));
announced.assert_next_wait();
}
#[tokio::test]
async fn capture_change_retracts_before_reannouncing_a_presented_prefix() {
let producer = origin(1).produce();
let _broad = producer.announce("room", Route::default()).unwrap();
let exact = producer.announce("room/alice", Route::default()).unwrap();
let consumer = producer
.consume()
.scope("", &Patterns::from("room/*".parse::<Pattern>().unwrap()))
.unwrap()
.scope("room/alice", &Patterns::from(Pattern::all()))
.unwrap();
let mut announced = consumer.announced();
let first = announced.next().now_or_never().expect("next").expect("announce");
assert_eq!(first.prefix.as_str(), "");
assert_eq!(first.kind, AnnounceKind::Announced);
assert_eq!(first.captures, Some(Vec::new()));
drop(exact);
let retracted = announced.next().now_or_never().expect("next").expect("retract");
assert_eq!(retracted.prefix.as_str(), "");
assert_eq!(retracted.kind, AnnounceKind::Retracted);
assert_eq!(retracted.captures, Some(Vec::new()));
let replacement = announced.next().now_or_never().expect("next").expect("announce");
assert_eq!(replacement.prefix.as_str(), "");
assert_eq!(replacement.kind, AnnounceKind::Announced);
assert_eq!(replacement.captures, None);
}
#[tokio::test]
async fn routed_broadcast_resolves_once_announced() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut resolving = Box::pin(consumer.routed_broadcast("room/alice"));
assert!((&mut resolving).now_or_never().is_none());
let broadcast = producer.create_broadcast("room/alice").unwrap();
for _ in 0..20 {
tokio::task::yield_now().await;
}
assert!((&mut resolving).now_or_never().is_none());
broadcast.announce(Route::default()).unwrap();
let resolved = resolving.await.expect("resolves once announced");
assert_eq!(resolved.info().path.as_str(), "room/alice");
drop(broadcast);
}
#[tokio::test]
async fn cheaper_remote_route_beats_a_local_broadcast() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let _local = producer.publish("room/alice", Route::default().with_cost(5)).unwrap();
assert_eq!(announced.assert_next_active("room/alice").cost, Cost::new(5));
let server = producer
.dynamic("room/alice", Route::default().with_hops(hops(&[10])).with_cost(1))
.unwrap();
let route = announced.assert_next_active("room/alice");
assert_eq!(route.cost, Cost::new(1));
assert_eq!(route.hops, hops(&[10]));
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
let upstream = broadcast::Info::new().produce();
request.accept(&upstream);
pending.await.expect("resolves through the cheaper route");
}
#[tokio::test]
async fn cheaper_route_after_a_front_wins_new_requests() {
let producer = origin(1).produce();
let consumer = producer.consume();
let _local = producer.publish("room/alice", Route::default().with_cost(5)).unwrap();
let first = consumer
.request_broadcast("room/alice")
.await
.expect("resolves locally");
let server = producer
.dynamic("room/alice", Route::default().with_hops(hops(&[10])).with_cost(1))
.unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
let upstream = broadcast::Info::new().produce();
request.accept(&upstream);
let second = pending.await.expect("resolves through the cheaper route");
assert!(!first.is_closed(), "the old front must keep serving its readers");
assert!(!first.is_clone(&second), "the newcomer must not join the old front");
}
#[tokio::test]
async fn local_broadcast_wins_a_tie_with_a_hopless_route() {
let producer = origin(1).produce();
let consumer = producer.consume();
let _local = producer.publish("room/alice", Route::default()).unwrap();
let server = producer.dynamic("room/alice", Route::default()).unwrap();
let resolved = tokio::time::timeout(Duration::from_secs(1), consumer.request_broadcast("room/alice"))
.await
.expect("the newer hopless route won the tie")
.expect("resolves");
assert_eq!(resolved.info().path.as_str(), "room/alice");
assert!(server.poll_requested_broadcast(&kio::Waiter::noop()).is_pending());
}
#[tokio::test]
async fn announce_stats_follow_the_advertisement() {
let registry = stats::Registry::new(stats::Config::new());
let producer = origin(1)
.produce()
.with_stats(registry.tier(stats::Tier::default()).session("root"));
let announces = || {
registry
.snapshot()
.traffic()
.into_iter()
.find(|(_, role, _)| *role == stats::Role::Subscriber)
.map(|(_, _, traffic)| (traffic.announces_started, traffic.announces_ended))
.unwrap_or_default()
};
let broadcast = producer.create_broadcast("room/alice").unwrap();
assert_eq!(announces(), (0, 0), "a hidden broadcast is not announced");
broadcast.announce(Route::default()).unwrap();
broadcast
.announce(Route {
cost: Cost::new(3),
..Route::default()
})
.unwrap();
assert_eq!(announces(), (1, 0), "a re-price is not another announce");
broadcast.unannounce();
assert_eq!(announces(), (1, 1));
broadcast.announce(Route::default()).unwrap();
drop(broadcast);
assert_eq!(announces(), (2, 2));
}
#[tokio::test]
async fn local_broadcast_wins_a_cost_tie() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let server = producer
.dynamic("room/alice", Route::default().with_hops(hops(&[10])).with_cost(2))
.unwrap();
announced.assert_next_active("room/alice");
let _local = producer.publish("room/alice", Route::default().with_cost(2)).unwrap();
assert!(announced.assert_next_active("room/alice").hops.is_empty());
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
assert_eq!(resolved.info().path.as_str(), "room/alice");
for _ in 0..20 {
tokio::task::yield_now().await;
}
assert!(server.poll_requested_broadcast(&kio::Waiter::noop()).is_pending());
}
#[tokio::test]
async fn unannounce_ends_the_front() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
broadcast.unannounce();
let err = consumer.request_broadcast("room/alice").await.err().unwrap();
assert!(matches!(err, Error::Unroutable), "joined a retracted front: {err}");
settle(|| resolved.is_closed()).await;
assert!(!broadcast.consume().is_closed(), "the broadcast itself lives on");
broadcast.announce(Route::default()).unwrap();
let again = consumer.request_broadcast("room/alice").await.expect("resolves again");
assert!(!again.is_clone(&resolved));
}
#[tokio::test]
async fn unannounce_keeps_a_track_awaiting_its_info() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let mut dynamic = broadcast.dynamic();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let track = resolved.track("video").unwrap();
let subscribing = tokio::spawn(async move { track.subscribe(None).await });
let request = tokio::time::timeout(Duration::from_secs(1), dynamic.requested_track())
.await
.expect("the front asked the source")
.expect("request");
broadcast.unannounce();
settle(|| resolved.is_closed()).await;
let source = request.accept(None);
let mut group = source.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"late".as_ref()).unwrap();
group.finish().unwrap();
source.finish().unwrap();
let mut subscription = subscribing.await.unwrap().expect("subscribe survives the retraction");
let mut group = subscription.recv_group().await.unwrap().expect("the source's group");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"late");
assert!(matches!(subscription.recv_group().await, Ok(None)), "ends cleanly");
}
#[tokio::test]
async fn unannounce_keeps_a_returning_reader_awaiting_its_info() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let mut dynamic = broadcast.dynamic();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let track = resolved.track("video").unwrap();
let subscribing = tokio::spawn(async move { track.subscribe(None).await });
let request = tokio::time::timeout(Duration::from_secs(1), dynamic.requested_track())
.await
.expect("the front asked the source")
.expect("request");
let source = request.accept(None);
let mut group = source.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"cached".as_ref()).unwrap();
group.finish().unwrap();
let mut subscription = subscribing.await.unwrap().expect("subscribe");
subscription.recv_group().await.unwrap().expect("the cached group");
drop(subscription);
tokio::time::timeout(Duration::from_secs(1), source.unused())
.await
.expect("parked")
.expect("source open");
drop(source);
let track = resolved.track("video").unwrap();
let subscribing = tokio::spawn(async move { track.subscribe(None).await });
let request = tokio::time::timeout(Duration::from_secs(1), dynamic.requested_track())
.await
.expect("the front asked the source again")
.expect("request");
broadcast.unannounce();
settle(|| resolved.is_closed()).await;
let source = request.accept(None);
let mut group = source.create_group(1u64.into()).unwrap();
group.write_frame(crate::Timestamp::ZERO, b"late".as_ref()).unwrap();
group.finish().unwrap();
source.finish().unwrap();
let mut subscription = subscribing.await.unwrap().expect("subscribe survives the retraction");
let mut payloads = Vec::new();
while let Some(mut group) = subscription.recv_group().await.expect("ends cleanly") {
payloads.push(group.read_frame().await.unwrap().unwrap().payload);
}
assert_eq!(payloads.last().map(|p| &p[..]), Some(&b"late"[..]));
}
#[tokio::test]
async fn reannounce_before_the_front_acts_keeps_it() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
broadcast.unannounce();
broadcast.announce(Route::default()).unwrap();
for _ in 0..20 {
tokio::task::yield_now().await;
}
assert!(!resolved.is_closed(), "the front ended across a reannouncement");
let again = consumer.request_broadcast("room/alice").await.expect("resolves");
assert!(again.is_clone(&resolved));
}
#[tokio::test]
async fn local_broadcast_resolves_once_announced() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.create_broadcast("room/alice").unwrap();
let err = consumer
.request_broadcast("room/alice")
.now_or_never()
.expect("unroutable is synchronous")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
broadcast.announce(Route::default()).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
assert_eq!(resolved.info().path.as_str(), "room/alice");
drop(broadcast);
let err = consumer
.request_broadcast("room/bob")
.now_or_never()
.expect("unroutable is synchronous")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
}
#[test]
fn create_broadcast_accepts_a_max_depth_path() {
let producer = origin(1).produce();
let path = vec!["a"; Path::MAX_PARTS].join("/");
let _broadcast = producer.create_broadcast(path.as_str()).expect("max depth is allowed");
let deeper = vec!["a"; Path::MAX_PARTS + 1].join("/");
assert!(matches!(
producer.create_broadcast(deeper.as_str()),
Err(Error::BoundsExceeded(_))
));
}
#[tokio::test]
async fn duplicate_routes_aggregate_until_the_last_leaves() {
let producer = origin(1).produce();
let first = producer.dynamic("live", Route::default().with_cost(3)).unwrap();
let second = producer.dynamic("live", Route::default().with_cost(1)).unwrap();
let mut announced = producer.consume().announced();
let update = announced.next().now_or_never().expect("next").expect("no next");
assert_eq!(update.prefix.as_str(), "live");
assert_eq!(update.kind, AnnounceKind::Announced);
assert_eq!(update.route.cost, Cost::new(1));
announced.assert_next_wait();
drop(second);
let update = announced.next().now_or_never().expect("next").expect("no next");
assert_eq!(update.prefix.as_str(), "live");
assert_eq!(update.kind, AnnounceKind::Updated);
assert_eq!(update.route.cost, Cost::new(3));
drop(first);
announced.assert_next_ended("live");
announced.assert_next_wait();
}
#[test]
fn dynamic_may_cover_a_scope_but_disjoint_prefixes_are_refused() {
let producer = origin(1).produce();
let scoped = producer.scope("", &scopes(&["room"])).unwrap();
let _broad = scoped
.dynamic("", Route::default())
.expect("an overlapping prefix is accepted");
let _ok = scoped
.dynamic("room/alice", Route::default())
.expect("a contained prefix is accepted");
assert!(matches!(
scoped.dynamic("other", Route::default()),
Err(Error::Unauthorized)
));
}
#[tokio::test]
async fn dynamic_route_keeps_its_producer_scope() {
let producer = origin(1).produce();
let scope = Patterns::from("*/chat".parse::<Pattern>().unwrap());
let scoped = producer.scope("", &scope).unwrap();
let dynamic = scoped.dynamic("", Route::default()).unwrap();
let mut matching = producer
.consume()
.scope("", &scopes(&["room/chat"]))
.unwrap()
.announced();
matching.assert_next_active("");
let mut outside = producer
.consume()
.scope("", &scopes(&["room/video"]))
.unwrap()
.announced();
outside.assert_next_wait();
let refused = producer
.consume()
.request_broadcast("room/video")
.now_or_never()
.expect("an out-of-scope request must be refused synchronously");
assert!(matches!(refused, Err(Error::Unroutable)));
assert!(dynamic.requested_broadcast().now_or_never().is_none());
let _pending = producer.consume().request_broadcast("room/chat");
let request = queued(&dynamic).await;
assert_eq!(request.path().as_str(), "room/chat");
}
#[tokio::test]
async fn dynamic_accepts_a_max_depth_prefix() {
let producer = origin(1).produce();
let path = (0..Path::MAX_PARTS)
.map(|i| format!("s{i}"))
.collect::<Vec<_>>()
.join("/");
let mut announced = producer.consume().announced();
let dynamic = producer.dynamic(&path, Route::default()).expect("max depth is allowed");
announced.assert_next_active(&path);
let _pending = producer.consume().request_broadcast(&path);
let request = queued(&dynamic).await;
assert_eq!(request.path().as_str(), path);
}
#[tokio::test]
async fn dynamic_exclusion_skips_routes_through_the_subscriber() {
let producer = origin(1).produce();
let _server = producer
.dynamic("live", Route::default().with_hops(hops(&[7])))
.unwrap();
let mut excluded = producer.consume().excluding(origin(7)).announced();
excluded.assert_next_wait();
let mut clean = producer.consume().excluding(origin(8)).announced();
clean.assert_next_active("live");
}
#[tokio::test]
async fn announce_consumer_is_a_stream() {
use futures::StreamExt;
let producer = origin(1).produce();
let server = producer.dynamic("live", Route::default()).unwrap();
let mut announced = producer.consume().announced();
let update = StreamExt::next(&mut announced)
.now_or_never()
.expect("next")
.expect("no next");
assert_eq!(update.prefix.as_str(), "live");
assert_eq!(update.kind, AnnounceKind::Announced);
assert!(StreamExt::next(&mut announced).now_or_never().is_none());
drop(server);
let update = StreamExt::next(&mut announced)
.now_or_never()
.expect("next")
.expect("no next");
assert_eq!(update.kind, AnnounceKind::Retracted);
}
#[tokio::test]
async fn dynamic_retracts() {
let producer = origin(1).produce();
let server = producer.dynamic("live", Route::default()).unwrap();
let mut announced = producer.consume().announced();
announced.assert_next_active("live");
drop(server);
announced.assert_next_ended("live");
}
#[test]
fn charged_wildcard_cost_accumulates_across_hops() {
let first = Cost::new(4).charged(1);
let second = first.charged(2);
assert_eq!(second, Cost { warm: 7, cold: 7 });
}
#[tokio::test]
async fn local_broadcast_is_invisible_until_announced() {
let producer = origin(1).produce();
let mut local = producer.consume().announced();
let mut peer = producer.consume().excluding(Hop::UNKNOWN).announced();
let broadcast = producer.create_broadcast("room/alice").unwrap();
local.assert_next_wait();
peer.assert_next_wait();
broadcast.announce(Route::default()).unwrap();
local.assert_next_active("room/alice");
peer.assert_next_active("room/alice");
drop(broadcast);
local.assert_next_ended("room/alice");
peer.assert_next_ended("room/alice");
}
#[tokio::test]
async fn served_route_materializes_on_demand() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
assert_eq!(request.path().as_str(), "room/alice");
let source = broadcast::Info::new().produce();
request.accept(&source);
let resolved = pending.await.expect("resolves");
assert_eq!(resolved.info().path.as_str(), "room/alice");
let again = consumer.request_broadcast("room/alice").await.expect("resolves");
assert!(again.is_clone(&resolved));
}
#[tokio::test]
async fn served_requests_coalesce() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let first = consumer.request_broadcast("room/alice");
let second = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
assert!(server.poll_requested_broadcast(&kio::Waiter::noop()).is_pending());
let source = broadcast::Info::new().produce();
request.accept(&source);
let first = first.await.expect("resolves");
let second = second.await.expect("resolves");
assert!(first.is_clone(&second));
}
#[tokio::test]
async fn retract_rejects_pending_requests() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let pending = consumer.request_broadcast("room/alice");
drop(server);
let err = pending.await.err().unwrap();
assert!(matches!(err, Error::Unroutable));
let err = consumer
.request_broadcast("room/alice")
.now_or_never()
.expect("unroutable")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
}
#[tokio::test]
async fn routed_broadcast_survives_serving_route_retraction() {
let producer = origin(1).produce();
let consumer = producer.consume();
let standby_server = producer.dynamic("room", Route::default()).unwrap();
let second_server = producer.dynamic("room", Route::default()).unwrap();
let incumbent_server = producer.dynamic("room", Route::default()).unwrap();
let mut resolving = Box::pin(consumer.routed_broadcast("room/alice"));
assert!((&mut resolving).now_or_never().is_none());
drop(incumbent_server);
assert!((&mut resolving).now_or_never().is_none());
drop(second_server);
assert!((&mut resolving).now_or_never().is_none());
let request = queued(&standby_server).await;
let source = broadcast::Info::new().produce();
request.accept(&source);
let resolved = resolving.await.expect("resolves via the standby");
assert_eq!(resolved.info().path.as_str(), "room/alice");
}
#[tokio::test]
async fn split_horizon_skips_routes_through_the_requester() {
let producer = origin(1).produce();
let _server = producer
.dynamic("room", Route::default().with_hops(hops(&[7])))
.unwrap();
let excluded = producer.consume().excluding(origin(7));
let err = excluded
.request_broadcast("room/alice")
.now_or_never()
.expect("unroutable")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
let clean = producer.consume().excluding(origin(8));
let pending = clean.request_broadcast("room/alice");
assert!(pending.now_or_never().is_none());
}
#[tokio::test]
async fn routes_report_where_they_entered() {
let producer = origin(1).produce();
let peer = producer.clone().peer();
let mut announced = producer.consume().announced();
let _ingest = producer
.dynamic("client", Route::default().with_hops(hops(&[5])).with_via(origin(5)))
.unwrap();
let _gateway = producer.publish("gateway", Route::default()).unwrap();
let _forwarded = peer
.dynamic(
"forwarded",
Route::default().with_hops(hops(&[5, 7])).with_via(origin(7)),
)
.unwrap();
assert_eq!(announced.assert_next_active("client").source(), Source::Local);
assert_eq!(
announced.assert_next_active("forwarded").source(),
Source::Peer(origin(7))
);
assert_eq!(announced.assert_next_active("gateway").source(), Source::Local);
let scoped = peer.scope("room", &Patterns::from(Pattern::all())).unwrap();
let _nested = scoped.dynamic("x", Route::default().with_via(origin(8))).unwrap();
assert_eq!(announced.assert_next_active("room/x").source(), Source::Peer(origin(8)));
}
#[tokio::test]
async fn source_change_is_an_update() {
let producer = origin(1).produce();
let peer = producer.clone().peer();
let mut announced = producer.consume().announced();
let route = Route::default().with_hops(hops(&[7])).with_via(origin(7));
let _forwarded = peer.dynamic("room", route.clone()).unwrap();
assert_eq!(announced.assert_next_active("room").source(), Source::Peer(origin(7)));
let local = producer.dynamic("room", route).unwrap();
let update = announced.next().now_or_never().expect("next blocked").expect("no next");
assert_eq!(update.kind, AnnounceKind::Updated);
assert_eq!(update.route.source(), Source::Local);
drop(local);
assert_eq!(announced.assert_next_active("room").source(), Source::Peer(origin(7)));
}
#[tokio::test]
async fn local_view_hides_peer_routes() {
let producer = origin(1).produce();
let peer = producer.clone().peer();
let mut local = producer.consume().local().announced();
let _forwarded = peer
.dynamic("remote", Route::default().with_hops(hops(&[7])).with_via(origin(7)))
.unwrap();
local.assert_next_wait();
let _shadow = peer
.dynamic("both", Route::default().with_hops(hops(&[7])).with_via(origin(7)))
.unwrap();
let ingest = producer
.dynamic(
"both",
Route::default().with_hops(hops(&[5])).with_via(origin(5)).with_cost(9),
)
.unwrap();
assert_eq!(local.assert_next_active("both").source(), Source::Local);
drop(ingest);
local.assert_next_ended("both");
let err = producer
.consume()
.local()
.request_broadcast("remote/alice")
.now_or_never()
.expect("unroutable")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
assert!(
producer
.consume()
.request_broadcast("remote/alice")
.now_or_never()
.is_none()
);
}
#[tokio::test]
async fn handler_rejection_is_final() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
request.reject(Error::Unroutable);
let err = tokio::time::timeout(Duration::from_secs(5), pending)
.await
.expect("the front must give up, not spin")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
let pending = consumer.request_broadcast("room/bob");
let request = queued(&server).await;
assert_eq!(request.path().as_str(), "room/bob");
let served = broadcast::Info::new().produce();
request.accept(&served);
pending.await.expect("resolves");
}
#[tokio::test]
async fn routed_broadcast_waits_out_a_rejection() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let mut resolving = Box::pin(consumer.routed_broadcast("room/alice"));
assert!((&mut resolving).now_or_never().is_none());
let request = queued(&server).await;
request.reject(Error::Unroutable);
for _ in 0..20 {
tokio::task::yield_now().await;
}
assert!((&mut resolving).now_or_never().is_none());
assert!(server.poll_requested_broadcast(&kio::Waiter::noop()).is_pending());
server.update(Route::default().with_cost(2)).unwrap();
assert!((&mut resolving).now_or_never().is_none());
let request = queued(&server).await;
let served = broadcast::Info::new().produce();
request.accept(&served);
resolving.await.expect("resolves");
}
#[tokio::test]
async fn routed_broadcast_reports_teardown_as_closed() {
let (producer, driver) = Producer::new(Config::new(origin(1)));
let consumer = producer.consume();
let _server = producer.dynamic("room", Route::default()).unwrap();
let mut resolving = Box::pin(consumer.routed_broadcast("room/alice"));
assert!((&mut resolving).now_or_never().is_none());
drop(driver);
let err = tokio::time::timeout(Duration::from_secs(5), resolving)
.await
.expect("teardown resolves the wait")
.err()
.unwrap();
assert!(matches!(err, Error::Closed), "unexpected end: {err}");
}
#[tokio::test]
async fn routed_broadcast_wakes_for_a_local_broadcast() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let mut resolving = Box::pin(consumer.routed_broadcast("room/alice"));
assert!((&mut resolving).now_or_never().is_none());
queued(&server).await.reject(Error::Unroutable);
for _ in 0..20 {
tokio::task::yield_now().await;
}
assert!((&mut resolving).now_or_never().is_none());
let _local = producer.publish("room/alice", Route::default()).unwrap();
let resolved = resolving.await.expect("resolves locally");
assert_eq!(resolved.info().path.as_str(), "room/alice");
assert!(server.poll_requested_broadcast(&kio::Waiter::noop()).is_pending());
}
#[tokio::test]
async fn late_track_on_a_served_front_replays() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer.dynamic("room", Route::default()).unwrap();
let source = broadcast::Info::new().produce();
for name in ["a", "b"] {
let track = source.create_track(name, None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, name.as_bytes()).unwrap();
group.finish().unwrap();
std::mem::forget(track);
}
let pending = consumer.request_broadcast("room/alice");
queued(&server).await.accept(&source);
let resolved = pending.await.expect("resolves");
let budget = track::Subscription::default().with_max_age(Duration::from_secs(3600));
for name in ["a", "b"] {
let mut subscription = resolved
.track(name)
.unwrap()
.subscribe(budget.clone())
.await
.expect("subscribe");
let mut group = tokio::time::timeout(Duration::from_secs(5), subscription.recv_group())
.await
.expect("the late track must replay, not park")
.expect("recv group")
.expect("track ended early");
let frame = group.read_frame().await.expect("read frame").expect("frame");
assert_eq!(&frame.payload[..], name.as_bytes());
}
}
#[tokio::test]
async fn most_specific_prefix_shadows() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broad_server = producer.dynamic("", Route::default()).unwrap();
let _narrow = producer.announce(".dash", Route::default()).unwrap();
let err = consumer
.request_broadcast(".dash/pid")
.now_or_never()
.expect("unroutable")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
let _pending = consumer.request_broadcast("room/alice");
let request = queued(&broad_server).await;
assert_eq!(request.path().as_str(), "room/alice");
}
#[tokio::test]
async fn root_dynamic_serves_any_path() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut announced = consumer.announced();
let dynamic = producer.dynamic("", Route::default()).unwrap();
announced.assert_next_active("");
let pending = consumer.request_broadcast("anything/at/all");
let request = queued(&dynamic).await;
assert_eq!(request.path().as_str(), "anything/at/all");
let source = broadcast::Info::new().produce();
request.accept(&source);
let resolved = pending.await.expect("resolves");
assert_eq!(resolved.info().path.as_str(), "anything/at/all");
drop(dynamic);
announced.assert_next_ended("");
let err = consumer
.request_broadcast("something/else")
.now_or_never()
.expect("unroutable")
.err()
.unwrap();
assert!(matches!(err, Error::Unroutable));
}
#[tokio::test]
async fn out_of_scope_request_never_reaches_the_dynamic_handler() {
let producer = origin(1).produce();
let dynamic = producer.dynamic("", Route::default()).unwrap();
let scoped = producer.consume().scope("", &scopes(&["tenant-a"])).unwrap();
for path in ["tenant-b/live", "tenant-a-other/live"] {
let refused = scoped
.request_broadcast(path)
.now_or_never()
.expect("an out-of-scope request must be refused synchronously, not queued");
assert!(matches!(refused, Err(Error::Unauthorized)));
assert!(
dynamic.requested_broadcast().now_or_never().is_none(),
"the dynamic handler was asked to create a broadcast the requester may not read"
);
}
}
#[tokio::test]
async fn routed_waits_for_coverage() {
let producer = origin(1).produce();
let consumer = producer.consume();
let mut fut = consumer.routed("room/alice").boxed();
assert!((&mut fut).now_or_never().is_none());
let _a = producer.announce("room", Route::default().with_cost(3)).unwrap();
let route = fut.now_or_never().expect("covered").expect("routed");
assert_eq!(route.cost, Cost::new(3));
consumer
.routed("room/alice/cam")
.now_or_never()
.expect("covered")
.expect("routed");
}
#[tokio::test]
async fn routed_ignores_deeper_routes() {
let producer = origin(1).produce();
let consumer = producer.consume();
let _deep = producer.announce("room/alice/cam", Route::default()).unwrap();
let mut fut = consumer.routed("room/alice").boxed();
assert!((&mut fut).now_or_never().is_none());
let _exact = producer.announce("room/alice", Route::default()).unwrap();
fut.now_or_never().expect("covered").expect("routed");
}
#[tokio::test]
async fn routed_accepts_a_max_depth_path() {
let producer = origin(1).produce();
let consumer = producer.consume();
let path = (0..Path::MAX_PARTS)
.map(|i| format!("s{i}"))
.collect::<Vec<_>>()
.join("/");
assert_eq!(Path::new(&path).parts().count(), Path::MAX_PARTS);
assert!(consumer.allowed().matches(&path));
let mut fut = consumer.routed(&path).boxed();
assert!((&mut fut).now_or_never().is_none());
let _a = producer.announce("", Route::default()).unwrap();
fut.now_or_never().expect("covered").expect("routed");
}
#[tokio::test]
async fn teardown_ends_everything() {
let (producer, driver) = Producer::new(Config::new(origin(1)));
let consumer = producer.consume();
let _announcement = producer.announce("room", Route::default()).unwrap();
let mut announced = consumer.announced();
announced.assert_next_active("room");
let _server = producer.dynamic("served", Route::default()).unwrap();
let pending = consumer.request_broadcast("served/path");
drop(driver);
announced.assert_next_active("served");
assert!(announced.next().now_or_never().expect("ended").is_none());
assert!(pending.now_or_never().expect("rejected").is_err());
assert!(matches!(producer.announce("x", Route::default()), Err(Error::Closed)));
assert!(matches!(producer.create_broadcast("x"), Err(Error::Closed)));
let err = consumer
.request_broadcast("y")
.now_or_never()
.expect("closed")
.err()
.unwrap();
assert!(matches!(err, Error::Closed));
let mut late = consumer.announced();
assert!(late.next().now_or_never().expect("ended").is_none());
}
struct ResumeRig {
producer: Producer,
resolved: broadcast::Consumer,
subscription: track::Subscriber,
incumbent_track: track::Producer,
}
impl ResumeRig {
async fn new(first: &[u64]) -> (Self, Dynamic, broadcast::Producer) {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer
.dynamic("room", Route::default().with_hops(hops(first)))
.unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
let source = broadcast::Info::new().produce();
let track = source.create_track("video", None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"before".as_ref()).unwrap();
group.finish().unwrap();
request.accept(&source);
let resolved = pending.await.expect("resolves");
let mut subscription = resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = subscription
.recv_group()
.await
.expect("recv group")
.expect("track ended early");
let frame = group.read_frame().await.expect("read frame").expect("frame");
assert_eq!(&frame.payload[..], b"before");
(
Self {
producer,
resolved,
subscription,
incumbent_track: track,
},
server,
source,
)
}
fn standby(&self, first: &[u64]) -> Dynamic {
self.producer
.dynamic("room", Route::default().with_hops(hops(first)))
.unwrap()
}
}
async fn assert_resumes(rig: &mut ResumeRig, server: &Dynamic) {
let request = queued(server).await;
let replacement = broadcast::Info::new().produce();
let track = replacement.create_track("video", None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"before".as_ref()).unwrap();
group.finish().unwrap();
request.accept(&replacement);
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"resumed".as_ref()).unwrap();
group.finish().unwrap();
let mut group = rig
.subscription
.recv_group()
.await
.expect("subscription survives the failover")
.expect("track ended early");
let frame = group.read_frame().await.expect("read frame").expect("frame");
assert_eq!(&frame.payload[..], b"resumed");
}
#[tokio::test]
async fn driver_resolves_with_live_consumers() {
let (producer, driver) = Producer::new(Config::new(origin(1)));
let consumer = producer.consume();
let run = crate::time::run(driver);
drop(producer);
tokio::time::timeout(Duration::from_secs(5), run)
.await
.expect("driver must finish once the producers are gone");
drop(consumer);
}
#[tokio::test]
async fn remote_source_resumes_through_same_first_hop() {
let (mut rig, incumbent, source) = ResumeRig::new(&[10]).await;
let standby_server = rig.standby(&[10, 20]);
drop(incumbent);
drop(source);
assert_resumes(&mut rig, &standby_server).await;
}
#[tokio::test]
async fn incompatible_successor_is_refused() {
for replacement in [
track::Info::default().with_timescale(crate::Timescale::MICRO),
track::Info::default().with_priority(7),
track::Info::default().with_max_age(Duration::from_secs(7)),
] {
let (mut rig, incumbent, source) = ResumeRig::new(&[10]).await;
let standby_server = rig.standby(&[10, 20]);
drop(incumbent);
drop(source);
let request = queued(&standby_server).await;
let successor = broadcast::Info::new().produce();
let track = successor.create_track("video", replacement).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"before".as_ref()).unwrap();
group.finish().unwrap();
request.accept(&successor);
assert!(
matches!(rig.subscription.recv_group().await, Err(Error::Unsupported)),
"the subscription must abort rather than resume onto incompatible metadata"
);
let reopened = rig.resolved.track("video").unwrap();
assert!(matches!(reopened.query().await, Err(Error::Unsupported)));
assert!(matches!(reopened.subscribe(None).await, Err(Error::Unsupported)));
}
}
#[tokio::test]
async fn different_first_hop_ends_the_subscription() {
let (mut rig, incumbent, source) = ResumeRig::new(&[10]).await;
let rival_server = rig.standby(&[11]);
drop(incumbent);
drop(source);
rig.incumbent_track.abort(Error::Dropped).unwrap();
let err = rig.subscription.recv_group().await.err().expect("subscription ends");
assert!(matches!(err, Error::Dropped), "unexpected end: {err}");
let consumer = rig.producer.consume();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&rival_server).await;
let replacement = broadcast::Info::new().produce();
request.accept(&replacement);
pending.await.expect("re-request resolves through the rival");
}
#[tokio::test]
async fn anonymous_routes_never_resume() {
let (mut rig, incumbent, source) = ResumeRig::new(&[]).await;
let _twin_server = rig.standby(&[]);
drop(incumbent);
drop(source);
rig.incumbent_track.abort(Error::Dropped).unwrap();
let err = rig.subscription.recv_group().await.err().expect("subscription ends");
assert!(matches!(err, Error::Dropped), "unexpected end: {err}");
}
#[tokio::test]
async fn anonymous_handoff_serves_the_newcomer_immediately() {
let producer = origin(1).produce();
let server_a = producer
.dynamic("room", Route::default().with_hops(hops(&[10])))
.unwrap();
let consumer = producer.consume().excluding(origin(30));
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server_a).await;
let source_a = broadcast::Info::new().produce();
let track_a = source_a.create_track("video", None).unwrap();
let mut group = track_a.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"from-a".as_ref()).unwrap();
group.finish().unwrap();
request.accept(&source_a);
let resolved_a = pending.await.expect("resolves");
let mut sub_a = resolved_a
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = sub_a
.recv_group()
.await
.expect("recv group")
.expect("track ended early");
assert_eq!(
&group.read_frame().await.expect("read frame").expect("frame").payload[..],
b"from-a"
);
drop(track_a);
drop(source_a);
drop(server_a);
let err = sub_a.recv_group().await.err().expect("front closed");
assert!(matches!(err, Error::Dropped), "unexpected end: {err}");
settle(|| consumer.get_broadcast("room/alice").is_none()).await;
settle(|| {
matches!(
consumer.request_broadcast("room/alice").now_or_never(),
Some(Err(Error::Unroutable))
)
})
.await;
let server_b = producer
.dynamic("room", Route::default().with_hops(hops(&[20])))
.unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server_b).await;
let source_b = broadcast::Info::new().produce();
let track_b = source_b.create_track("video", None).unwrap();
let mut group = track_b.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"from-b".as_ref()).unwrap();
group.finish().unwrap();
request.accept(&source_b);
let resolved_b = pending.await.expect("B's front is served immediately");
assert!(
!resolved_b.is_clone(&resolved_a),
"B must not splice into A's closed front"
);
let mut sub_b = resolved_b
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = sub_b
.recv_group()
.await
.expect("recv group")
.expect("track ended early");
assert_eq!(
&group.read_frame().await.expect("read frame").expect("frame").payload[..],
b"from-b"
);
}
#[tokio::test]
async fn reprice_is_invisible_to_the_subscription() {
let (rig, incumbent, source) = ResumeRig::new(&[10]).await;
incumbent
.update(Route::default().with_hops(hops(&[10])).with_cost(9))
.unwrap();
let track = source.create_track("audio", None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"steady".as_ref()).unwrap();
group.finish().unwrap();
let mut audio = rig
.resolved
.track("audio")
.unwrap()
.subscribe(None)
.await
.expect("subscribe survives the reprice");
let mut group = audio
.recv_group()
.await
.expect("recv group")
.expect("track ended early");
let frame = group.read_frame().await.expect("read frame").expect("frame");
assert_eq!(&frame.payload[..], b"steady");
}
#[tokio::test]
async fn drain_reprice_migrates_before_the_session_dies() {
let (mut rig, incumbent, source) = ResumeRig::new(&[10]).await;
let standby_server = rig.standby(&[10, 20]);
incumbent
.update(Route::default().with_hops(hops(&[10])).with_cost(Cost::DRAIN))
.unwrap();
assert_resumes(&mut rig, &standby_server).await;
drop(incumbent);
drop(source);
}
#[tokio::test]
async fn local_sources_splice_newest_first() {
let producer = origin(1).produce();
let consumer = producer.consume();
let first = producer.publish("room/alice", Route::default()).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let second = producer.publish("room/alice", Route::default()).unwrap();
let again = consumer.request_broadcast("room/alice").await.expect("resolves");
assert!(again.is_clone(&resolved));
first.finish();
settle(|| consumer.get_broadcast("room/alice").is_some()).await;
second.finish();
settle(|| consumer.get_broadcast("room/alice").is_none()).await;
let _third = producer.publish("room/alice", Route::default()).unwrap();
assert!(consumer.get_broadcast("room/alice").is_some());
}
#[tokio::test]
async fn a_finished_broadcast_concludes_in_flight_subscriptions() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let track = broadcast.create_track("video", None).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let mut subscription = resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"tail".as_ref()).unwrap();
group.finish().unwrap();
track.finish().unwrap();
drop(track);
broadcast.finish();
let mut group = next_group(&mut subscription)
.await
.expect("a cleanly finished track was served as an error")
.expect("the track ended before its last group");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"tail");
drop(group);
let end = next_group(&mut subscription)
.await
.expect("a cleanly finished track ended as an error");
assert!(end.is_none(), "a group followed the final one");
}
#[tokio::test]
async fn a_retracted_route_concludes_in_flight_subscriptions() {
let producer = origin(1).produce();
let consumer = producer.consume();
let server = producer
.dynamic("room", Route::default().with_hops(hops(&[10])))
.unwrap();
let pending = consumer.request_broadcast("room/alice");
let request = queued(&server).await;
let source = broadcast::Info::new().produce();
let track = source.create_track("video", None).unwrap();
request.accept(&source);
let resolved = pending.await.expect("resolves");
let mut subscription = resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
source.finish();
drop(server);
settle(|| resolved.is_closed()).await;
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"tail".as_ref()).unwrap();
group.finish().unwrap();
track.finish().unwrap();
drop(track);
let mut group = next_group(&mut subscription)
.await
.expect("a retracted route's track was served as an error")
.expect("the track ended before its last group");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"tail");
drop(group);
let end = next_group(&mut subscription)
.await
.expect("a cleanly finished track ended as an error");
assert!(end.is_none(), "a group followed the final one");
}
#[tokio::test]
async fn origin_front_drops_the_source_when_unused() {
let producer = origin(1).produce();
let consumer = producer.consume();
let broadcast = producer.publish("room/alice", Route::default()).unwrap();
let track = broadcast.create_track("video", None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"cached".as_ref()).unwrap();
group.finish().unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let mut subscription = resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"cached");
drop(group);
drop(subscription);
tokio::time::timeout(Duration::from_secs(1), track.unused())
.await
.expect("source unused should resolve far below TRACK_IDLE_LINGER")
.expect("source closed");
let mut again = resolved
.track("video")
.unwrap()
.subscribe(track::Subscription::default().with_max_age(Duration::from_secs(3600)))
.await
.expect("resubscribe");
let mut group = tokio::time::timeout(Duration::from_secs(1), again.recv_group())
.await
.expect("cached group is still on the front")
.expect("recv group")
.expect("track ended early");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"cached");
tokio::time::timeout(Duration::from_secs(1), track.used())
.await
.expect("returning reader re-splices the source")
.expect("source closed");
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"live".as_ref()).unwrap();
group.finish().unwrap();
let mut group = tokio::time::timeout(Duration::from_secs(1), again.recv_group())
.await
.expect("groups past the cached edge come from the re-splice")
.expect("recv group")
.expect("track ended early");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"live");
}
#[tokio::test]
async fn chained_front_drops_the_source_when_unused() {
let leaf = origin(1).produce();
let leaf_consumer = leaf.consume();
let broadcast = leaf.publish("room/alice", Route::default()).unwrap();
let track = broadcast.create_track("video", None).unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"cached".as_ref()).unwrap();
group.finish().unwrap();
let leaf_front = leaf_consumer.request_broadcast("room/alice").await.expect("resolves");
let mid = origin(2).produce();
let mid_server = mid.dynamic("room", Route::default().with_hops(hops(&[10]))).unwrap();
let mid_pending = mid.consume().request_broadcast("room/alice");
queued(&mid_server).await.accept(&leaf_front);
let mid_resolved = mid_pending.await.expect("mid resolves");
let edge = origin(3).produce();
let edge_server = edge.dynamic("room", Route::default().with_hops(hops(&[20]))).unwrap();
let edge_pending = edge.consume().request_broadcast("room/alice");
queued(&edge_server).await.accept(&mid_resolved);
let edge_resolved = edge_pending.await.expect("edge resolves");
let mut subscription = edge_resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"cached");
drop(group);
drop(subscription);
tokio::time::timeout(Duration::from_secs(5), track.unused())
.await
.expect("chained unused should resolve far below TRACK_IDLE_LINGER")
.expect("source closed");
let cached = edge_resolved.track("video").unwrap().cached_groups();
assert_eq!(
cached.iter().map(|(group, _)| group.sequence).collect::<Vec<_>>(),
vec![0],
"every front keeps the delivered groups after releasing its source"
);
let mut subscription = edge_resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("resubscribe");
tokio::time::timeout(Duration::from_secs(5), track.used())
.await
.expect("resubscribe should reach the leaf")
.expect("source open");
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"live".as_ref()).unwrap();
group.finish().unwrap();
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"cached");
drop(group);
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"live");
drop(group);
drop(subscription);
tokio::time::timeout(Duration::from_secs(5), track.unused())
.await
.expect("second chained unused should resolve far below TRACK_IDLE_LINGER")
.expect("source closed");
let cached = edge_resolved.track("video").unwrap().cached_groups();
assert_eq!(
cached.iter().map(|(group, _)| group.sequence).collect::<Vec<_>>(),
vec![0, 1],
"repeated demand keeps every complete group while releasing its source"
);
let fetch = edge_resolved.track("video").unwrap().fetch_group(2, None);
let mut fetch = std::pin::pin!(fetch);
assert!(futures::poll!(fetch.as_mut()).is_pending(), "fetch should re-splice");
tokio::time::timeout(Duration::from_secs(5), track.used())
.await
.expect("fetch should reach the leaf")
.expect("source open");
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"fetched".as_ref()).unwrap();
group.finish().unwrap();
let mut group = tokio::time::timeout(Duration::from_secs(5), fetch)
.await
.expect("re-spliced source should answer the fetch")
.expect("fetch succeeds");
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"fetched");
}
#[tokio::test]
async fn incompatible_local_source_keeps_the_incumbent() {
let producer = origin(1).produce();
let consumer = producer.consume();
let first = producer.publish("room/alice", Route::default()).unwrap();
let track = first.create_track("video", None).unwrap();
let resolved = consumer.request_broadcast("room/alice").await.expect("resolves");
let mut subscription = resolved
.track("video")
.unwrap()
.subscribe(None)
.await
.expect("subscribe");
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"before".as_ref()).unwrap();
group.finish().unwrap();
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"before");
let second = producer.publish("room/alice", Route::default()).unwrap();
let _incompatible = second
.create_track("video", track::Info::default().with_timescale(crate::Timescale::MICRO))
.unwrap();
for _ in 0..10 {
tokio::task::yield_now().await;
}
let mut group = track.append_group().unwrap();
group.write_frame(crate::Timestamp::ZERO, b"still".as_ref()).unwrap();
group.finish().unwrap();
let mut group = subscription.recv_group().await.unwrap().unwrap();
assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"still");
drop(track);
first.finish();
assert!(matches!(subscription.recv_group().await, Err(Error::Unsupported)));
}
#[tokio::test]
async fn multiple_scopes_present_one_broad_prefix() {
let producer = origin(1).produce();
let _a = producer.announce("", Route::default()).unwrap();
let consumer = producer.consume().scope("", &scopes(&["alpha", "beta"])).unwrap();
let mut announced = consumer.announced();
announced.assert_next_active("");
announced.assert_next_wait();
}
#[test]
fn scope_accepts_every_pattern_union() {
let producer = origin(1).produce();
let root = producer.scope("", &Patterns::from(Pattern::all())).unwrap();
assert_eq!(root.allowed(), Patterns::from(Pattern::all()));
let scoped = producer.scope("", &scopes(&["room"])).unwrap();
assert_eq!(scoped.allowed(), scopes(&["room"]));
let multi = producer.scope("", &scopes(&["room", "room/chat", "anon"])).unwrap();
assert_eq!(multi.allowed(), scopes(&["room", "anon"]));
let consumer = producer.consume().scope("", &scopes(&["room"])).unwrap();
assert_eq!(consumer.allowed(), scopes(&["room"]));
for text in ["room", "", "*room", "room/*", "*", "**/room", "room/**/chat", "*.hang"] {
let union = Patterns::from(text.parse::<Pattern>().unwrap());
assert_eq!(producer.scope("", &union).expect(text).allowed(), union, "{text}");
assert_eq!(
producer.consume().scope("", &union).expect(text).allowed(),
union,
"{text}"
);
}
let mixed: Patterns = ["room/**".parse().unwrap(), "other".parse().unwrap()]
.into_iter()
.collect();
assert_eq!(producer.scope("", &mixed).unwrap().allowed(), mixed);
}
#[test]
fn route_table_prunes_to_empty() {
let producer = origin(1).produce();
let consumer = producer.consume();
let cursor = consumer
.scope("", &scopes(&["room/a", "other/deep/head"]))
.unwrap()
.announced();
let route = producer.announce("room/a/b/c", Route::default()).unwrap();
{
let table = producer.shared.lock();
assert!(table.routes.root.find(Path::new("room/a/b/c").parts()).is_some());
assert!(table.routes.root.find(Path::new("other/deep/head").parts()).is_some());
assert_eq!(table.routes.root.cursors_below, 2);
}
drop(route);
drop(cursor);
let table = producer.shared.lock();
assert!(table.routes.root.is_empty());
assert_eq!(table.routes.root.cursors_below, 0);
}
#[test]
fn a_published_broadcast_keeps_the_driver_running() {
let (producer, mut driver) = Producer::new(Config::new(origin(1)));
let waiter = kio::Waiter::noop();
let broadcast = producer.create_broadcast("room/a").unwrap();
drop(producer);
assert!(
driver.poll(Instant::now(), &waiter).is_ok(),
"the broadcast is lifecycle work"
);
drop(broadcast);
assert!(matches!(driver.poll(Instant::now(), &waiter), Err(Error::Closed)));
}
#[test]
fn watch_wakes_only_for_covering_changes() {
let producer = origin(1).produce();
let waiter = kio::Waiter::noop();
let watch = producer.shared.lock().watch(&producer.shared, &Path::new("room/a"));
let seen = watch.seen();
let _other = producer.announce("other", Route::default()).unwrap();
let _below = producer.announce("room/a/b", Route::default()).unwrap();
assert!(watch.poll_changed(&waiter, seen).is_pending());
let above = producer.announce("room", Route::default()).unwrap();
assert!(watch.poll_changed(&waiter, seen).is_ready());
let seen = watch.seen();
drop(above);
assert!(watch.poll_changed(&waiter, seen).is_ready());
let seen = watch.seen();
let _beside = producer.create_broadcast("room/b").unwrap();
assert!(watch.poll_changed(&waiter, seen).is_pending());
let _here = producer.create_broadcast("room/a").unwrap();
assert!(watch.poll_changed(&waiter, seen).is_ready());
drop(watch);
let table = producer.shared.lock();
let node = table
.routes
.root
.find(Path::new("room/a").parts())
.expect("route below keeps the node");
assert!(node.watches.is_empty());
assert_eq!(table.routes.root.watches_below, 0);
}
#[test]
fn a_discarded_front_task_unregisters_its_watch() {
let (producer, _driver) = Producer::new(Config {
hop: origin(1),
..Default::default()
});
let consumer = producer.consume();
let _served = producer.dynamic("room", Route::default()).unwrap();
drop(producer);
let _pending = consumer.request_broadcast("room/a");
}
#[test]
fn create_broadcast_refuses_a_path_no_pattern_can_spell() {
let producer = origin(1).produce();
assert!(matches!(
producer.create_broadcast("room/*"),
Err(Error::InvalidPath(_))
));
assert!(matches!(
producer.announce("room/**", Route::default()),
Err(Error::InvalidPath(_))
));
}
#[test]
fn scope_empty_union_grants_nothing() {
let producer = origin(1).produce();
assert!(matches!(producer.scope("", &Patterns::new()), Err(Error::Unauthorized)));
assert!(matches!(
producer.consume().scope("", &Patterns::new()),
Err(Error::Unauthorized)
));
}
#[test]
fn scope_nests_and_rebases_roots() {
let producer = origin(1).produce();
let scoped = producer.scope("", &scopes(&["room"])).unwrap();
let nested = scoped.scope("", &scopes(&["room/chat"])).unwrap();
assert_eq!(nested.allowed(), scopes(&["room/chat"]));
assert!(matches!(
scoped.scope("", &scopes(&["other"])),
Err(Error::Unauthorized)
));
let rooted = nested.scope("room/chat", &Patterns::from(Pattern::all())).unwrap();
assert_eq!(rooted.allowed(), scopes(&[""]));
let broadcast = nested.create_broadcast("room/chat/live").unwrap();
assert!(producer.consume().get_broadcast("room/chat/live").is_some());
broadcast.finish();
}
#[test]
fn scope_intersects_and_rebases_arbitrary_grants() {
let producer = origin(1).produce();
let rooms = producer
.scope("", &Patterns::from("room/*".parse::<Pattern>().unwrap()))
.unwrap();
let chats = rooms
.scope("", &Patterns::from("*/chat".parse::<Pattern>().unwrap()))
.unwrap();
assert_eq!(chats.allowed(), Patterns::from("room/chat".parse::<Pattern>().unwrap()));
let exact = producer
.scope("", &Patterns::from("room/alice".parse::<Pattern>().unwrap()))
.unwrap();
let rooted = exact.scope("room", &Patterns::from(Pattern::all())).unwrap();
assert_eq!(rooted.allowed(), Patterns::from("alice".parse::<Pattern>().unwrap()));
assert!(matches!(
exact.scope("room/bob", &Patterns::from(Pattern::all())),
Err(Error::Unauthorized)
));
let broadcast = exact.create_broadcast("room/alice").unwrap();
assert!(matches!(
exact.create_broadcast("room/alice/cam"),
Err(Error::Unauthorized)
));
assert!(producer.consume().get_broadcast("room/alice").is_some());
drop(broadcast);
}
#[tokio::test]
async fn wildcard_scope_filters_announcements_and_reports_captures() {
let producer = origin(1).produce();
let consumer = producer
.consume()
.scope("", &Patterns::from("room/*/chat".parse::<Pattern>().unwrap()))
.unwrap();
let mut announced = consumer.announced();
let alice = producer.create_broadcast("room/alice/chat").unwrap();
alice.announce(Route::default()).unwrap();
let update = announced.try_next().expect("alice's chat");
assert_eq!(update.prefix.as_str(), "room/alice/chat");
assert_eq!(update.captures, Some(vec!["alice".parse::<Pattern>().unwrap()]));
let audio = producer.create_broadcast("room/alice/audio").unwrap();
audio.announce(Route::default()).unwrap();
announced.assert_next_wait();
let broad = producer.announce("room", Route::default()).unwrap();
let update = announced.try_next().expect("overlapping broad route");
assert_eq!(update.prefix.as_str(), "room");
assert_eq!(update.captures, None, "an overlap does not pin the wildcard");
drop(broad);
drop(audio);
drop(alice);
}
#[tokio::test]
async fn local_broadcast_wins_announcement_ties() {
let producer = origin(1).produce();
let remote = producer.announce("room/alice", Route::default().with_cost(9)).unwrap();
let local = producer.create_broadcast("room/alice").unwrap();
local.announce(Route::default()).unwrap();
let mut announced = producer.consume().announced();
let update = announced.try_next().expect("one winning route");
assert_eq!(update.prefix.as_str(), "room/alice");
assert_eq!(update.route.cost, Cost::default());
announced.assert_next_wait();
drop(local);
drop(remote);
}
#[test]
fn cost_charge_saturates() {
assert_eq!(Cost { warm: 4, cold: 6 }.charged(5), Cost { warm: 9, cold: 11 });
assert_eq!(Cost::new(u64::MAX).charged(10), Cost::new(MAX_COST));
assert_eq!(Cost::UNKNOWN.charged(3).cold, MAX_COST);
}
fn expiring_origin(expiry: Duration) -> Producer {
let pool = cache::Pool::new(cache::Config::default().with_expiry(expiry));
Config {
pool,
..Config::default()
}
.produce()
}
#[tokio::test(start_paused = true)]
async fn stalled_publisher_open_group_is_reclaimed() {
let expiry = Duration::from_secs(1);
let origin = expiring_origin(expiry);
let broadcast = origin.create_broadcast("test").unwrap();
let track = broadcast.create_track("video", None).unwrap();
let mut stalled = track.append_group().unwrap();
stalled.write_frame(crate::Timestamp::ZERO, b"x".as_slice()).unwrap();
let _successor = track.append_group().unwrap();
let mut reading = stalled.consume();
assert!(reading.read_frame().await.unwrap().is_some());
crate::model::clock::advance(expiry * 2);
let reclaimed = tokio::time::timeout(Duration::from_secs(60), reading.read_frame()).await;
assert!(
matches!(reclaimed, Ok(Err(Error::Old))),
"the sweep must reclaim an idle open group and surface the gap, got {reclaimed:?}"
);
}
#[tokio::test(start_paused = true)]
async fn sweep_respects_a_disabled_expiry() {
let origin = Config {
pool: cache::Pool::unbounded(),
..Config::default()
}
.produce();
let broadcast = origin.create_broadcast("test").unwrap();
let track = broadcast.create_track("video", None).unwrap();
let mut stalled = track.append_group().unwrap();
stalled.write_frame(crate::Timestamp::ZERO, b"x".as_slice()).unwrap();
let _successor = track.append_group().unwrap();
let mut reading = stalled.consume();
assert!(reading.read_frame().await.unwrap().is_some());
crate::model::clock::advance(Duration::from_secs(3600));
tokio::time::advance(Duration::from_secs(3600)).await;
assert!(
reading.read_frame().now_or_never().is_none(),
"a pool without an expiry window never reclaims"
);
}
#[test]
fn drain_cost_is_encodable() {
use crate::coding::Encode;
let mut buf = Vec::new();
Cost::DRAIN
.encode(&mut buf, crate::lite::Version::Lite06)
.expect("a draining route is still forwarded, so its cost must encode");
}
}