use std::{
collections::HashMap,
fmt,
pin::Pin,
sync::{Arc, OnceLock},
task::{Context, Poll},
time::Duration,
};
use futures_util::{Stream, StreamExt};
use polyoxide_ws::{MembershipHandle as PathHandle, Supervised, Supervisor, Timing};
use tokio::sync::{mpsc, oneshot, watch};
use crate::usdm::ws::{
error::{StopCause, UsdmWsError},
event::Update,
protocol::{Change, UsdmProtocol},
stream::StreamName,
StreamPath, MAX_STREAMS_PER_CONNECTION, USDM_WS_BASE,
};
const DEFAULT_MAX_CONNECTION_AGE: Duration = Duration::from_secs(23 * 3600 + 50 * 60);
const EVENT_BUFFER: usize = 1024;
#[derive(Debug)]
#[non_exhaustive]
pub enum Event {
Update(Box<Update>),
Disconnected {
path: StreamPath,
reason: DisconnectReason,
},
Reconnected {
path: StreamPath,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DisconnectReason {
Closed {
code: Option<u16>,
reason: String,
},
Error(UsdmWsError),
Stale,
Rotation,
}
impl fmt::Display for DisconnectReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Closed { code, reason } => match code {
Some(code) => write!(f, "closed by the server ({code} {reason})"),
None => f.write_str("closed by the server"),
},
Self::Error(err) => write!(f, "{err}"),
Self::Stale => f.write_str("no frame within the staleness window"),
Self::Rotation => f.write_str("rotated before the 24-hour cutoff"),
}
}
}
#[derive(Debug, Clone)]
pub struct UsdmWsBuilder {
base_url: String,
timing: Timing,
streams: Vec<StreamName>,
}
impl Default for UsdmWsBuilder {
fn default() -> Self {
Self::new()
}
}
impl UsdmWsBuilder {
pub fn new() -> Self {
let mut timing = Timing::default();
timing.max_connection_age = Some(DEFAULT_MAX_CONNECTION_AGE);
Self {
base_url: USDM_WS_BASE.to_owned(),
timing,
streams: Vec::new(),
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
polyoxide_ws::timing_setters! {
ping_interval;
stale_after;
backoff;
connect_timeout;
max_connection_age;
}
pub fn streams(mut self, streams: impl IntoIterator<Item = StreamName>) -> Self {
self.streams = streams.into_iter().collect();
self
}
pub async fn connect(self) -> Result<SupervisedUsdmWs, UsdmWsError> {
let mut membership: HashMap<StreamPath, Vec<StreamName>> = HashMap::new();
for stream in self.streams.iter().cloned() {
let wanted = membership.entry(stream.path()).or_default();
if !wanted.contains(&stream) {
wanted.push(stream);
}
}
for (path, wanted) in &membership {
if wanted.len() > MAX_STREAMS_PER_CONNECTION {
return Err(UsdmWsError::TooManyStreams {
path: *path,
limit: MAX_STREAMS_PER_CONNECTION,
});
}
}
let mut opened: Vec<(StreamPath, Supervised<UsdmProtocol>)> = Vec::new();
for path in StreamPath::ALL {
let Some(wanted) = membership.get(path) else {
continue;
};
match self.supervisor(*path).connect(wanted.clone()).await {
Ok(supervised) => opened.push((*path, supervised)),
Err(err) => {
for (_, supervised) in opened {
let _ = supervised.close().await;
}
return Err(err);
}
}
}
let (events, events_rx) = mpsc::channel(EVENT_BUFFER);
let (commands, commands_rx) = mpsc::channel(16);
let (ended, ended_rx) = mpsc::unbounded_channel();
let (closing, _) = watch::channel(false);
let stop = Stop::default();
let mut router = Router {
config: self,
paths: HashMap::new(),
membership,
events,
closing,
ended,
forwarders: Vec::new(),
generation: 0,
stop: stop.clone(),
};
for (path, supervised) in opened {
router.adopt(path, supervised);
}
let task = tokio::spawn(route(router, commands_rx, ended_rx));
Ok(SupervisedUsdmWs {
events: events_rx,
commands,
task,
stop,
})
}
fn supervisor(&self, path: StreamPath) -> Supervisor<UsdmProtocol> {
Supervisor::new(UsdmProtocol::new(self.base_url.clone(), path), self.timing)
}
}
type Reply = oneshot::Sender<Result<(), UsdmWsError>>;
enum Command {
Subscribe(Vec<StreamName>, Reply),
Unsubscribe(Vec<StreamName>, Reply),
Close,
}
#[derive(Debug, Clone)]
pub struct MembershipHandle {
commands: mpsc::Sender<Command>,
stop: Stop,
}
impl MembershipHandle {
pub async fn subscribe(
&self,
streams: impl IntoIterator<Item = StreamName>,
) -> Result<(), UsdmWsError> {
self.send(|reply| Command::Subscribe(streams.into_iter().collect(), reply))
.await
}
pub async fn unsubscribe(
&self,
streams: impl IntoIterator<Item = StreamName>,
) -> Result<(), UsdmWsError> {
self.send(|reply| Command::Unsubscribe(streams.into_iter().collect(), reply))
.await
}
async fn send(&self, make: impl FnOnce(Reply) -> Command) -> Result<(), UsdmWsError> {
let (reply_tx, reply_rx) = oneshot::channel();
self.commands
.send(make(reply_tx))
.await
.map_err(|_| self.stop.error())?;
reply_rx.await.map_err(|_| self.stop.error())?
}
}
#[derive(Debug, Clone, Default)]
struct Stop(Arc<OnceLock<StopCause>>);
impl Stop {
fn set(&self, cause: StopCause) {
let _ = self.0.set(cause);
}
fn error(&self) -> UsdmWsError {
UsdmWsError::Stopped {
cause: self.0.get().cloned(),
}
}
}
pub struct SupervisedUsdmWs {
events: mpsc::Receiver<Item>,
commands: mpsc::Sender<Command>,
task: tokio::task::JoinHandle<Result<(), UsdmWsError>>,
stop: Stop,
}
impl fmt::Debug for SupervisedUsdmWs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SupervisedUsdmWs")
.field("running", &!self.task.is_finished())
.finish_non_exhaustive()
}
}
impl SupervisedUsdmWs {
pub fn membership(&self) -> MembershipHandle {
MembershipHandle {
commands: self.commands.clone(),
stop: self.stop.clone(),
}
}
pub async fn close(self) -> Result<(), UsdmWsError> {
let Self {
events,
commands,
task,
stop,
} = self;
drop(events);
let _ = commands.send(Command::Close).await;
drop(commands);
match task.await {
Ok(result) => result,
Err(_) => Err(stop.error()),
}
}
}
impl Stream for SupervisedUsdmWs {
type Item = Result<Event, UsdmWsError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.events.poll_recv(cx)
}
}
type Item = Result<Event, UsdmWsError>;
type Events = mpsc::Sender<Item>;
struct Path {
generation: u64,
handle: PathHandle<UsdmProtocol>,
}
struct Ended {
path: StreamPath,
generation: u64,
stopped: bool,
cause: Option<StopCause>,
}
struct Router {
config: UsdmWsBuilder,
paths: HashMap<StreamPath, Path>,
membership: HashMap<StreamPath, Vec<StreamName>>,
events: Events,
closing: watch::Sender<bool>,
ended: mpsc::UnboundedSender<Ended>,
forwarders: Vec<tokio::task::JoinHandle<()>>,
generation: u64,
stop: Stop,
}
impl Router {
fn adopt(&mut self, path: StreamPath, supervised: Supervised<UsdmProtocol>) {
self.generation += 1;
let handle = supervised.membership();
self.forwarders.retain(|forwarder| !forwarder.is_finished());
self.forwarders.push(tokio::spawn(forward(
path,
self.generation,
supervised,
self.events.clone(),
self.closing.subscribe(),
self.ended.clone(),
)));
self.paths.insert(
path,
Path {
generation: self.generation,
handle,
},
);
}
fn ended(&mut self, ended: Ended) -> Result<(), UsdmWsError> {
if ended.stopped {
if let Some(cause) = ended.cause {
self.stop.set(cause);
}
return Err(self.stop.error());
}
if self
.paths
.get(&ended.path)
.is_some_and(|path| path.generation == ended.generation)
{
self.paths.remove(&ended.path);
self.membership.remove(&ended.path);
}
Ok(())
}
async fn subscribe(&mut self, streams: Vec<StreamName>) -> Result<(), UsdmWsError> {
let by_path = new_by_path(&self.membership, streams);
for (path, add) in &by_path {
let held = self.membership.get(path).map_or(0, Vec::len);
if held + add.len() > MAX_STREAMS_PER_CONNECTION {
return Err(UsdmWsError::TooManyStreams {
path: *path,
limit: MAX_STREAMS_PER_CONNECTION,
});
}
}
for path in StreamPath::ALL {
let Some(add) = by_path.get(path) else {
continue;
};
match self.paths.get(path) {
Some(slot) => {
let handle = slot.handle.clone();
self.change(*path, &handle, Change::Subscribe(add.clone()))
.await?;
}
None => self.open(*path, add.clone()).await?,
}
}
Ok(())
}
async fn unsubscribe(&mut self, streams: Vec<StreamName>) -> Result<(), UsdmWsError> {
let mut by_path: HashMap<StreamPath, Vec<StreamName>> = HashMap::new();
for stream in streams {
let path = stream.path();
if self
.membership
.get(&path)
.is_some_and(|m| m.contains(&stream))
{
let batch = by_path.entry(path).or_default();
if !batch.contains(&stream) {
batch.push(stream);
}
}
}
for path in StreamPath::ALL {
let (Some(remove), Some(slot)) = (by_path.get(path), self.paths.get(path)) else {
continue;
};
let handle = slot.handle.clone();
self.change(*path, &handle, Change::Unsubscribe(remove.clone()))
.await?;
}
Ok(())
}
async fn open(
&mut self,
path: StreamPath,
streams: Vec<StreamName>,
) -> Result<(), UsdmWsError> {
let supervised = self.config.supervisor(path).start(streams.clone()).await?;
self.adopt(path, supervised);
self.membership.insert(path, streams);
Ok(())
}
async fn change(
&mut self,
path: StreamPath,
handle: &PathHandle<UsdmProtocol>,
change: Change,
) -> Result<(), UsdmWsError> {
match handle.apply(change.clone()).await {
Ok(streams) => {
self.settle(path, streams);
Ok(())
}
Err(UsdmWsError::NoAnswer { .. }) => {
let mut streams = self.membership.get(&path).cloned().unwrap_or_default();
match change {
Change::Subscribe(add) => streams.extend(add),
Change::Unsubscribe(remove) => streams.retain(|s| !remove.contains(s)),
}
self.settle(path, streams);
Ok(())
}
Err(err @ UsdmWsError::Refused { .. }) => {
if let Ok(streams) = handle.apply(Change::Subscribe(Vec::new())).await {
self.settle(path, streams);
}
Err(err)
}
Err(err) => Err(err),
}
}
fn settle(&mut self, path: StreamPath, streams: Vec<StreamName>) {
if streams.is_empty() {
self.membership.remove(&path);
self.paths.remove(&path);
} else {
self.membership.insert(path, streams);
}
}
async fn close(self) {
let _ = self.closing.send(true);
for forwarder in self.forwarders {
let _ = forwarder.await;
}
}
}
fn new_by_path(
membership: &HashMap<StreamPath, Vec<StreamName>>,
streams: Vec<StreamName>,
) -> HashMap<StreamPath, Vec<StreamName>> {
let mut by_path: HashMap<StreamPath, Vec<StreamName>> = HashMap::new();
for stream in streams {
let path = stream.path();
let known = membership.get(&path).is_some_and(|m| m.contains(&stream));
let batch = by_path.entry(path).or_default();
if !known && !batch.contains(&stream) {
batch.push(stream);
}
}
by_path.retain(|_, batch| !batch.is_empty());
by_path
}
async fn route(
mut router: Router,
mut commands: mpsc::Receiver<Command>,
mut ended: mpsc::UnboundedReceiver<Ended>,
) -> Result<(), UsdmWsError> {
let events = router.events.clone();
let result = loop {
tokio::select! {
biased;
Some(path) = ended.recv() => {
if let Err(err) = router.ended(path) {
break Err(err);
}
}
() = events.closed() => break Ok(()),
command = commands.recv() => match command {
None | Some(Command::Close) => break Ok(()),
Some(Command::Subscribe(streams, reply)) => {
let _ = reply.send(router.subscribe(streams).await);
}
Some(Command::Unsubscribe(streams, reply)) => {
let _ = reply.send(router.unsubscribe(streams).await);
}
},
}
};
drop(events);
router.close().await;
result
}
async fn forward(
path: StreamPath,
generation: u64,
mut supervised: Supervised<UsdmProtocol>,
events: Events,
mut closing: watch::Receiver<bool>,
ended: mpsc::UnboundedSender<Ended>,
) {
let mut last_error = None;
loop {
let next = tokio::select! {
biased;
_ = closing.wait_for(|closing| *closing) => break,
() = events.closed() => break,
next = supervised.next() => next,
};
let Some(item) = next else {
let stopped = supervised.close().await.is_err();
let _ = ended.send(Ended {
path,
generation,
stopped,
cause: last_error.filter(|_| stopped),
});
return;
};
last_error = item.as_ref().err().map(StopCause::of);
let slot = tokio::select! {
biased;
_ = closing.wait_for(|closing| *closing) => break,
slot = events.reserve() => slot,
};
match slot {
Ok(slot) => slot.send(item),
Err(_) => break,
}
}
let _ = supervised.close().await;
}
#[cfg(test)]
mod tests {
use polyoxide_ws::Backoff;
use super::*;
#[test]
fn defaults_match_the_documented_cadences() {
let b = UsdmWsBuilder::new();
assert_eq!(b.base_url, USDM_WS_BASE);
assert_eq!(b.timing.ping_interval, Some(Duration::from_secs(20)));
assert_eq!(b.timing.stale_after, Duration::from_secs(30));
assert_eq!(b.timing.initial_backoff, Duration::from_millis(500));
assert_eq!(b.timing.max_backoff, Duration::from_secs(60));
assert_eq!(b.timing.connect_timeout, Duration::from_secs(10));
assert_eq!(
b.timing.max_connection_age,
Some(Duration::from_secs(85_800))
);
assert!(b.timing.max_connection_age < Some(Duration::from_secs(24 * 3600)));
}
#[test]
fn backoff_doubles_to_the_ceiling_and_resets_after_a_working_connection() {
let mut b = Backoff::new(Duration::from_millis(100), Duration::from_millis(350));
assert_eq!(b.take(), Duration::from_millis(100));
assert_eq!(b.take(), Duration::from_millis(200));
assert_eq!(b.take(), Duration::from_millis(350));
b.after_connection_ended(true);
assert_eq!(b.take(), Duration::from_millis(100));
b.after_connection_ended(false);
assert_eq!(b.take(), Duration::from_millis(200));
}
#[test]
fn a_zero_initial_delay_still_grows_and_a_huge_one_saturates() {
let mut b = Backoff::new(Duration::ZERO, Duration::from_secs(1));
assert_eq!(b.take(), Duration::from_millis(1));
assert_eq!(b.take(), Duration::from_millis(2));
let mut b = Backoff::new(Duration::MAX, Duration::MAX);
assert_eq!(b.take(), Duration::MAX);
assert_eq!(b.take(), Duration::MAX);
}
}