Skip to main content

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}