mod events;
mod message;
mod router;
mod subscription;
mod updates;
pub use events::{
ClosedReason, Connected, Continuity, DisconnectReason, Recovery, RecoveryOutcome, Resubscribed,
ServerInfo, SessionEvent, SessionEvents, StateValidity, SubscriptionId,
};
pub use message::{Message, MessageOutcome, MessageResult, SequenceName};
pub use subscription::{
BufferSize, FieldSchema, FrequencyLimit, ItemGroup, MaxFrequency, Snapshot, Subscription,
SubscriptionMode,
};
pub use updates::{CommandFields, Filtering, SubscriptionEvent, UpdateFrequency, Updates};
use tokio::sync::{mpsc, oneshot};
use crate::config::ClientConfig;
use crate::error::{Error, Result};
use crate::session::{self, SessionHandle};
use self::events::SessionEvents as SessionEventStream;
use self::router::{Router, RouterCommand};
static NEXT_CLIENT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ClientId(u64);
impl ClientId {
#[cfg(feature = "test-util")]
pub(crate) const DETACHED: Self = Self(0);
fn allocate() -> Result<Self> {
use std::sync::atomic::Ordering::Relaxed;
NEXT_CLIENT_ID
.fetch_update(Relaxed, Relaxed, |current| current.checked_add(1))
.map(Self)
.map_err(|_| Error::Internal {
reason: "this process has run out of client identities".to_owned(),
})
}
pub(crate) const fn get(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy)]
struct Capacities {
update: std::num::NonZeroUsize,
session_event: std::num::NonZeroUsize,
shutdown_timeout: std::time::Duration,
}
fn stage(staged: &mut std::collections::VecDeque<SessionEvent>, event: SessionEvent, limit: usize) {
if staged.len() < limit {
staged.push_back(event);
return;
}
tracing::warn!(
limit,
"discarding a session event produced before connect() returned"
);
}
#[derive(Debug)]
pub struct Client {
id: ClientId,
handle: SessionHandle,
router: mpsc::UnboundedSender<RouterCommand>,
update_capacity: std::num::NonZeroUsize,
shutdown_timeout: std::time::Duration,
tasks: Vec<tokio::task::JoinHandle<()>>,
stop_on_drop: bool,
_alive: mpsc::Sender<()>,
}
impl Drop for Client {
fn drop(&mut self) {
if self.stop_on_drop {
self.handle.stop();
}
}
}
impl Client {
#[tracing::instrument(skip(config), fields(address = %config.address(), transport = config.transport().as_str()))]
pub async fn connect(config: ClientConfig) -> Result<(Self, SessionEvents)> {
let capacities = Capacities {
update: config.options().update_capacity(),
session_event: config.options().session_event_capacity(),
shutdown_timeout: config.options().open_timeout(),
};
let parts = session::connect_configured(config)?;
Self::assemble(parts, capacities).await
}
async fn assemble<T>(
parts: (
session::SessionDriver<T>,
SessionHandle,
mpsc::Receiver<session::SessionEvent>,
),
capacities: Capacities,
) -> Result<(Self, SessionEvents)>
where
T: session::Transport + Send + 'static,
{
let Capacities {
update: update_capacity,
session_event: session_capacity,
shutdown_timeout,
} = capacities;
let id = ClientId::allocate()?;
let (driver, handle, session_events) = parts;
let (router_tx, router_rx) = mpsc::unbounded_channel();
let (unsubscribe_tx, unsubscribe_rx) = mpsc::unbounded_channel();
let (session_out_tx, mut session_out_rx) = mpsc::channel(session_capacity.get());
let (alive_tx, alive_rx) = mpsc::channel(1);
let (ready_tx, ready_rx) = oneshot::channel();
let router = Router::new(
id,
session_events,
router_rx,
session_out_tx,
unsubscribe_tx,
ready_tx,
handle.stop_signal(),
);
let tasks = vec![
tokio::spawn(async move {
let closed = driver.run().await;
tracing::debug!(?closed, "session driver stopped");
}),
tokio::spawn(router.run()),
tokio::spawn(router::issue_unsubscriptions(
handle.clone(),
unsubscribe_rx,
alive_rx,
handle.stop_signal(),
)),
];
let mut staged = std::collections::VecDeque::new();
tokio::pin!(ready_rx);
let outcome = loop {
tokio::select! {
biased;
result = &mut ready_rx => break result,
event = session_out_rx.recv() => match event {
Some(event) => stage(&mut staged, event, session_capacity.get()),
None => break Ok(Err(Error::Disconnected)),
},
}
};
let connected = match outcome {
Ok(Ok(connected)) => connected,
Ok(Err(error)) => return Err(error),
Err(_) => return Err(Error::Disconnected),
};
tracing::info!(
client = id.get(),
keepalive = ?connected.keepalive,
request_limit_bytes = connected.request_limit_bytes,
"session established"
);
Ok((
Self {
id,
handle,
router: router_tx,
update_capacity,
shutdown_timeout,
tasks,
stop_on_drop: true,
_alive: alive_tx,
},
SessionEventStream::with_staged(staged, session_out_rx),
))
}
#[tracing::instrument(skip(self, subscription), fields(mode = subscription.mode().as_str()))]
pub async fn subscribe(&self, subscription: Subscription) -> Result<Updates> {
subscription.validate()?;
let (events_tx, events_rx) = mpsc::channel(self.update_capacity.get());
let key = self
.handle
.allocate_key()
.map_err(|error| Error::Internal {
reason: error.to_string(),
})?;
let id = SubscriptionId::new(self.id, key);
self.router
.send(RouterCommand::Register {
id,
events: events_tx,
})
.map_err(|_| Error::Disconnected)?;
if let Err(error) = self
.handle
.subscribe_with_key(key, subscription.into_spec())
.await
{
tracing::debug!(id = id.get(), %error, "the subscription could not be queued");
let _ = self.router.send(RouterCommand::Unregister { id });
return Err(Error::Disconnected);
}
tracing::debug!(id = id.get(), "subscribed");
Ok(Updates::new(id, events_rx, self.router.clone()))
}
pub async fn unsubscribe(&self, id: SubscriptionId) -> Result<()> {
if id.client() != self.id {
return Err(Error::ForeignSubscription { id: id.get() });
}
self.handle
.unsubscribe(id.key())
.await
.map_err(|_| Error::Disconnected)
}
#[tracing::instrument(skip(self, message))]
pub async fn send_message(&self, message: Message) -> Result<()> {
message.validate()?;
self.handle
.send_message(message.into_outgoing())
.await
.map_err(|_| Error::Disconnected)
}
#[tracing::instrument(skip(self))]
pub async fn disconnect(mut self) -> Result<()> {
self.stop_on_drop = false;
let budget = self.shutdown_timeout;
let mut tasks = std::mem::take(&mut self.tasks);
let handle = self.handle.clone();
let asked = match tokio::time::timeout(budget, self.handle.destroy(None)).await {
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => Err(Error::Disconnected),
Err(_) => Err(Error::Internal {
reason: "the session did not accept the destroy request in time".to_owned(),
}),
};
drop(self);
let mut outcome = asked;
if tokio::time::timeout(budget, join_all(&mut tasks))
.await
.is_err()
{
tracing::warn!(
?budget,
"the session did not shut down in time; stopping it"
);
handle.stop();
if tokio::time::timeout(budget, join_all(&mut tasks))
.await
.is_err()
{
for task in &tasks {
task.abort();
}
join_all(&mut tasks).await;
}
if outcome.is_ok() {
outcome = Err(Error::Internal {
reason: "the session did not shut down within the configured budget".to_owned(),
});
}
}
drop(handle);
outcome
}
}
async fn join_all(tasks: &mut Vec<tokio::task::JoinHandle<()>>) {
while let Some(task) = tasks.last_mut() {
if let Err(error) = task.await
&& !error.is_cancelled()
{
tracing::warn!(%error, "a client task did not finish cleanly");
}
tasks.pop();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use futures_util::{FutureExt, StreamExt};
use tokio::sync::{mpsc, oneshot};
use super::*;
use crate::client::events::Connected;
use crate::client::router::Router;
use crate::session::{
BindKind, BoundInfo, SessionClosed as WireClosed, SessionEvent as WireEvent, SessionId,
SubscriptionError, SubscriptionEvent as WireSubscriptionEvent,
};
use std::time::Duration;
fn a_client_id() -> ClientId {
match ClientId::allocate() {
Ok(id) => id,
Err(error) => panic!("this process has client identities left: {error}"),
}
}
struct Harness {
client: ClientId,
session_tx: mpsc::Sender<WireEvent>,
commands: mpsc::UnboundedSender<RouterCommand>,
session_events: SessionEvents,
ready: oneshot::Receiver<Result<Box<Connected>>>,
unsubscribed: mpsc::UnboundedReceiver<crate::session::SubscriptionKey>,
stop: tokio::sync::watch::Sender<bool>,
}
fn harness(session_capacity: usize) -> Harness {
let (session_tx, session_rx) = mpsc::channel(16);
let (commands_tx, commands_rx) = mpsc::unbounded_channel();
let (out_tx, out_rx) = mpsc::channel(session_capacity);
let (unsub_tx, unsub_rx) = mpsc::unbounded_channel();
let (ready_tx, ready_rx) = oneshot::channel();
let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
let client = a_client_id();
let router = Router::new(
client,
session_rx,
commands_rx,
out_tx,
unsub_tx,
ready_tx,
stop_rx,
);
tokio::spawn(router.run());
Harness {
client,
session_tx,
commands: commands_tx,
session_events: SessionEvents::with_staged(std::collections::VecDeque::new(), out_rx),
ready: ready_rx,
unsubscribed: unsub_rx,
stop: stop_tx,
}
}
fn bound(kind: BindKind) -> WireEvent {
WireEvent::Bound(Box::new(BoundInfo {
session_id: SessionId::new("S1"),
kind,
keep_alive: Duration::from_secs(5),
request_limit_bytes: 50_000,
control_link: None,
}))
}
#[tokio::test]
async fn test_router_reports_the_first_bind_to_connect() {
let harness = harness(8);
assert!(
harness
.session_tx
.send(bound(BindKind::Created))
.await
.is_ok()
);
match harness.ready.await {
Ok(Ok(connected)) => {
assert_eq!(connected.session_id, "S1");
assert!(matches!(connected.continuity, Continuity::New));
}
other => panic!("expected a successful bind, got {other:?}"),
}
}
#[tokio::test]
async fn test_router_reports_a_terminal_failure_to_connect() {
let harness = harness(8);
let closed = WireClosed::ByServer {
cause: crate::session::ServerCause {
code: 1,
message: "User/password check failed".to_owned(),
},
};
assert!(
harness
.session_tx
.send(WireEvent::Closed(closed))
.await
.is_ok()
);
match harness.ready.await {
Ok(Err(Error::Session(cause))) => assert_eq!(cause.code(), 1),
other => panic!("expected a session refusal, got {other:?}"),
}
}
#[tokio::test]
async fn test_session_events_distinguish_the_three_recovery_outcomes() {
let harness = harness(8);
assert!(
harness
.session_tx
.send(bound(BindKind::Rebound))
.await
.is_ok()
);
assert!(
harness
.session_tx
.send(bound(BindKind::Recreated {
previous: Some(SessionId::new("S0"))
}))
.await
.is_ok()
);
assert!(
harness
.session_tx
.send(WireEvent::Closed(WireClosed::RetriesExhausted {
attempts: 8,
last: None
}))
.await
.is_ok()
);
let mut events = harness.session_events;
match events.next().await {
Some(SessionEvent::Connected(connected)) => {
assert_eq!(connected.continuity.state_validity(), StateValidity::Valid);
assert!(matches!(connected.continuity, Continuity::Preserved));
}
other => panic!("expected a preserved bind, got {other:?}"),
}
match events.next().await {
Some(SessionEvent::Connected(connected)) => {
assert_eq!(
connected.continuity.state_validity(),
StateValidity::Invalid
);
}
other => panic!("expected a replaced bind, got {other:?}"),
}
match events.next().await {
Some(SessionEvent::Closed(ClosedReason::ReconnectExhausted {
attempts: 8, ..
})) => {}
other => panic!("expected a definitive loss, got {other:?}"),
}
assert!(events.next().await.is_none());
}
#[tokio::test]
async fn test_dropping_the_session_event_stream_does_not_stall_the_router() {
let harness = harness(1);
drop(harness.session_events);
for _ in 0..16 {
assert!(
harness
.session_tx
.send(bound(BindKind::Rebound))
.await
.is_ok(),
"the router stopped consuming session events"
);
}
}
#[tokio::test]
async fn test_dropping_an_update_stream_asks_for_an_unsubscription() {
let mut harness = harness(8);
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, events_rx) = mpsc::channel(4);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
let updates = Updates::new(id, events_rx, harness.commands.clone());
drop(updates);
match harness.unsubscribed.recv().await {
Some(key) => assert_eq!(key.get(), id.get()),
None => panic!("dropping the stream did not unsubscribe"),
}
}
#[tokio::test]
async fn test_updates_are_bounded_and_block_rather_than_drop() {
let harness = harness(8);
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, mut events_rx) = mpsc::channel(1);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Activated {
item_count: 1,
field_count: 1,
command_fields: None,
}
))
.await
.is_ok()
);
for dropped_count in [1, 2, 3] {
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Overflow {
item_index: 1,
dropped_count,
}
))
.await
.is_ok()
);
}
assert!(matches!(
events_rx.recv().await,
Some(SubscriptionEvent::Activated { field_count: 1, .. })
));
for expected in [1, 2, 3] {
match events_rx.recv().await {
Some(SubscriptionEvent::Overflow { dropped_count, .. }) => {
assert_eq!(dropped_count, expected);
}
other => panic!("expected an overflow of {expected}, got {other:?}"),
}
}
}
#[tokio::test]
async fn test_an_undecodable_notification_is_surfaced_not_swallowed() {
let harness = harness(8);
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, mut events_rx) = mpsc::channel(4);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
assert!(
harness
.session_tx
.send(undecodable(
id,
SubscriptionError::NotActivated { tag: "U" }
))
.await
.is_ok()
);
assert!(matches!(
events_rx.recv().await,
Some(SubscriptionEvent::Undecodable { .. })
));
}
fn refused(target: crate::session::ControlTarget, code: i64, message: &str) -> WireEvent {
WireEvent::ControlResponse {
request_id: None,
target,
outcome: crate::session::ControlOutcome::Rejected {
cause: crate::session::ServerCause {
code,
message: message.to_owned(),
},
},
}
}
#[tokio::test]
async fn test_a_session_wide_rejection_reaches_the_session_stream() {
let mut harness = harness(8);
assert!(
harness
.session_tx
.send(refused(
crate::session::ControlTarget::Session,
23,
"Bad Field schema name"
))
.await
.is_ok()
);
match harness.session_events.next().await {
Some(SessionEvent::RequestRejected(error)) => {
assert_eq!(error.code(), 23);
assert_eq!(error.message(), "Bad Field schema name");
}
other => panic!("expected a rejection, got {other:?}"),
}
}
#[tokio::test]
async fn test_a_request_that_never_left_is_not_dressed_as_a_server_error() {
let mut harness = harness(8);
assert!(
harness
.session_tx
.send(WireEvent::ControlResponse {
request_id: None,
target: crate::session::ControlTarget::Session,
outcome: crate::session::ControlOutcome::NotSent {
reason: "the stream connection was down".to_owned(),
},
})
.await
.is_ok()
);
match harness.session_events.next().await {
Some(SessionEvent::RequestNotSent { reason }) => {
assert_eq!(reason, "the stream connection was down");
}
other => panic!("expected a local failure, got {other:?}"),
}
}
async fn register(
harness: &Harness,
capacity: usize,
) -> (SubscriptionId, mpsc::Receiver<SubscriptionEvent>) {
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, events_rx) = mpsc::channel(capacity);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
(id, events_rx)
}
#[tokio::test]
async fn test_a_refused_subscription_is_reported_on_its_own_stream() {
let harness = harness(8);
let (id, mut events) = register(&harness, 4).await;
assert!(
harness
.session_tx
.send(refused(
crate::session::ControlTarget::Subscription {
key: id.key(),
operation: crate::session::SubscriptionOperation::Subscribe,
},
21,
"Bad Item Group name"
))
.await
.is_ok()
);
match events.recv().await {
Some(SubscriptionEvent::Rejected(error)) => {
assert_eq!(error.code(), 21);
assert_eq!(error.message(), "Bad Item Group name");
}
other => panic!("expected a rejection on the subscription stream, got {other:?}"),
}
assert!(events.recv().await.is_none());
}
#[tokio::test]
async fn test_an_unsent_subscription_request_is_not_terminal() {
let harness = harness(8);
let (id, mut events) = register(&harness, 4).await;
assert!(
harness
.session_tx
.send(WireEvent::ControlResponse {
request_id: None,
target: crate::session::ControlTarget::Subscription {
key: id.key(),
operation: crate::session::SubscriptionOperation::Subscribe,
},
outcome: crate::session::ControlOutcome::NotSent {
reason: "stream connection lost".to_owned(),
},
})
.await
.is_ok()
);
match events.recv().await {
Some(SubscriptionEvent::Deferred { reason }) => {
assert_eq!(reason, "stream connection lost");
}
other => panic!("expected a non-terminal deferral, got {other:?}"),
}
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Activated {
item_count: 1,
field_count: 1,
command_fields: None,
}
))
.await
.is_ok()
);
assert!(matches!(
events.recv().await,
Some(SubscriptionEvent::Activated { .. })
));
}
#[tokio::test]
async fn test_an_accepted_control_request_produces_no_event() {
let mut harness = harness(8);
let (id, mut events) = register(&harness, 4).await;
assert!(
harness
.session_tx
.send(WireEvent::ControlResponse {
request_id: None,
target: crate::session::ControlTarget::Subscription {
key: id.key(),
operation: crate::session::SubscriptionOperation::Subscribe,
},
outcome: crate::session::ControlOutcome::Accepted,
})
.await
.is_ok()
);
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Activated {
item_count: 1,
field_count: 1,
command_fields: None,
}
))
.await
.is_ok()
);
assert!(matches!(
events.recv().await,
Some(SubscriptionEvent::Activated { .. })
));
assert!(harness.session_events.next().now_or_never().is_none());
}
#[tokio::test]
async fn test_a_refused_unsubscription_leaves_the_subscription_alive() {
let mut harness = harness(8);
let (id, mut events) = register(&harness, 4).await;
assert!(
harness
.session_tx
.send(refused(
crate::session::ControlTarget::Subscription {
key: id.key(),
operation: crate::session::SubscriptionOperation::Unsubscribe,
},
19,
"Specified subscription not found"
))
.await
.is_ok()
);
match harness.session_events.next().await {
Some(SessionEvent::RequestRejected(error)) => assert_eq!(error.code(), 19),
other => panic!("expected a session-level rejection, got {other:?}"),
}
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Activated {
item_count: 1,
field_count: 1,
command_fields: None,
}
))
.await
.is_ok()
);
assert!(matches!(
events.recv().await,
Some(SubscriptionEvent::Activated { .. })
));
}
#[tokio::test]
async fn test_an_unparsable_line_is_surfaced_verbatim() {
let mut harness = harness(8);
assert!(
harness
.session_tx
.send(WireEvent::Unparsed {
line: "FUTURE,1,2".to_owned(),
error: crate::session::ProtocolError::UnknownTag {
tag: "FUTURE".to_owned(),
line: "FUTURE,1,2".to_owned(),
},
})
.await
.is_ok()
);
match harness.session_events.next().await {
Some(SessionEvent::Unrecognized { line }) => assert_eq!(line, "FUTURE,1,2"),
other => panic!("expected the raw line, got {other:?}"),
}
}
fn data(id: SubscriptionId, event: WireSubscriptionEvent) -> WireEvent {
WireEvent::Subscription {
progressive: 1,
key: id.key(),
outcome: Box::new(Ok(event)),
}
}
fn undecodable(id: SubscriptionId, error: SubscriptionError) -> WireEvent {
WireEvent::Subscription {
progressive: 1,
key: id.key(),
outcome: Box::new(Err(error)),
}
}
#[derive(Debug)]
struct NullTransport;
impl session::Transport for NullTransport {
fn properties(&self) -> session::TransportProperties {
session::TransportProperties {
control_shares_stream: true,
ends_on_content_length: false,
is_polling: false,
}
}
fn set_control_link(&mut self, _host: Option<&str>) {}
async fn open_stream(
&mut self,
_request: session::StreamOpen,
) -> std::result::Result<(), crate::error::TransportError> {
Ok(())
}
async fn next_line(
&mut self,
) -> Option<std::result::Result<String, crate::error::TransportError>> {
std::future::pending().await
}
async fn send_control(
&mut self,
_request: session::EncodedRequest,
) -> std::result::Result<(), crate::error::TransportError> {
Ok(())
}
async fn close(&mut self) -> std::result::Result<(), crate::error::TransportError> {
Ok(())
}
}
async fn mint_keys(count: usize) -> Vec<crate::session::SubscriptionKey> {
let options = crate::session::options::SessionOptions::default();
let Ok((driver, handle, events)) = crate::session::connect(NullTransport, options) else {
panic!("a default session configuration is valid");
};
let mut keys = Vec::with_capacity(count);
for index in 0..count {
let spec = crate::session::SubscriptionSpec::new(
format!("item{index}"),
"price",
session::SubscriptionMode::Merge,
);
match handle.subscribe(spec).await {
Ok(key) => keys.push(key),
Err(error) => panic!("the driver's command channel is open: {error}"),
}
}
drop((driver, events));
keys
}
async fn a_key() -> crate::session::SubscriptionKey {
match mint_keys(1).await.into_iter().next() {
Some(key) => key,
None => panic!("one key was requested"),
}
}
#[derive(Debug, Clone, Default)]
struct ScriptLog {
controls: Vec<String>,
closes: usize,
}
#[derive(Debug, Clone, Copy)]
enum Say {
Line(&'static str),
AwaitControls(usize),
}
#[derive(Debug)]
struct ScriptedTransport {
scripts: std::collections::VecDeque<Option<Vec<Say>>>,
current: std::collections::VecDeque<Say>,
log: std::sync::Arc<std::sync::Mutex<ScriptLog>>,
}
impl ScriptedTransport {
fn new(scripts: Vec<Option<Vec<Say>>>) -> Self {
Self {
scripts: scripts.into(),
current: std::collections::VecDeque::new(),
log: std::sync::Arc::new(std::sync::Mutex::new(ScriptLog::default())),
}
}
fn log(&self) -> std::sync::Arc<std::sync::Mutex<ScriptLog>> {
std::sync::Arc::clone(&self.log)
}
}
impl session::Transport for ScriptedTransport {
fn properties(&self) -> session::TransportProperties {
session::TransportProperties {
control_shares_stream: true,
ends_on_content_length: false,
is_polling: false,
}
}
fn set_control_link(&mut self, _host: Option<&str>) {}
async fn open_stream(
&mut self,
_request: session::StreamOpen,
) -> std::result::Result<(), crate::error::TransportError> {
match self.scripts.pop_front() {
Some(Some(script)) => {
self.current = script.into();
Ok(())
}
Some(None) => Err(crate::error::TransportError::ConnectionLost {
reason: "scripted failure".to_owned(),
}),
None => {
self.current.clear();
Ok(())
}
}
}
async fn next_line(
&mut self,
) -> Option<std::result::Result<String, crate::error::TransportError>> {
loop {
match self.current.front().copied() {
Some(Say::Line(line)) => {
self.current.pop_front();
return Some(Ok(line.to_owned()));
}
Some(Say::AwaitControls(wanted)) => {
let sent = self.log.lock().map_or(0, |log| log.controls.len());
if sent >= wanted {
self.current.pop_front();
continue;
}
return std::future::pending().await;
}
None => return std::future::pending().await,
}
}
}
async fn send_control(
&mut self,
request: session::EncodedRequest,
) -> std::result::Result<(), crate::error::TransportError> {
if let Ok(mut log) = self.log.lock() {
log.controls.push(request.parameters);
}
Ok(())
}
async fn close(&mut self) -> std::result::Result<(), crate::error::TransportError> {
if let Ok(mut log) = self.log.lock() {
log.closes = log.closes.saturating_add(1);
}
Ok(())
}
}
const CONOK: &str = "CONOK,S1,50000,5000,*";
fn capacities(session_event: usize) -> Capacities {
Capacities {
update: std::num::NonZeroUsize::new(8).unwrap_or(std::num::NonZeroUsize::MIN),
session_event: std::num::NonZeroUsize::new(session_event)
.unwrap_or(std::num::NonZeroUsize::MIN),
shutdown_timeout: Duration::from_secs(5),
}
}
fn session_options() -> crate::session::options::SessionOptions {
crate::session::options::SessionOptions::default().with_backoff(
crate::session::backoff::BackoffPolicy {
initial: Duration::from_millis(10),
max: Duration::from_millis(40),
max_attempts: std::num::NonZeroU32::new(6),
},
)
}
#[tokio::test(start_paused = true)]
async fn test_connect_completes_when_retries_fill_a_capacity_one_event_stream() {
let transport = ScriptedTransport::new(vec![
None,
None,
None,
Some(vec![Say::Line(CONOK), Say::AwaitControls(usize::MAX)]),
]);
let (client, mut events) = tokio::time::timeout(
Duration::from_secs(30),
Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(1),
),
)
.await
.expect("connect must not wait on a stream the caller cannot hold yet")
.expect("the fourth attempt bound");
assert!(
matches!(events.next().await, Some(SessionEvent::Disconnected { .. })),
"the retained startup event comes first"
);
assert!(matches!(
events.next().await,
Some(SessionEvent::Connected(_))
));
drop(client);
}
#[tokio::test(start_paused = true)]
async fn test_connect_retains_startup_events_in_order() {
let transport = ScriptedTransport::new(vec![
None,
None,
None,
Some(vec![Say::Line(CONOK), Say::AwaitControls(usize::MAX)]),
]);
let (client, events) = tokio::time::timeout(
Duration::from_secs(30),
Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(16),
),
)
.await
.expect("connect must not wait on a stream the caller cannot hold yet")
.expect("the fourth attempt bound");
let seen: Vec<SessionEvent> = events.take(4).collect().await;
assert!(
seen.iter()
.take(3)
.all(|event| matches!(event, SessionEvent::Disconnected { .. })),
"the three failed attempts come first, in order: {seen:?}"
);
assert!(
matches!(seen.get(3), Some(SessionEvent::Connected(_))),
"then the bind: {seen:?}"
);
drop(client);
}
#[tokio::test(start_paused = true)]
async fn test_connect_reports_exhaustion_with_a_capacity_one_event_stream() {
let transport = ScriptedTransport::new(vec![None, None, None, None, None, None, None]);
let outcome = tokio::time::timeout(
Duration::from_secs(30),
Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(1),
),
)
.await
.expect("connect must not hang once the retries are spent");
match outcome {
Err(Error::ReconnectExhausted {
last: Some(reason), ..
}) => assert!(
matches!(*reason, DisconnectReason::ConnectionFailed { .. }),
"expected the scripted connection failure, got {reason:?}"
),
other => panic!(
"expected exhaustion with a cause, got {:?}",
other.map(|_| ())
),
}
}
#[tokio::test(start_paused = true)]
async fn test_disconnect_waits_for_the_destroy_the_socket_and_the_tasks() {
let transport = ScriptedTransport::new(vec![Some(vec![
Say::Line(CONOK),
Say::AwaitControls(1),
Say::Line("END,31,session destroyed"),
])]);
let log = transport.log();
let (client, mut events) = Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(16),
)
.await
.expect("the session bound");
tokio::time::timeout(Duration::from_secs(10), client.disconnect())
.await
.expect("disconnect must not hang")
.expect("the session was destroyed cleanly");
let recorded = log.lock().expect("the log is not poisoned").clone();
assert!(
recorded
.controls
.iter()
.any(|parameters| parameters.contains("LS_op=destroy")),
"the destroy reached the wire: {:?}",
recorded.controls
);
assert!(recorded.closes >= 1, "the transport was closed");
let remaining: Vec<SessionEvent> = std::iter::from_fn(|| events.next().now_or_never())
.flatten()
.collect();
assert!(
matches!(remaining.last(), Some(SessionEvent::Closed(_))),
"the last event is terminal: {remaining:?}"
);
}
#[tokio::test(start_paused = true)]
async fn test_disconnect_completes_even_with_an_unread_event_stream() {
let transport = ScriptedTransport::new(vec![Some(vec![
Say::Line(CONOK),
Say::Line("SERVNAME,Lightstreamer HTTP Server"),
Say::Line("CLIENTIP,127.0.0.1"),
Say::Line("CONS,unlimited"),
])]);
let log = transport.log();
let (client, events) = Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(1),
)
.await
.expect("the session bound");
tokio::time::sleep(Duration::from_millis(20)).await;
let disconnected = tokio::time::timeout(Duration::from_secs(60), client.disconnect()).await;
assert!(
disconnected.is_ok(),
"a stream nobody reads must not hold the shutdown open"
);
assert!(
log.lock().expect("the log is not poisoned").closes >= 1,
"the socket is closed on the forced path too"
);
drop(events);
}
#[tokio::test(start_paused = true)]
async fn test_subscribe_registers_the_stream_before_it_submits_the_request() {
let options = crate::session::options::SessionOptions {
command_capacity: std::num::NonZeroUsize::MIN,
..Default::default()
};
let Ok((driver, handle, session_events)) = crate::session::connect(NullTransport, options)
else {
panic!("a default session configuration is valid");
};
let filler = handle
.allocate_key()
.expect("the key space is not exhausted");
assert!(handle.unsubscribe(filler).await.is_ok());
let (router_tx, mut router_rx) = mpsc::unbounded_channel();
let (alive_tx, _alive_rx) = mpsc::channel(1);
let client = Client {
id: a_client_id(),
handle: handle.clone(),
router: router_tx,
update_capacity: std::num::NonZeroUsize::new(8).unwrap_or(std::num::NonZeroUsize::MIN),
shutdown_timeout: Duration::from_secs(5),
tasks: Vec::new(),
stop_on_drop: false,
_alive: alive_tx,
};
let subscribing = tokio::spawn(async move {
client
.subscribe(Subscription::new(
SubscriptionMode::Merge,
ItemGroup::from_items(["item1"]).expect("a valid group"),
FieldSchema::from_fields(["price"]).expect("a valid schema"),
))
.await
.is_ok()
});
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(
!subscribing.is_finished(),
"the submission is blocked, which is the window under test"
);
assert!(
matches!(router_rx.try_recv(), Ok(RouterCommand::Register { .. })),
"the stream is registered before the request is submitted"
);
drop((driver, session_events, handle));
assert!(
!tokio::time::timeout(Duration::from_secs(5), subscribing)
.await
.expect("the submission fails once the driver is gone")
.expect("the task did not panic"),
"a subscription that could not be submitted is an error"
);
assert!(
matches!(router_rx.try_recv(), Ok(RouterCommand::Unregister { .. })),
"the registration is rolled back, not left behind"
);
}
#[tokio::test(start_paused = true)]
async fn test_a_handle_from_one_client_cannot_unsubscribe_another() {
async fn a_client() -> (Client, Updates, std::sync::Arc<std::sync::Mutex<ScriptLog>>) {
let transport = ScriptedTransport::new(vec![Some(vec![
Say::Line(CONOK),
Say::AwaitControls(usize::MAX),
])]);
let log = transport.log();
let (client, events) = Client::assemble(
session::connect(transport, session_options()).expect("a valid session"),
capacities(16),
)
.await
.expect("the session bound");
drop(events);
let updates = client
.subscribe(Subscription::new(
SubscriptionMode::Merge,
ItemGroup::from_items(["item1"]).expect("a valid group"),
FieldSchema::from_fields(["price"]).expect("a valid schema"),
))
.await
.expect("the subscription was queued");
(client, updates, log)
}
let (first, first_updates, _) = a_client().await;
let (second, second_updates, second_log) = a_client().await;
let (first_id, second_id) = (first_updates.id(), second_updates.id());
assert_eq!(
first_id.get(),
second_id.get(),
"each client numbers its subscriptions from one"
);
assert_ne!(first_id, second_id, "the handles are still distinct");
tokio::time::sleep(Duration::from_millis(20)).await;
let before = second_log.lock().expect("the log is not poisoned").clone();
assert!(
before
.controls
.iter()
.any(|parameters| parameters.contains("LS_op=add")),
"the second client's own subscription was sent: {:?}",
before.controls
);
match second.unsubscribe(first_id).await {
Err(Error::ForeignSubscription { id }) => assert_eq!(id, first_id.get()),
other => panic!("a foreign handle was accepted: {other:?}"),
}
tokio::time::sleep(Duration::from_millis(20)).await;
let after = second_log.lock().expect("the log is not poisoned").clone();
assert!(
!after
.controls
.iter()
.any(|parameters| parameters.contains("LS_op=delete")),
"nothing was sent for a handle this client does not own: {:?}",
after.controls
);
assert!(second.unsubscribe(second_id).await.is_ok());
drop((first, second, first_updates, second_updates));
}
#[tokio::test]
async fn test_a_stream_dropped_while_the_router_is_blocked_still_unsubscribes() {
let mut harness = harness(8);
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, events_rx) = mpsc::channel(1);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
let updates = Updates::new(id, events_rx, harness.commands.clone());
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Activated {
item_count: 1,
field_count: 1,
command_fields: None,
}
))
.await
.is_ok()
);
for dropped_count in [1, 2] {
assert!(
harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Overflow {
item_index: 1,
dropped_count,
}
))
.await
.is_ok()
);
}
tokio::task::yield_now().await;
drop(updates);
match tokio::time::timeout(Duration::from_secs(5), harness.unsubscribed.recv()).await {
Ok(Some(key)) => assert_eq!(key.get(), id.get()),
other => panic!("the server was left subscribed: {other:?}"),
}
assert!(harness.unsubscribed.recv().now_or_never().is_none());
}
#[tokio::test]
async fn test_the_router_stops_while_a_callers_stream_is_full() {
let harness = harness(1);
let id = SubscriptionId::new(harness.client, a_key().await);
let (events_tx, _events_rx) = mpsc::channel(1);
assert!(
harness
.commands
.send(RouterCommand::Register {
id,
events: events_tx,
})
.is_ok()
);
for _ in 0..4 {
let _ = harness
.session_tx
.send(data(
id,
WireSubscriptionEvent::Overflow {
item_index: 1,
dropped_count: 1,
},
))
.await;
}
assert!(harness.stop.send(true).is_ok());
match tokio::time::timeout(Duration::from_secs(5), harness.session_tx.closed()).await {
Ok(()) => {}
Err(_) => panic!("the router did not stop while a stream was full"),
}
}
}