lightstreamer_rs/client/mod.rs
1//! The client: connect, subscribe, and get out of the way.
2//!
3//! This is the whole public surface of the crate's behaviour. Everything below
4//! it — the session state machine, the wire parser, the transports — is
5//! private, because it is not something a caller should have to reason about
6//! to use a protocol client.
7//!
8//! # The shape of a program
9//!
10//! ```no_run
11//! use futures_util::StreamExt;
12//! use lightstreamer_rs::{
13//! AdapterSet, Client, ClientConfig, FieldSchema, ItemGroup, ServerAddress,
14//! Subscription, SubscriptionEvent, SubscriptionMode,
15//! };
16//!
17//! # async fn run() -> lightstreamer_rs::Result<()> {
18//! let config = ClientConfig::builder(ServerAddress::try_new("https://push.lightstreamer.com")?)
19//! .with_adapter_set(AdapterSet::try_new("DEMO")?)
20//! .build()?;
21//!
22//! // Not interested in session events: `drop` opts out. Binding them to
23//! //`_session_events` would keep the stream alive and unread, which stalls
24//! // the client once it fills — see `SessionEvents`.
25//! let (client, session_events) = Client::connect(config).await?;
26//! drop(session_events);
27//!
28//! let mut updates = client
29//! .subscribe(Subscription::new(
30//! SubscriptionMode::Merge,
31//! ItemGroup::from_items(["item1"])?,
32//! FieldSchema::from_fields(["last_price"])?,
33//! ))
34//! .await?;
35//!
36//! while let Some(event) = updates.next().await {
37//! if let SubscriptionEvent::Update(update) = event {
38//! println!("{}: {:?}", update.item_name(), update.changed_fields());
39//! }
40//! }
41//! # Ok(())
42//! # }
43//! ```
44
45mod events;
46mod message;
47mod router;
48mod subscription;
49mod updates;
50
51pub use events::{
52 ClosedReason, Connected, Continuity, DisconnectReason, Recovery, RecoveryOutcome, Resubscribed,
53 ServerInfo, SessionEvent, SessionEvents, StateValidity, SubscriptionId,
54};
55pub use message::{Message, MessageOutcome, MessageResult, SequenceName};
56pub use subscription::{
57 BufferSize, FieldSchema, FrequencyLimit, ItemGroup, MaxFrequency, Snapshot, Subscription,
58 SubscriptionMode,
59};
60pub use updates::{CommandFields, Filtering, SubscriptionEvent, UpdateFrequency, Updates};
61
62use tokio::sync::{mpsc, oneshot};
63
64use crate::config::ClientConfig;
65use crate::error::{Error, Result};
66use crate::session::{self, SessionHandle};
67
68use self::events::SessionEvents as SessionEventStream;
69use self::router::{Router, RouterCommand};
70
71/// Hands out one identity per [`Client`] built in this process.
72///
73/// Starts above [`ClientId::DETACHED`] so that no real client can ever share
74/// an identity with a hand-made [`crate::test_util`] handle.
75static NEXT_CLIENT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
76
77/// Which [`Client`] a [`SubscriptionId`] came from.
78///
79/// Not exposed: a caller has no use for it, and its whole job is to make a
80/// handle from one client unusable on another. See [`SubscriptionId`].
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub(crate) struct ClientId(u64);
83
84impl ClientId {
85 /// The identity of a handle that belongs to no client, which
86 /// [`crate::test_util`] mints. It matches no real client, so such a handle
87 /// can name a subscription in an assertion but never cancel one.
88 #[cfg(feature = "test-util")]
89 pub(crate) const DETACHED: Self = Self(0);
90
91 /// The next unused identity.
92 ///
93 /// Fails rather than wrapping. Reaching this needs 2⁶⁴ clients in one
94 /// process, but a wrapped identity would silently make two clients'
95 /// handles interchangeable again, which is the whole thing being
96 /// prevented.
97 fn allocate() -> Result<Self> {
98 use std::sync::atomic::Ordering::Relaxed;
99 NEXT_CLIENT_ID
100 .fetch_update(Relaxed, Relaxed, |current| current.checked_add(1))
101 .map(Self)
102 .map_err(|_| Error::Internal {
103 reason: "this process has run out of client identities".to_owned(),
104 })
105 }
106
107 /// The identity as a number, for `Display` and for logs.
108 pub(crate) const fn get(self) -> u64 {
109 self.0
110 }
111}
112
113/// The channel sizes and budgets a client is built with.
114#[derive(Debug, Clone, Copy)]
115struct Capacities {
116 /// Events buffered per subscription stream.
117 update: std::num::NonZeroUsize,
118 /// Events buffered on the session stream.
119 session_event: std::num::NonZeroUsize,
120 /// How long [`Client::disconnect`] waits before forcing a stop.
121 shutdown_timeout: std::time::Duration,
122}
123
124/// Holds one session event produced before the caller could hold the stream.
125///
126/// The buffer is capped at the capacity the caller configured, which is the
127/// budget it already said it wanted for session events. Past that, further
128/// pre-connection events are **discarded with a warning**: they are session
129/// events for a session that has not bound yet, whose outcome
130/// [`Client::connect`] reports as its own return value, and blocking instead
131/// would deadlock the bind against a stream nobody can yet read.
132fn stage(staged: &mut std::collections::VecDeque<SessionEvent>, event: SessionEvent, limit: usize) {
133 if staged.len() < limit {
134 staged.push_back(event);
135 return;
136 }
137 tracing::warn!(
138 limit,
139 "discarding a session event produced before connect() returned"
140 );
141}
142
143/// A live Lightstreamer session.
144///
145/// Get one from [`Client::connect`]. It is a handle, not the session itself:
146/// the work happens on background tasks, and the client is what you use to
147/// steer them.
148///
149/// # Shutdown is part of the contract
150///
151/// **Dropping the client shuts the session down.** The background tasks stop,
152/// the socket is closed, and every stream you are still holding ends. You
153/// never need `std::process::exit` to stop a `lightstreamer-rs` client, and
154/// you never need to remember a deregistration step.
155///
156/// Dropping does not tell the *server* the session is over: it just goes
157/// quiet, and the server keeps the session buffering until its own timeout
158/// expires [`docs/spec/02-session-lifecycle.md` §6.3]. Call
159/// [`Client::disconnect`] when you want the server to know at once — for
160/// instance because the same user is limited to a fixed number of sessions.
161///
162/// # Cloning
163///
164/// The client is **not** `Clone`, deliberately: its identity is what decides
165/// when the session ends, and a clone would make "dropping the client shuts
166/// the session down" untrue in a way that is hard to see at the call site. To
167/// use it from several tasks, put it in an [`std::sync::Arc`] — the session
168/// then ends when the last reference goes, which is the same rule stated once.
169#[derive(Debug)]
170pub struct Client {
171 /// This client's identity, carried by every [`SubscriptionId`] it hands
172 /// out so that another client's handle cannot be spent here.
173 id: ClientId,
174 handle: SessionHandle,
175 router: mpsc::UnboundedSender<RouterCommand>,
176 update_capacity: std::num::NonZeroUsize,
177 /// How long [`Client::disconnect`] waits for the ordered shutdown before
178 /// forcing one. Taken from
179 /// [`ConnectionOptions::with_open_timeout`](crate::ConnectionOptions::with_open_timeout),
180 /// which is already this crate's answer to "how long may one server round
181 /// trip take"; a destroy is exactly one.
182 shutdown_timeout: std::time::Duration,
183 /// The background tasks, so that [`Client::disconnect`] can wait for them
184 /// instead of merely asking them to stop.
185 tasks: Vec<tokio::task::JoinHandle<()>>,
186 /// Whether dropping this client should order an immediate stop.
187 ///
188 /// Always true except while [`Client::disconnect`] is running its ordered
189 /// shutdown, which needs the session to survive long enough to tell the
190 /// server.
191 stop_on_drop: bool,
192 /// Held only to be dropped. Closing it is how the background tasks learn
193 /// the client is gone, which is what makes "dropping the client shuts the
194 /// session down" true.
195 _alive: mpsc::Sender<()>,
196}
197
198impl Drop for Client {
199 /// Shuts the session down.
200 ///
201 /// Closing the channels the background tasks watch is not enough on its
202 /// own: a task blocked delivering into a stream the caller stopped reading
203 /// is not watching anything. The explicit stop is what makes "dropping the
204 /// client shuts the session down" true even then — it is exempt from
205 /// backpressure by construction, and whatever was still undelivered goes
206 /// with the streams it was going to.
207 fn drop(&mut self) {
208 if self.stop_on_drop {
209 self.handle.stop();
210 }
211 }
212}
213
214impl Client {
215 /// Opens a session and waits until it is streaming.
216 ///
217 /// Returns the client and the session's event stream. The stream is handed
218 /// over here rather than fetched later so that ignoring it is a decision
219 /// you write down — `drop(events)` — instead of an omission that quietly
220 /// costs you the recovery notifications
221 /// (`docs/adr/0005-recovery-is-visible-in-the-event-stream.md`).
222 ///
223 /// This resolves when the server has confirmed the session, so a
224 /// [`Client`] you hold is one that was streaming at least once. It does
225 /// **not** mean the connection is still up now: reconnection happens
226 /// underneath, and the session event stream is where you learn about it.
227 ///
228 /// # Errors
229 ///
230 /// - [`Error::Transport`] if the connection could not be established at
231 /// all, or the address could not be turned into an endpoint.
232 /// - [`Error::Session`] if the server refused the session with a code that
233 /// admits no retry — bad credentials (Appendix A code 1) and an unknown
234 /// Adapter Set (code 2) are the usual ones.
235 /// - [`Error::ReconnectExhausted`] if every attempt allowed by
236 /// [`RetryPolicy`](crate::RetryPolicy) failed. Codes the specification
237 /// marks temporary are retried before this is reported, and the last
238 /// failure is carried in the error rather than reduced to a count.
239 /// - [`Error::Internal`] if this crate could not build a request it should
240 /// always be able to build.
241 ///
242 /// # Examples
243 ///
244 /// ```no_run
245 /// use lightstreamer_rs::{AdapterSet, Client, ClientConfig, ServerAddress};
246 ///
247 /// # async fn run() -> lightstreamer_rs::Result<()> {
248 /// let config = ClientConfig::builder(ServerAddress::try_new("https://push.lightstreamer.com")?)
249 /// .with_adapter_set(AdapterSet::try_new("DEMO")?)
250 /// .build()?;
251 ///
252 /// let (client, events) = Client::connect(config).await?;
253 /// # drop((client, events));
254 /// # Ok(())
255 /// # }
256 /// ```
257 #[tracing::instrument(skip(config), fields(address = %config.address(), transport = config.transport().as_str()))]
258 pub async fn connect(config: ClientConfig) -> Result<(Self, SessionEvents)> {
259 let capacities = Capacities {
260 update: config.options().update_capacity(),
261 session_event: config.options().session_event_capacity(),
262 shutdown_timeout: config.options().open_timeout(),
263 };
264
265 // Which transport carries the session, and how it is built, is the
266 // session layer's composition root — not this one. The façade names no
267 // adapter, so a second transport is added below without touching the
268 // semver-governed surface
269 // [`docs/adr/0007-transport-port-shape.md` §3].
270 let parts = session::connect_configured(config)?;
271 Self::assemble(parts, capacities).await
272 }
273
274 /// Wires the driver, the router and the unsubscriber together and waits
275 /// for the first bind.
276 ///
277 /// Generic over the transport so that the whole client — not merely the
278 /// session layer — can be exercised over a scripted one, with no socket and
279 /// no real timer [`docs/adr/0007-transport-port-shape.md`]. It takes the
280 /// session already composed, because choosing and building a transport is
281 /// not this layer's business.
282 async fn assemble<T>(
283 parts: (
284 session::SessionDriver<T>,
285 SessionHandle,
286 mpsc::Receiver<session::SessionEvent>,
287 ),
288 capacities: Capacities,
289 ) -> Result<(Self, SessionEvents)>
290 where
291 T: session::Transport + Send + 'static,
292 {
293 let Capacities {
294 update: update_capacity,
295 session_event: session_capacity,
296 shutdown_timeout,
297 } = capacities;
298 let id = ClientId::allocate()?;
299 let (driver, handle, session_events) = parts;
300
301 let (router_tx, router_rx) = mpsc::unbounded_channel();
302 let (unsubscribe_tx, unsubscribe_rx) = mpsc::unbounded_channel();
303 let (session_out_tx, mut session_out_rx) = mpsc::channel(session_capacity.get());
304 let (alive_tx, alive_rx) = mpsc::channel(1);
305 let (ready_tx, ready_rx) = oneshot::channel();
306
307 let router = Router::new(
308 id,
309 session_events,
310 router_rx,
311 session_out_tx,
312 unsubscribe_tx,
313 ready_tx,
314 handle.stop_signal(),
315 );
316
317 let tasks = vec![
318 tokio::spawn(async move {
319 let closed = driver.run().await;
320 tracing::debug!(?closed, "session driver stopped");
321 }),
322 tokio::spawn(router.run()),
323 tokio::spawn(router::issue_unsubscriptions(
324 handle.clone(),
325 unsubscribe_rx,
326 alive_rx,
327 handle.stop_signal(),
328 )),
329 ];
330
331 // The router fires readiness once, on the first bind or the first
332 // terminal failure. Waiting for it *while draining* the event channel
333 // is not an optimisation: nobody holds the event stream yet, so a
334 // startup that retries can fill the channel, and a router blocked on a
335 // full channel never processes the bind that readiness is waiting for.
336 // Draining here is what stops `connect` waiting on itself.
337 let mut staged = std::collections::VecDeque::new();
338 tokio::pin!(ready_rx);
339 let outcome = loop {
340 tokio::select! {
341 biased;
342 result = &mut ready_rx => break result,
343 event = session_out_rx.recv() => match event {
344 Some(event) => stage(&mut staged, event, session_capacity.get()),
345 // The router stopped without saying anything, which can
346 // only happen if everything was torn down first.
347 None => break Ok(Err(Error::Disconnected)),
348 },
349 }
350 };
351 let connected = match outcome {
352 Ok(Ok(connected)) => connected,
353 Ok(Err(error)) => return Err(error),
354 Err(_) => return Err(Error::Disconnected),
355 };
356 // Deliberately without the session identifier. `LS_session` is what a
357 // control request or a rebind names the session with
358 // [`docs/spec/03-requests.md` §5.1], so it is a bearer value, and this
359 // crate does not put one in a log line on a caller's behalf. It is on
360 // `Connected` for a caller who wants to.
361 tracing::info!(
362 client = id.get(),
363 keepalive = ?connected.keepalive,
364 request_limit_bytes = connected.request_limit_bytes,
365 "session established"
366 );
367
368 Ok((
369 Self {
370 id,
371 handle,
372 router: router_tx,
373 update_capacity,
374 shutdown_timeout,
375 tasks,
376 stop_on_drop: true,
377 _alive: alive_tx,
378 },
379 SessionEventStream::with_staged(staged, session_out_rx),
380 ))
381 }
382
383 /// Subscribes to a set of items and returns the stream of what happens to
384 /// them.
385 ///
386 /// The subscription is requested immediately and re-created automatically
387 /// on any session that replaces a lost one, so the stream you get back
388 /// survives a reconnection without any action from you. Whether the server
389 /// accepted it is reported *on the stream*, as
390 /// [`SubscriptionEvent::Activated`] or [`SubscriptionEvent::Rejected`] —
391 /// this call does not wait for that round trip, because a caller usually
392 /// wants to subscribe to several things and then start reading.
393 ///
394 /// # Errors
395 ///
396 /// - [`Error::Config`] if the subscription's options and its mode cannot
397 /// go together — see [`Subscription::validate`], which this calls before
398 /// anything is sent. Checking here rather than at encoding time is what
399 /// makes an impossible subscription a mistake you see at the call site
400 /// instead of a stream that quietly never activates.
401 /// - [`Error::Disconnected`] if the session has already stopped.
402 /// - [`Error::Internal`] if this client has run out of the identifiers it
403 /// names subscriptions by — which takes 2⁶⁴ of them, and is reported
404 /// rather than wrapped around because a reused identifier would silently
405 /// conflate two subscriptions.
406 ///
407 /// Everything the *server* has to say about the subscription arrives on
408 /// the returned stream instead, where it can be interleaved correctly with
409 /// the data.
410 ///
411 /// # Examples
412 ///
413 /// ```no_run
414 /// use futures_util::StreamExt;
415 /// use lightstreamer_rs::{
416 /// Client, FieldSchema, ItemGroup, Snapshot, Subscription, SubscriptionEvent,
417 /// SubscriptionMode,
418 /// };
419 ///
420 /// # async fn run(client: &Client) -> lightstreamer_rs::Result<()> {
421 /// let mut updates = client
422 /// .subscribe(
423 /// Subscription::new(
424 /// SubscriptionMode::Merge,
425 /// ItemGroup::from_items(["item1", "item2"])?,
426 /// FieldSchema::from_fields(["last_price", "time"])?,
427 /// )
428 /// .with_snapshot(Snapshot::On),
429 /// )
430 /// .await?;
431 ///
432 /// while let Some(event) = updates.next().await {
433 /// match event {
434 /// SubscriptionEvent::Update(update) => {
435 /// println!("{}: {:?}", update.item_name(), update.changed_fields());
436 /// }
437 /// SubscriptionEvent::Rejected(error) => {
438 /// eprintln!("the server refused it: {error}");
439 /// break;
440 /// }
441 /// _ => {}
442 /// }
443 /// }
444 /// # Ok(())
445 /// # }
446 /// ```
447 #[tracing::instrument(skip(self, subscription), fields(mode = subscription.mode().as_str()))]
448 pub async fn subscribe(&self, subscription: Subscription) -> Result<Updates> {
449 // Before anything is allocated or registered: a combination the mode
450 // does not admit is the caller's mistake, and it is one here rather
451 // than a `Deferred` on a stream that never activates.
452 subscription.validate()?;
453
454 let (events_tx, events_rx) = mpsc::channel(self.update_capacity.get());
455 // The key is allocated, and everything it names registered, *before*
456 // the request that could produce a notification for it is queued. The
457 // other order leaves a window in which an immediate `SUBOK`, an early
458 // update, or a `REQERR` refusing the subscription outright arrives with
459 // nowhere to go [`docs/spec/03-requests.md` §13] — and a refusal that
460 // lands in that window would leave the caller holding a stream that
461 // never receives its own terminal event.
462 //
463 // The router's channel is unbounded, so this cannot block and cannot
464 // fail while the router lives, and the router takes its commands ahead
465 // of session events.
466 let key = self
467 .handle
468 .allocate_key()
469 .map_err(|error| Error::Internal {
470 reason: error.to_string(),
471 })?;
472 let id = SubscriptionId::new(self.id, key);
473 self.router
474 .send(RouterCommand::Register {
475 id,
476 events: events_tx,
477 })
478 .map_err(|_| Error::Disconnected)?;
479
480 if let Err(error) = self
481 .handle
482 .subscribe_with_key(key, subscription.into_spec())
483 .await
484 {
485 // Nothing was subscribed, so nothing is unsubscribed: the
486 // registration is simply undone.
487 tracing::debug!(id = id.get(), %error, "the subscription could not be queued");
488 let _ = self.router.send(RouterCommand::Unregister { id });
489 return Err(Error::Disconnected);
490 }
491
492 tracing::debug!(id = id.get(), "subscribed");
493 Ok(Updates::new(id, events_rx, self.router.clone()))
494 }
495
496 /// Unsubscribes, and waits for the request to be handed to the session.
497 ///
498 /// Usually unnecessary: dropping the [`Updates`] stream unsubscribes on
499 /// its own. Use this when you want the unsubscription to be ordered with
500 /// respect to your other calls, or when you want to keep reading the
501 /// stream until [`SubscriptionEvent::Unsubscribed`] arrives — after which
502 /// the server sends nothing more for those items
503 /// [`docs/spec/04-notifications.md` §3.4].
504 ///
505 /// Updates may still arrive between this call and that event; that is the
506 /// protocol's behaviour, not a race in this crate.
507 ///
508 /// # Errors
509 ///
510 /// - [`Error::ForeignSubscription`] if the handle was created by a
511 /// different client. Nothing is sent: every client numbers its
512 /// subscriptions from one, so the same number names different data on
513 /// each of them, and acting on it would cancel somebody else's.
514 /// - [`Error::Disconnected`] if the session has already stopped, in which
515 /// case the subscription is gone regardless.
516 pub async fn unsubscribe(&self, id: SubscriptionId) -> Result<()> {
517 if id.client() != self.id {
518 return Err(Error::ForeignSubscription { id: id.get() });
519 }
520 self.handle
521 .unsubscribe(id.key())
522 .await
523 .map_err(|_| Error::Disconnected)
524 }
525
526 /// Sends a message to the server's Metadata Adapter
527 /// [`docs/spec/03-requests.md` §12].
528 ///
529 /// `Ok(())` means the message was **queued for the session**, not that it
530 /// reached the server. What actually happened to it is reported on the
531 /// session event stream as [`SessionEvent::Message`] — not here, because
532 /// the round trip runs through the Metadata Adapter and may take as long
533 /// as the application behind it does.
534 ///
535 /// The guarantee is one report per message: **every message that asked for
536 /// an outcome produces exactly one [`SessionEvent::Message`]**, whether it
537 /// was processed, rejected by the adapter, refused by the server, or never
538 /// sent at all. A [`Message::fire_and_forget`] one produces a report only
539 /// in the last two cases, since it declined the notification the other two
540 /// would have arrived on.
541 ///
542 /// Note that the server acknowledges *acceptance* and *processing*
543 /// separately: acceptance is internal to this crate, and
544 /// [`MessageResult::Delivered`] is the one that means the Metadata Adapter
545 /// actually ran [`docs/spec/04-notifications.md` §4.1].
546 ///
547 /// # Messages are never resent
548 ///
549 /// If the session is replaced while a message is in flight, this crate
550 /// reports [`MessageResult::NotSent`] rather than sending it again. The
551 /// server deduplicates messages by their progressive number *within a
552 /// session*, so replaying one across a replacement would restart the
553 /// numbering it deduplicates against. Whether to retry is a decision only
554 /// the caller can make correctly.
555 ///
556 /// # Errors
557 ///
558 /// - [`Error::Config`] if the message's parts cannot be sent together —
559 /// see [`Message::validate`], which this calls first.
560 /// - [`Error::Disconnected`] if the session has already stopped.
561 ///
562 /// # Examples
563 ///
564 /// ```no_run
565 /// use std::num::NonZeroU32;
566 /// use lightstreamer_rs::{Client, Message, SequenceName};
567 ///
568 /// # async fn run(client: &Client) -> lightstreamer_rs::Result<()> {
569 /// client
570 /// .send_message(
571 /// Message::new("BUY 100", NonZeroU32::MIN)
572 /// .in_sequence(SequenceName::try_new("ORDERS")?),
573 /// )
574 /// .await?;
575 /// # Ok(())
576 /// # }
577 /// ```
578 #[tracing::instrument(skip(self, message))]
579 pub async fn send_message(&self, message: Message) -> Result<()> {
580 // The text is the caller's and may be anything, so nothing about the
581 // message is logged; what is checked here is only its shape.
582 message.validate()?;
583 self.handle
584 .send_message(message.into_outgoing())
585 .await
586 .map_err(|_| Error::Disconnected)
587 }
588
589 /// Ends the session, telling the server so, and waits until it is over.
590 ///
591 /// The server closes the session at once instead of holding it open until
592 /// its own timeout [`docs/spec/02-session-lifecycle.md` §6.3]. When this
593 /// returns, the `destroy` has been written, the socket has been closed, and
594 /// every background task this client owns has finished — so a caller may
595 /// shut its runtime down immediately afterwards without cutting the
596 /// shutdown short. Every stream this client handed out has ended, the last
597 /// session event being a [`SessionEvent::Closed`].
598 ///
599 /// Dropping the client instead ends the session *without* telling the
600 /// server, and without waiting. Prefer this call when the session limit per
601 /// user matters, or when you want a clean line in the server's log.
602 ///
603 /// # The wait is bounded
604 ///
605 /// A server that never answers, or a stream the caller stopped reading,
606 /// cannot make this hang: after
607 /// [`ConnectionOptions::with_open_timeout`](crate::ConnectionOptions::with_open_timeout)
608 /// — the same budget one server round trip is given anywhere else — the
609 /// shutdown stops being polite and the tasks are stopped regardless. The
610 /// socket is closed on that path too.
611 ///
612 /// # Errors
613 ///
614 /// [`Error::Disconnected`] if the session had already stopped — which is
615 /// the outcome you asked for, so it is usually safe to ignore.
616 /// [`Error::Internal`] if the session did not finish within the budget
617 /// above; it has been stopped anyway, and nothing is left running.
618 ///
619 /// # Examples
620 ///
621 /// ```no_run
622 /// # async fn run(client: lightstreamer_rs::Client) {
623 /// // An already-stopped session is not a failure worth propagating here.
624 /// let _ = client.disconnect().await;
625 /// # }
626 /// ```
627 #[tracing::instrument(skip(self))]
628 pub async fn disconnect(mut self) -> Result<()> {
629 // From here on, dropping this value must not cut the session short:
630 // the point of the call is to let it finish saying goodbye.
631 self.stop_on_drop = false;
632 let budget = self.shutdown_timeout;
633 let mut tasks = std::mem::take(&mut self.tasks);
634 let handle = self.handle.clone();
635
636 // Asking is itself bounded. The command channel is bounded, and a
637 // driver blocked delivering into a stream nobody is reading must not
638 // be able to swallow the request to end it.
639 let asked = match tokio::time::timeout(budget, self.handle.destroy(None)).await {
640 Ok(Ok(())) => Ok(()),
641 Ok(Err(_)) => Err(Error::Disconnected),
642 Err(_) => Err(Error::Internal {
643 reason: "the session did not accept the destroy request in time".to_owned(),
644 }),
645 };
646
647 // Everything that keeps the background tasks alive goes now: the
648 // router's command channel, the liveness token, and this client's own
649 // session handle. What is left is the ordered shutdown itself.
650 drop(self);
651
652 let mut outcome = asked;
653 if tokio::time::timeout(budget, join_all(&mut tasks))
654 .await
655 .is_err()
656 {
657 tracing::warn!(
658 ?budget,
659 "the session did not shut down in time; stopping it"
660 );
661 // Exempt from backpressure by construction: this is what a stream
662 // the caller stopped reading cannot hold up.
663 handle.stop();
664 if tokio::time::timeout(budget, join_all(&mut tasks))
665 .await
666 .is_err()
667 {
668 for task in &tasks {
669 task.abort();
670 }
671 join_all(&mut tasks).await;
672 }
673 if outcome.is_ok() {
674 outcome = Err(Error::Internal {
675 reason: "the session did not shut down within the configured budget".to_owned(),
676 });
677 }
678 }
679 drop(handle);
680 outcome
681 }
682}
683
684/// Waits for every task, keeping the ones not yet finished.
685///
686/// Written to be cancel-safe: a handle is removed only once it has resolved,
687/// so a caller that times this out still owns whatever is still running and can
688/// escalate rather than detach it.
689async fn join_all(tasks: &mut Vec<tokio::task::JoinHandle<()>>) {
690 while let Some(task) = tasks.last_mut() {
691 if let Err(error) = task.await
692 && !error.is_cancelled()
693 {
694 tracing::warn!(%error, "a client task did not finish cleanly");
695 }
696 tasks.pop();
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 #![allow(clippy::unwrap_used, clippy::expect_used)]
703
704 use futures_util::{FutureExt, StreamExt};
705 use tokio::sync::{mpsc, oneshot};
706
707 use super::*;
708 use crate::client::events::Connected;
709 use crate::client::router::Router;
710 use crate::session::{
711 BindKind, BoundInfo, SessionClosed as WireClosed, SessionEvent as WireEvent, SessionId,
712 SubscriptionError, SubscriptionEvent as WireSubscriptionEvent,
713 };
714 use std::time::Duration;
715
716 /// A fresh client identity, as [`Client::assemble`] would allocate one.
717 fn a_client_id() -> ClientId {
718 match ClientId::allocate() {
719 Ok(id) => id,
720 Err(error) => panic!("this process has client identities left: {error}"),
721 }
722 }
723
724 /// Everything a router test needs, wired but with no session behind it.
725 struct Harness {
726 /// The identity the router stamps its handles with.
727 client: ClientId,
728 /// Pretends to be the session driver.
729 session_tx: mpsc::Sender<WireEvent>,
730 commands: mpsc::UnboundedSender<RouterCommand>,
731 session_events: SessionEvents,
732 ready: oneshot::Receiver<Result<Box<Connected>>>,
733 unsubscribed: mpsc::UnboundedReceiver<crate::session::SubscriptionKey>,
734 /// Raises the stop lane, as a client being torn down would.
735 stop: tokio::sync::watch::Sender<bool>,
736 }
737
738 fn harness(session_capacity: usize) -> Harness {
739 let (session_tx, session_rx) = mpsc::channel(16);
740 let (commands_tx, commands_rx) = mpsc::unbounded_channel();
741 let (out_tx, out_rx) = mpsc::channel(session_capacity);
742 let (unsub_tx, unsub_rx) = mpsc::unbounded_channel();
743 let (ready_tx, ready_rx) = oneshot::channel();
744 let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
745
746 let client = a_client_id();
747 let router = Router::new(
748 client,
749 session_rx,
750 commands_rx,
751 out_tx,
752 unsub_tx,
753 ready_tx,
754 stop_rx,
755 );
756 tokio::spawn(router.run());
757
758 Harness {
759 client,
760 session_tx,
761 commands: commands_tx,
762 session_events: SessionEvents::with_staged(std::collections::VecDeque::new(), out_rx),
763 ready: ready_rx,
764 unsubscribed: unsub_rx,
765 stop: stop_tx,
766 }
767 }
768
769 fn bound(kind: BindKind) -> WireEvent {
770 WireEvent::Bound(Box::new(BoundInfo {
771 session_id: SessionId::new("S1"),
772 kind,
773 keep_alive: Duration::from_secs(5),
774 request_limit_bytes: 50_000,
775 control_link: None,
776 }))
777 }
778
779 #[tokio::test]
780 async fn test_router_reports_the_first_bind_to_connect() {
781 let harness = harness(8);
782 assert!(
783 harness
784 .session_tx
785 .send(bound(BindKind::Created))
786 .await
787 .is_ok()
788 );
789
790 match harness.ready.await {
791 Ok(Ok(connected)) => {
792 assert_eq!(connected.session_id, "S1");
793 assert!(matches!(connected.continuity, Continuity::New));
794 }
795 other => panic!("expected a successful bind, got {other:?}"),
796 }
797 }
798
799 #[tokio::test]
800 async fn test_router_reports_a_terminal_failure_to_connect() {
801 let harness = harness(8);
802 let closed = WireClosed::ByServer {
803 cause: crate::session::ServerCause {
804 code: 1,
805 message: "User/password check failed".to_owned(),
806 },
807 };
808 assert!(
809 harness
810 .session_tx
811 .send(WireEvent::Closed(closed))
812 .await
813 .is_ok()
814 );
815
816 match harness.ready.await {
817 Ok(Err(Error::Session(cause))) => assert_eq!(cause.code(), 1),
818 other => panic!("expected a session refusal, got {other:?}"),
819 }
820 }
821
822 #[tokio::test]
823 async fn test_session_events_distinguish_the_three_recovery_outcomes() {
824 let harness = harness(8);
825
826 // Continuity preserved by a rebind.
827 assert!(
828 harness
829 .session_tx
830 .send(bound(BindKind::Rebound))
831 .await
832 .is_ok()
833 );
834 // The session was replaced by a new one.
835 assert!(
836 harness
837 .session_tx
838 .send(bound(BindKind::Recreated {
839 previous: Some(SessionId::new("S0"))
840 }))
841 .await
842 .is_ok()
843 );
844 // The session is definitively gone.
845 assert!(
846 harness
847 .session_tx
848 .send(WireEvent::Closed(WireClosed::RetriesExhausted {
849 attempts: 8,
850 last: None
851 }))
852 .await
853 .is_ok()
854 );
855
856 let mut events = harness.session_events;
857 match events.next().await {
858 Some(SessionEvent::Connected(connected)) => {
859 assert_eq!(connected.continuity.state_validity(), StateValidity::Valid);
860 assert!(matches!(connected.continuity, Continuity::Preserved));
861 }
862 other => panic!("expected a preserved bind, got {other:?}"),
863 }
864 match events.next().await {
865 Some(SessionEvent::Connected(connected)) => {
866 assert_eq!(
867 connected.continuity.state_validity(),
868 StateValidity::Invalid
869 );
870 }
871 other => panic!("expected a replaced bind, got {other:?}"),
872 }
873 match events.next().await {
874 Some(SessionEvent::Closed(ClosedReason::ReconnectExhausted {
875 attempts: 8, ..
876 })) => {}
877 other => panic!("expected a definitive loss, got {other:?}"),
878 }
879 // Terminal: the stream ends.
880 assert!(events.next().await.is_none());
881 }
882
883 #[tokio::test]
884 async fn test_dropping_the_session_event_stream_does_not_stall_the_router() {
885 // Capacity of one, and nobody reading: without the drop the router
886 // would block on the second event forever.
887 let harness = harness(1);
888 drop(harness.session_events);
889
890 for _ in 0..16 {
891 assert!(
892 harness
893 .session_tx
894 .send(bound(BindKind::Rebound))
895 .await
896 .is_ok(),
897 "the router stopped consuming session events"
898 );
899 }
900 }
901
902 #[tokio::test]
903 async fn test_dropping_an_update_stream_asks_for_an_unsubscription() {
904 let mut harness = harness(8);
905 let id = SubscriptionId::new(harness.client, a_key().await);
906 let (events_tx, events_rx) = mpsc::channel(4);
907 assert!(
908 harness
909 .commands
910 .send(RouterCommand::Register {
911 id,
912 events: events_tx,
913 })
914 .is_ok()
915 );
916
917 let updates = Updates::new(id, events_rx, harness.commands.clone());
918 drop(updates);
919
920 match harness.unsubscribed.recv().await {
921 Some(key) => assert_eq!(key.get(), id.get()),
922 None => panic!("dropping the stream did not unsubscribe"),
923 }
924 }
925
926 #[tokio::test]
927 async fn test_updates_are_bounded_and_block_rather_than_drop() {
928 let harness = harness(8);
929 let id = SubscriptionId::new(harness.client, a_key().await);
930 // One slot only: the second update has nowhere to go until the caller
931 // reads, and the router must wait rather than discard it.
932 let (events_tx, mut events_rx) = mpsc::channel(1);
933 assert!(
934 harness
935 .commands
936 .send(RouterCommand::Register {
937 id,
938 events: events_tx,
939 })
940 .is_ok()
941 );
942
943 assert!(
944 harness
945 .session_tx
946 .send(data(
947 id,
948 WireSubscriptionEvent::Activated {
949 item_count: 1,
950 field_count: 1,
951 command_fields: None,
952 }
953 ))
954 .await
955 .is_ok()
956 );
957 for dropped_count in [1, 2, 3] {
958 assert!(
959 harness
960 .session_tx
961 .send(data(
962 id,
963 WireSubscriptionEvent::Overflow {
964 item_index: 1,
965 dropped_count,
966 }
967 ))
968 .await
969 .is_ok()
970 );
971 }
972
973 // Nothing was dropped: activation, then all three events, in order.
974 assert!(matches!(
975 events_rx.recv().await,
976 Some(SubscriptionEvent::Activated { field_count: 1, .. })
977 ));
978 for expected in [1, 2, 3] {
979 match events_rx.recv().await {
980 Some(SubscriptionEvent::Overflow { dropped_count, .. }) => {
981 assert_eq!(dropped_count, expected);
982 }
983 other => panic!("expected an overflow of {expected}, got {other:?}"),
984 }
985 }
986 }
987
988 #[tokio::test]
989 async fn test_an_undecodable_notification_is_surfaced_not_swallowed() {
990 let harness = harness(8);
991 let id = SubscriptionId::new(harness.client, a_key().await);
992 let (events_tx, mut events_rx) = mpsc::channel(4);
993 assert!(
994 harness
995 .commands
996 .send(RouterCommand::Register {
997 id,
998 events: events_tx,
999 })
1000 .is_ok()
1001 );
1002
1003 // An update before the subscription was ever activated: the session
1004 // had no schema to decode it against, and says so in a typed error.
1005 assert!(
1006 harness
1007 .session_tx
1008 .send(undecodable(
1009 id,
1010 SubscriptionError::NotActivated { tag: "U" }
1011 ))
1012 .await
1013 .is_ok()
1014 );
1015
1016 assert!(matches!(
1017 events_rx.recv().await,
1018 Some(SubscriptionEvent::Undecodable { .. })
1019 ));
1020 }
1021
1022 /// A refused control request, aimed at whatever it was acting on.
1023 fn refused(target: crate::session::ControlTarget, code: i64, message: &str) -> WireEvent {
1024 WireEvent::ControlResponse {
1025 request_id: None,
1026 target,
1027 outcome: crate::session::ControlOutcome::Rejected {
1028 cause: crate::session::ServerCause {
1029 code,
1030 message: message.to_owned(),
1031 },
1032 },
1033 }
1034 }
1035
1036 #[tokio::test]
1037 async fn test_a_session_wide_rejection_reaches_the_session_stream() {
1038 let mut harness = harness(8);
1039 assert!(
1040 harness
1041 .session_tx
1042 .send(refused(
1043 crate::session::ControlTarget::Session,
1044 23,
1045 "Bad Field schema name"
1046 ))
1047 .await
1048 .is_ok()
1049 );
1050
1051 match harness.session_events.next().await {
1052 Some(SessionEvent::RequestRejected(error)) => {
1053 assert_eq!(error.code(), 23);
1054 assert_eq!(error.message(), "Bad Field schema name");
1055 }
1056 other => panic!("expected a rejection, got {other:?}"),
1057 }
1058 }
1059
1060 #[tokio::test]
1061 async fn test_a_request_that_never_left_is_not_dressed_as_a_server_error() {
1062 let mut harness = harness(8);
1063 assert!(
1064 harness
1065 .session_tx
1066 .send(WireEvent::ControlResponse {
1067 request_id: None,
1068 target: crate::session::ControlTarget::Session,
1069 outcome: crate::session::ControlOutcome::NotSent {
1070 reason: "the stream connection was down".to_owned(),
1071 },
1072 })
1073 .await
1074 .is_ok()
1075 );
1076
1077 // A fabricated code would be indistinguishable from one a Metadata
1078 // Adapter supplied, which this crate cannot honestly claim.
1079 match harness.session_events.next().await {
1080 Some(SessionEvent::RequestNotSent { reason }) => {
1081 assert_eq!(reason, "the stream connection was down");
1082 }
1083 other => panic!("expected a local failure, got {other:?}"),
1084 }
1085 }
1086
1087 /// Registers a subscription stream and hands back its id and receiver.
1088 async fn register(
1089 harness: &Harness,
1090 capacity: usize,
1091 ) -> (SubscriptionId, mpsc::Receiver<SubscriptionEvent>) {
1092 let id = SubscriptionId::new(harness.client, a_key().await);
1093 let (events_tx, events_rx) = mpsc::channel(capacity);
1094 assert!(
1095 harness
1096 .commands
1097 .send(RouterCommand::Register {
1098 id,
1099 events: events_tx,
1100 })
1101 .is_ok()
1102 );
1103 (id, events_rx)
1104 }
1105
1106 #[tokio::test]
1107 async fn test_a_refused_subscription_is_reported_on_its_own_stream() {
1108 // The silent-loss case: without the target the caller's stream would
1109 // simply go quiet forever.
1110 let harness = harness(8);
1111 let (id, mut events) = register(&harness, 4).await;
1112
1113 assert!(
1114 harness
1115 .session_tx
1116 .send(refused(
1117 crate::session::ControlTarget::Subscription {
1118 key: id.key(),
1119 operation: crate::session::SubscriptionOperation::Subscribe,
1120 },
1121 21,
1122 "Bad Item Group name"
1123 ))
1124 .await
1125 .is_ok()
1126 );
1127
1128 match events.recv().await {
1129 Some(SubscriptionEvent::Rejected(error)) => {
1130 assert_eq!(error.code(), 21);
1131 assert_eq!(error.message(), "Bad Item Group name");
1132 }
1133 other => panic!("expected a rejection on the subscription stream, got {other:?}"),
1134 }
1135 // Terminal: the subscription is gone and the stream ends.
1136 assert!(events.recv().await.is_none());
1137 }
1138
1139 #[tokio::test]
1140 async fn test_an_unsent_subscription_request_is_not_terminal() {
1141 // The session layer releases the wire binding and re-issues the
1142 // subscription at the next bind, so ending the stream here would lose
1143 // a subscription that is still coming.
1144 let harness = harness(8);
1145 let (id, mut events) = register(&harness, 4).await;
1146
1147 assert!(
1148 harness
1149 .session_tx
1150 .send(WireEvent::ControlResponse {
1151 request_id: None,
1152 target: crate::session::ControlTarget::Subscription {
1153 key: id.key(),
1154 operation: crate::session::SubscriptionOperation::Subscribe,
1155 },
1156 outcome: crate::session::ControlOutcome::NotSent {
1157 reason: "stream connection lost".to_owned(),
1158 },
1159 })
1160 .await
1161 .is_ok()
1162 );
1163
1164 match events.recv().await {
1165 Some(SubscriptionEvent::Deferred { reason }) => {
1166 assert_eq!(reason, "stream connection lost");
1167 }
1168 other => panic!("expected a non-terminal deferral, got {other:?}"),
1169 }
1170
1171 // The registration survived: the retry's `SUBOK` still lands here.
1172 assert!(
1173 harness
1174 .session_tx
1175 .send(data(
1176 id,
1177 WireSubscriptionEvent::Activated {
1178 item_count: 1,
1179 field_count: 1,
1180 command_fields: None,
1181 }
1182 ))
1183 .await
1184 .is_ok()
1185 );
1186 assert!(matches!(
1187 events.recv().await,
1188 Some(SubscriptionEvent::Activated { .. })
1189 ));
1190 }
1191
1192 #[tokio::test]
1193 async fn test_an_accepted_control_request_produces_no_event() {
1194 let mut harness = harness(8);
1195 let (id, mut events) = register(&harness, 4).await;
1196
1197 assert!(
1198 harness
1199 .session_tx
1200 .send(WireEvent::ControlResponse {
1201 request_id: None,
1202 target: crate::session::ControlTarget::Subscription {
1203 key: id.key(),
1204 operation: crate::session::SubscriptionOperation::Subscribe,
1205 },
1206 outcome: crate::session::ControlOutcome::Accepted,
1207 })
1208 .await
1209 .is_ok()
1210 );
1211 // An acceptance is an acknowledgement, not news: `SUBOK` is what says
1212 // the subscription is live, and that is what reaches the caller.
1213 assert!(
1214 harness
1215 .session_tx
1216 .send(data(
1217 id,
1218 WireSubscriptionEvent::Activated {
1219 item_count: 1,
1220 field_count: 1,
1221 command_fields: None,
1222 }
1223 ))
1224 .await
1225 .is_ok()
1226 );
1227 assert!(matches!(
1228 events.recv().await,
1229 Some(SubscriptionEvent::Activated { .. })
1230 ));
1231 assert!(harness.session_events.next().now_or_never().is_none());
1232 }
1233
1234 #[tokio::test]
1235 async fn test_a_refused_unsubscription_leaves_the_subscription_alive() {
1236 // A refused `delete` means the subscription is still active, so
1237 // reporting it on that stream would read as "your subscription is
1238 // gone" — the opposite of the truth.
1239 let mut harness = harness(8);
1240 let (id, mut events) = register(&harness, 4).await;
1241
1242 assert!(
1243 harness
1244 .session_tx
1245 .send(refused(
1246 crate::session::ControlTarget::Subscription {
1247 key: id.key(),
1248 operation: crate::session::SubscriptionOperation::Unsubscribe,
1249 },
1250 19,
1251 "Specified subscription not found"
1252 ))
1253 .await
1254 .is_ok()
1255 );
1256
1257 match harness.session_events.next().await {
1258 Some(SessionEvent::RequestRejected(error)) => assert_eq!(error.code(), 19),
1259 other => panic!("expected a session-level rejection, got {other:?}"),
1260 }
1261
1262 // The stream is still live: an update still reaches it.
1263 assert!(
1264 harness
1265 .session_tx
1266 .send(data(
1267 id,
1268 WireSubscriptionEvent::Activated {
1269 item_count: 1,
1270 field_count: 1,
1271 command_fields: None,
1272 }
1273 ))
1274 .await
1275 .is_ok()
1276 );
1277 assert!(matches!(
1278 events.recv().await,
1279 Some(SubscriptionEvent::Activated { .. })
1280 ));
1281 }
1282
1283 #[tokio::test]
1284 async fn test_an_unparsable_line_is_surfaced_verbatim() {
1285 let mut harness = harness(8);
1286 assert!(
1287 harness
1288 .session_tx
1289 .send(WireEvent::Unparsed {
1290 line: "FUTURE,1,2".to_owned(),
1291 error: crate::session::ProtocolError::UnknownTag {
1292 tag: "FUTURE".to_owned(),
1293 line: "FUTURE,1,2".to_owned(),
1294 },
1295 })
1296 .await
1297 .is_ok()
1298 );
1299
1300 match harness.session_events.next().await {
1301 Some(SessionEvent::Unrecognized { line }) => assert_eq!(line, "FUTURE,1,2"),
1302 other => panic!("expected the raw line, got {other:?}"),
1303 }
1304 }
1305
1306 /// One subscription event as the session layer would hand it up.
1307 ///
1308 /// The session interprets the wire line and routes the *meaning*; these
1309 /// tests are about the fan-out, so they start where the router does.
1310 fn data(id: SubscriptionId, event: WireSubscriptionEvent) -> WireEvent {
1311 WireEvent::Subscription {
1312 progressive: 1,
1313 key: id.key(),
1314 outcome: Box::new(Ok(event)),
1315 }
1316 }
1317
1318 /// A line the session could not reconcile with this subscription's state.
1319 fn undecodable(id: SubscriptionId, error: SubscriptionError) -> WireEvent {
1320 WireEvent::Subscription {
1321 progressive: 1,
1322 key: id.key(),
1323 outcome: Box::new(Err(error)),
1324 }
1325 }
1326
1327 /// A transport that connects to nothing and says nothing.
1328 ///
1329 /// The router tests need real [`SubscriptionKey`](crate::session::SubscriptionKey)
1330 /// values, which only the session layer can mint. A session is therefore
1331 /// created over this transport and its driver is never run: keys are
1332 /// allocated by `subscribe` before anything reaches the wire.
1333 #[derive(Debug)]
1334 struct NullTransport;
1335
1336 impl session::Transport for NullTransport {
1337 fn properties(&self) -> session::TransportProperties {
1338 session::TransportProperties {
1339 control_shares_stream: true,
1340 ends_on_content_length: false,
1341 is_polling: false,
1342 }
1343 }
1344
1345 fn set_control_link(&mut self, _host: Option<&str>) {}
1346
1347 async fn open_stream(
1348 &mut self,
1349 _request: session::StreamOpen,
1350 ) -> std::result::Result<(), crate::error::TransportError> {
1351 Ok(())
1352 }
1353
1354 async fn next_line(
1355 &mut self,
1356 ) -> Option<std::result::Result<String, crate::error::TransportError>> {
1357 std::future::pending().await
1358 }
1359
1360 async fn send_control(
1361 &mut self,
1362 _request: session::EncodedRequest,
1363 ) -> std::result::Result<(), crate::error::TransportError> {
1364 Ok(())
1365 }
1366
1367 async fn close(&mut self) -> std::result::Result<(), crate::error::TransportError> {
1368 Ok(())
1369 }
1370 }
1371
1372 /// Mints `count` distinct subscription keys.
1373 async fn mint_keys(count: usize) -> Vec<crate::session::SubscriptionKey> {
1374 let options = crate::session::options::SessionOptions::default();
1375 let Ok((driver, handle, events)) = crate::session::connect(NullTransport, options) else {
1376 panic!("a default session configuration is valid");
1377 };
1378 let mut keys = Vec::with_capacity(count);
1379 for index in 0..count {
1380 let spec = crate::session::SubscriptionSpec::new(
1381 format!("item{index}"),
1382 "price",
1383 session::SubscriptionMode::Merge,
1384 );
1385 match handle.subscribe(spec).await {
1386 Ok(key) => keys.push(key),
1387 Err(error) => panic!("the driver's command channel is open: {error}"),
1388 }
1389 }
1390 // The driver is deliberately never run; dropping it closes nothing the
1391 // tests rely on.
1392 drop((driver, events));
1393 keys
1394 }
1395
1396 /// One subscription key.
1397 async fn a_key() -> crate::session::SubscriptionKey {
1398 match mint_keys(1).await.into_iter().next() {
1399 Some(key) => key,
1400 None => panic!("one key was requested"),
1401 }
1402 }
1403
1404 // -----------------------------------------------------------------------
1405 // Startup, shutdown and registration ordering
1406 // -----------------------------------------------------------------------
1407
1408 /// A transport that replays lines and records what it was asked to do.
1409 ///
1410 /// The whole client is exercised over this: driver, router and
1411 /// unsubscriber, with no socket and no real timer
1412 /// [`docs/adr/0007-transport-port-shape.md`].
1413 #[derive(Debug, Clone, Default)]
1414 struct ScriptLog {
1415 controls: Vec<String>,
1416 closes: usize,
1417 }
1418
1419 /// One thing a scripted connection does when asked for a line.
1420 #[derive(Debug, Clone, Copy)]
1421 enum Say {
1422 /// Yield this line.
1423 Line(&'static str),
1424 /// Yield nothing until this many control requests have been sent, so a
1425 /// script can answer what the client actually did.
1426 AwaitControls(usize),
1427 }
1428
1429 #[derive(Debug)]
1430 struct ScriptedTransport {
1431 /// One script per stream connection; `None` for a connection that
1432 /// cannot be opened at all.
1433 scripts: std::collections::VecDeque<Option<Vec<Say>>>,
1434 current: std::collections::VecDeque<Say>,
1435 log: std::sync::Arc<std::sync::Mutex<ScriptLog>>,
1436 }
1437
1438 impl ScriptedTransport {
1439 fn new(scripts: Vec<Option<Vec<Say>>>) -> Self {
1440 Self {
1441 scripts: scripts.into(),
1442 current: std::collections::VecDeque::new(),
1443 log: std::sync::Arc::new(std::sync::Mutex::new(ScriptLog::default())),
1444 }
1445 }
1446
1447 fn log(&self) -> std::sync::Arc<std::sync::Mutex<ScriptLog>> {
1448 std::sync::Arc::clone(&self.log)
1449 }
1450 }
1451
1452 impl session::Transport for ScriptedTransport {
1453 fn properties(&self) -> session::TransportProperties {
1454 session::TransportProperties {
1455 control_shares_stream: true,
1456 ends_on_content_length: false,
1457 is_polling: false,
1458 }
1459 }
1460
1461 fn set_control_link(&mut self, _host: Option<&str>) {}
1462
1463 async fn open_stream(
1464 &mut self,
1465 _request: session::StreamOpen,
1466 ) -> std::result::Result<(), crate::error::TransportError> {
1467 match self.scripts.pop_front() {
1468 Some(Some(script)) => {
1469 self.current = script.into();
1470 Ok(())
1471 }
1472 Some(None) => Err(crate::error::TransportError::ConnectionLost {
1473 reason: "scripted failure".to_owned(),
1474 }),
1475 None => {
1476 self.current.clear();
1477 Ok(())
1478 }
1479 }
1480 }
1481
1482 async fn next_line(
1483 &mut self,
1484 ) -> Option<std::result::Result<String, crate::error::TransportError>> {
1485 loop {
1486 match self.current.front().copied() {
1487 Some(Say::Line(line)) => {
1488 self.current.pop_front();
1489 return Some(Ok(line.to_owned()));
1490 }
1491 Some(Say::AwaitControls(wanted)) => {
1492 let sent = self.log.lock().map_or(0, |log| log.controls.len());
1493 if sent >= wanted {
1494 self.current.pop_front();
1495 continue;
1496 }
1497 return std::future::pending().await;
1498 }
1499 None => return std::future::pending().await,
1500 }
1501 }
1502 }
1503
1504 async fn send_control(
1505 &mut self,
1506 request: session::EncodedRequest,
1507 ) -> std::result::Result<(), crate::error::TransportError> {
1508 if let Ok(mut log) = self.log.lock() {
1509 log.controls.push(request.parameters);
1510 }
1511 Ok(())
1512 }
1513
1514 async fn close(&mut self) -> std::result::Result<(), crate::error::TransportError> {
1515 if let Ok(mut log) = self.log.lock() {
1516 log.closes = log.closes.saturating_add(1);
1517 }
1518 Ok(())
1519 }
1520 }
1521
1522 const CONOK: &str = "CONOK,S1,50000,5000,*";
1523
1524 fn capacities(session_event: usize) -> Capacities {
1525 Capacities {
1526 update: std::num::NonZeroUsize::new(8).unwrap_or(std::num::NonZeroUsize::MIN),
1527 session_event: std::num::NonZeroUsize::new(session_event)
1528 .unwrap_or(std::num::NonZeroUsize::MIN),
1529 shutdown_timeout: Duration::from_secs(5),
1530 }
1531 }
1532
1533 fn session_options() -> crate::session::options::SessionOptions {
1534 crate::session::options::SessionOptions::default().with_backoff(
1535 crate::session::backoff::BackoffPolicy {
1536 initial: Duration::from_millis(10),
1537 max: Duration::from_millis(40),
1538 max_attempts: std::num::NonZeroU32::new(6),
1539 },
1540 )
1541 }
1542
1543 /// C-01. Readiness must never depend on the caller consuming a stream it
1544 /// cannot hold yet.
1545 #[tokio::test(start_paused = true)]
1546 async fn test_connect_completes_when_retries_fill_a_capacity_one_event_stream() {
1547 // Three failed attempts before the bind, with a single slot for the
1548 // session events they produce. Nobody can read that slot until
1549 // `connect` returns, so a router that blocked on it would never
1550 // deliver the bind that resolves this call.
1551 let transport = ScriptedTransport::new(vec![
1552 None,
1553 None,
1554 None,
1555 Some(vec![Say::Line(CONOK), Say::AwaitControls(usize::MAX)]),
1556 ]);
1557
1558 let (client, mut events) = tokio::time::timeout(
1559 Duration::from_secs(30),
1560 Client::assemble(
1561 session::connect(transport, session_options()).expect("a valid session"),
1562 capacities(1),
1563 ),
1564 )
1565 .await
1566 .expect("connect must not wait on a stream the caller cannot hold yet")
1567 .expect("the fourth attempt bound");
1568
1569 // One slot was configured, so one pre-connection event is retained —
1570 // the first, in the order it happened — and the rest are discarded by
1571 // the documented policy rather than blocking the bind.
1572 assert!(
1573 matches!(events.next().await, Some(SessionEvent::Disconnected { .. })),
1574 "the retained startup event comes first"
1575 );
1576 assert!(matches!(
1577 events.next().await,
1578 Some(SessionEvent::Connected(_))
1579 ));
1580 drop(client);
1581 }
1582
1583 /// C-01, the retention half: with room for them, every startup event is
1584 /// kept, in the order it happened.
1585 #[tokio::test(start_paused = true)]
1586 async fn test_connect_retains_startup_events_in_order() {
1587 let transport = ScriptedTransport::new(vec![
1588 None,
1589 None,
1590 None,
1591 Some(vec![Say::Line(CONOK), Say::AwaitControls(usize::MAX)]),
1592 ]);
1593
1594 let (client, events) = tokio::time::timeout(
1595 Duration::from_secs(30),
1596 Client::assemble(
1597 session::connect(transport, session_options()).expect("a valid session"),
1598 capacities(16),
1599 ),
1600 )
1601 .await
1602 .expect("connect must not wait on a stream the caller cannot hold yet")
1603 .expect("the fourth attempt bound");
1604
1605 let seen: Vec<SessionEvent> = events.take(4).collect().await;
1606 assert!(
1607 seen.iter()
1608 .take(3)
1609 .all(|event| matches!(event, SessionEvent::Disconnected { .. })),
1610 "the three failed attempts come first, in order: {seen:?}"
1611 );
1612 assert!(
1613 matches!(seen.get(3), Some(SessionEvent::Connected(_))),
1614 "then the bind: {seen:?}"
1615 );
1616 drop(client);
1617 }
1618
1619 /// C-01. The same, when the attempts run out instead of succeeding: the
1620 /// failure has to reach the caller rather than deadlock behind it.
1621 #[tokio::test(start_paused = true)]
1622 async fn test_connect_reports_exhaustion_with_a_capacity_one_event_stream() {
1623 let transport = ScriptedTransport::new(vec![None, None, None, None, None, None, None]);
1624
1625 let outcome = tokio::time::timeout(
1626 Duration::from_secs(30),
1627 Client::assemble(
1628 session::connect(transport, session_options()).expect("a valid session"),
1629 capacities(1),
1630 ),
1631 )
1632 .await
1633 .expect("connect must not hang once the retries are spent");
1634
1635 // A-04: exhaustion carries the last thing that went wrong. A count on
1636 // its own reads the same whether the host did not resolve or the
1637 // server refused the credentials.
1638 match outcome {
1639 Err(Error::ReconnectExhausted {
1640 last: Some(reason), ..
1641 }) => assert!(
1642 matches!(*reason, DisconnectReason::ConnectionFailed { .. }),
1643 "expected the scripted connection failure, got {reason:?}"
1644 ),
1645 other => panic!(
1646 "expected exhaustion with a cause, got {:?}",
1647 other.map(|_| ())
1648 ),
1649 }
1650 }
1651
1652 /// C-09. `disconnect()` must mean the session is over, not that a request
1653 /// to end it was queued.
1654 #[tokio::test(start_paused = true)]
1655 async fn test_disconnect_waits_for_the_destroy_the_socket_and_the_tasks() {
1656 // The server answers the destroy — and only the destroy — with the
1657 // `END` that T7 promises
1658 // [`docs/spec/02-session-lifecycle.md` §2.2, T7].
1659 let transport = ScriptedTransport::new(vec![Some(vec![
1660 Say::Line(CONOK),
1661 Say::AwaitControls(1),
1662 Say::Line("END,31,session destroyed"),
1663 ])]);
1664 let log = transport.log();
1665
1666 let (client, mut events) = Client::assemble(
1667 session::connect(transport, session_options()).expect("a valid session"),
1668 capacities(16),
1669 )
1670 .await
1671 .expect("the session bound");
1672
1673 tokio::time::timeout(Duration::from_secs(10), client.disconnect())
1674 .await
1675 .expect("disconnect must not hang")
1676 .expect("the session was destroyed cleanly");
1677
1678 let recorded = log.lock().expect("the log is not poisoned").clone();
1679 assert!(
1680 recorded
1681 .controls
1682 .iter()
1683 .any(|parameters| parameters.contains("LS_op=destroy")),
1684 "the destroy reached the wire: {:?}",
1685 recorded.controls
1686 );
1687 assert!(recorded.closes >= 1, "the transport was closed");
1688 // Every task has finished, so the streams they feed have ended.
1689 let remaining: Vec<SessionEvent> = std::iter::from_fn(|| events.next().now_or_never())
1690 .flatten()
1691 .collect();
1692 assert!(
1693 matches!(remaining.last(), Some(SessionEvent::Closed(_))),
1694 "the last event is terminal: {remaining:?}"
1695 );
1696 }
1697
1698 /// C-02 and C-09 together: a caller that stopped reading must not be able
1699 /// to make `disconnect` hang.
1700 #[tokio::test(start_paused = true)]
1701 async fn test_disconnect_completes_even_with_an_unread_event_stream() {
1702 // Three session-level notifications and one slot to put them in. The
1703 // stream is held and never read, so the router wedges on the second and
1704 // stops taking anything from the driver — which is exactly the state in
1705 // which a shutdown used to be impossible.
1706 let transport = ScriptedTransport::new(vec![Some(vec![
1707 Say::Line(CONOK),
1708 Say::Line("SERVNAME,Lightstreamer HTTP Server"),
1709 Say::Line("CLIENTIP,127.0.0.1"),
1710 Say::Line("CONS,unlimited"),
1711 ])]);
1712 let log = transport.log();
1713
1714 let (client, events) = Client::assemble(
1715 session::connect(transport, session_options()).expect("a valid session"),
1716 capacities(1),
1717 )
1718 .await
1719 .expect("the session bound");
1720 // Let the router reach the wedge before asking it to stop.
1721 tokio::time::sleep(Duration::from_millis(20)).await;
1722
1723 let disconnected = tokio::time::timeout(Duration::from_secs(60), client.disconnect()).await;
1724 assert!(
1725 disconnected.is_ok(),
1726 "a stream nobody reads must not hold the shutdown open"
1727 );
1728 assert!(
1729 log.lock().expect("the log is not poisoned").closes >= 1,
1730 "the socket is closed on the forced path too"
1731 );
1732 drop(events);
1733 }
1734
1735 /// C-04. A subscription's stream must be registered *before* the request
1736 /// that could produce an event for it is queued, and the registration must
1737 /// be undone if that request cannot be queued at all.
1738 ///
1739 /// The window is made observable by giving the session a single command
1740 /// slot, filling it, and never running the driver: the submission is then
1741 /// stuck, and the registration either already happened or never will.
1742 #[tokio::test(start_paused = true)]
1743 async fn test_subscribe_registers_the_stream_before_it_submits_the_request() {
1744 let options = crate::session::options::SessionOptions {
1745 command_capacity: std::num::NonZeroUsize::MIN,
1746 ..Default::default()
1747 };
1748 let Ok((driver, handle, session_events)) = crate::session::connect(NullTransport, options)
1749 else {
1750 panic!("a default session configuration is valid");
1751 };
1752
1753 // Occupy the one command slot. The driver is deliberately never run,
1754 // so nothing will ever free it.
1755 let filler = handle
1756 .allocate_key()
1757 .expect("the key space is not exhausted");
1758 assert!(handle.unsubscribe(filler).await.is_ok());
1759
1760 let (router_tx, mut router_rx) = mpsc::unbounded_channel();
1761 let (alive_tx, _alive_rx) = mpsc::channel(1);
1762 let client = Client {
1763 id: a_client_id(),
1764 handle: handle.clone(),
1765 router: router_tx,
1766 update_capacity: std::num::NonZeroUsize::new(8).unwrap_or(std::num::NonZeroUsize::MIN),
1767 shutdown_timeout: Duration::from_secs(5),
1768 tasks: Vec::new(),
1769 stop_on_drop: false,
1770 _alive: alive_tx,
1771 };
1772
1773 let subscribing = tokio::spawn(async move {
1774 client
1775 .subscribe(Subscription::new(
1776 SubscriptionMode::Merge,
1777 ItemGroup::from_items(["item1"]).expect("a valid group"),
1778 FieldSchema::from_fields(["price"]).expect("a valid schema"),
1779 ))
1780 .await
1781 .is_ok()
1782 });
1783 tokio::time::sleep(Duration::from_millis(20)).await;
1784
1785 assert!(
1786 !subscribing.is_finished(),
1787 "the submission is blocked, which is the window under test"
1788 );
1789 assert!(
1790 matches!(router_rx.try_recv(), Ok(RouterCommand::Register { .. })),
1791 "the stream is registered before the request is submitted"
1792 );
1793
1794 // Now make the submission fail outright: the registration must be
1795 // rolled back, and no unsubscription asked for, because nothing was
1796 // ever subscribed.
1797 drop((driver, session_events, handle));
1798 assert!(
1799 !tokio::time::timeout(Duration::from_secs(5), subscribing)
1800 .await
1801 .expect("the submission fails once the driver is gone")
1802 .expect("the task did not panic"),
1803 "a subscription that could not be submitted is an error"
1804 );
1805 assert!(
1806 matches!(router_rx.try_recv(), Ok(RouterCommand::Unregister { .. })),
1807 "the registration is rolled back, not left behind"
1808 );
1809 }
1810
1811 /// A-07. Two clients number their subscriptions from one, so their handles
1812 /// carry the same number. Spending one on the other must fail with a typed
1813 /// error and must not reach the wire.
1814 #[tokio::test(start_paused = true)]
1815 async fn test_a_handle_from_one_client_cannot_unsubscribe_another() {
1816 async fn a_client() -> (Client, Updates, std::sync::Arc<std::sync::Mutex<ScriptLog>>) {
1817 let transport = ScriptedTransport::new(vec![Some(vec![
1818 Say::Line(CONOK),
1819 Say::AwaitControls(usize::MAX),
1820 ])]);
1821 let log = transport.log();
1822 let (client, events) = Client::assemble(
1823 session::connect(transport, session_options()).expect("a valid session"),
1824 capacities(16),
1825 )
1826 .await
1827 .expect("the session bound");
1828 drop(events);
1829 let updates = client
1830 .subscribe(Subscription::new(
1831 SubscriptionMode::Merge,
1832 ItemGroup::from_items(["item1"]).expect("a valid group"),
1833 FieldSchema::from_fields(["price"]).expect("a valid schema"),
1834 ))
1835 .await
1836 .expect("the subscription was queued");
1837 (client, updates, log)
1838 }
1839
1840 // The `Updates` streams are held, not dropped: dropping one
1841 // unsubscribes, which is the thing a cross-client unsubscribe must not
1842 // be confused with.
1843 let (first, first_updates, _) = a_client().await;
1844 let (second, second_updates, second_log) = a_client().await;
1845 let (first_id, second_id) = (first_updates.id(), second_updates.id());
1846
1847 // The premise: the numbers really do collide.
1848 assert_eq!(
1849 first_id.get(),
1850 second_id.get(),
1851 "each client numbers its subscriptions from one"
1852 );
1853 assert_ne!(first_id, second_id, "the handles are still distinct");
1854
1855 // The subscription each client did ask for reached the wire, so the
1856 // absence of a `delete` below is not the transport being idle.
1857 tokio::time::sleep(Duration::from_millis(20)).await;
1858 let before = second_log.lock().expect("the log is not poisoned").clone();
1859 assert!(
1860 before
1861 .controls
1862 .iter()
1863 .any(|parameters| parameters.contains("LS_op=add")),
1864 "the second client's own subscription was sent: {:?}",
1865 before.controls
1866 );
1867
1868 match second.unsubscribe(first_id).await {
1869 Err(Error::ForeignSubscription { id }) => assert_eq!(id, first_id.get()),
1870 other => panic!("a foreign handle was accepted: {other:?}"),
1871 }
1872
1873 tokio::time::sleep(Duration::from_millis(20)).await;
1874 let after = second_log.lock().expect("the log is not poisoned").clone();
1875 assert!(
1876 !after
1877 .controls
1878 .iter()
1879 .any(|parameters| parameters.contains("LS_op=delete")),
1880 "nothing was sent for a handle this client does not own: {:?}",
1881 after.controls
1882 );
1883
1884 // Its own handle still works.
1885 assert!(second.unsubscribe(second_id).await.is_ok());
1886 drop((first, second, first_updates, second_updates));
1887 }
1888
1889 /// C-05. A blocked delivery that discovers a dropped receiver must not
1890 /// swallow the unsubscription the drop itself asked for.
1891 #[tokio::test]
1892 async fn test_a_stream_dropped_while_the_router_is_blocked_still_unsubscribes() {
1893 let mut harness = harness(8);
1894 let id = SubscriptionId::new(harness.client, a_key().await);
1895 // One slot: the second event has nowhere to go, so the router blocks
1896 // inside the send — the state in which the receiver then disappears.
1897 let (events_tx, events_rx) = mpsc::channel(1);
1898 assert!(
1899 harness
1900 .commands
1901 .send(RouterCommand::Register {
1902 id,
1903 events: events_tx,
1904 })
1905 .is_ok()
1906 );
1907 let updates = Updates::new(id, events_rx, harness.commands.clone());
1908
1909 assert!(
1910 harness
1911 .session_tx
1912 .send(data(
1913 id,
1914 WireSubscriptionEvent::Activated {
1915 item_count: 1,
1916 field_count: 1,
1917 command_fields: None,
1918 }
1919 ))
1920 .await
1921 .is_ok()
1922 );
1923 for dropped_count in [1, 2] {
1924 assert!(
1925 harness
1926 .session_tx
1927 .send(data(
1928 id,
1929 WireSubscriptionEvent::Overflow {
1930 item_index: 1,
1931 dropped_count,
1932 }
1933 ))
1934 .await
1935 .is_ok()
1936 );
1937 }
1938 // The router is now blocked on a full stream. Dropping it is what the
1939 // caller does when it walks away.
1940 tokio::task::yield_now().await;
1941 drop(updates);
1942
1943 match tokio::time::timeout(Duration::from_secs(5), harness.unsubscribed.recv()).await {
1944 Ok(Some(key)) => assert_eq!(key.get(), id.get()),
1945 other => panic!("the server was left subscribed: {other:?}"),
1946 }
1947 // Exactly one: cleanup is not duplicated either.
1948 assert!(harness.unsubscribed.recv().now_or_never().is_none());
1949 }
1950
1951 /// C-02. The router honours a stop while a caller's stream is full, so a
1952 /// client being torn down is not held open by a consumer that walked away.
1953 #[tokio::test]
1954 async fn test_the_router_stops_while_a_callers_stream_is_full() {
1955 let harness = harness(1);
1956 let id = SubscriptionId::new(harness.client, a_key().await);
1957 let (events_tx, _events_rx) = mpsc::channel(1);
1958 assert!(
1959 harness
1960 .commands
1961 .send(RouterCommand::Register {
1962 id,
1963 events: events_tx,
1964 })
1965 .is_ok()
1966 );
1967 for _ in 0..4 {
1968 let _ = harness
1969 .session_tx
1970 .send(data(
1971 id,
1972 WireSubscriptionEvent::Overflow {
1973 item_index: 1,
1974 dropped_count: 1,
1975 },
1976 ))
1977 .await;
1978 }
1979
1980 assert!(harness.stop.send(true).is_ok());
1981 // The router drops every sender as it stops, which is what ends the
1982 // caller's streams.
1983 match tokio::time::timeout(Duration::from_secs(5), harness.session_tx.closed()).await {
1984 Ok(()) => {}
1985 Err(_) => panic!("the router did not stop while a stream was full"),
1986 }
1987 }
1988}