Skip to main content

lightstreamer_rs/config/
options.rs

1//! Timeouts, limits and the reconnection policy.
2//!
3//! # On defaults
4//!
5//! The specification states of every timing it defines that its minimum and
6//! maximum are configured on the **server**, and gives no numeric default for
7//! any of them; a client must treat the values echoed back at bind time as the
8//! only authoritative ones [`docs/spec/02-session-lifecycle.md` §8.7]. Every
9//! default in this module is therefore **a choice of this crate**, documented
10//! as such, and every one is overridable. None is presented as a protocol
11//! requirement.
12//!
13//! # Where validation happens
14//!
15//! [`ServerAddress`](crate::ServerAddress) and
16//! [`AdapterSet`](crate::AdapterSet) check themselves at construction, because
17//! each is one value with one rule. The knobs in this module are checked
18//! together, when [`ClientConfig::builder`](crate::ClientConfig::builder) is
19//! built: several of them constrain each other (a retry ceiling below its
20//! first delay, for instance) and checking them one at a time could not catch
21//! that. Until then a [`ConnectionOptions`] is inert — it opens nothing and
22//! sends nothing — so an unchecked one cannot reach a server.
23
24use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
25use std::time::Duration;
26
27/// Default limit on how long a session-opening request may go unanswered.
28///
29/// A choice of this crate; the specification defines no such limit.
30const DEFAULT_OPEN_TIMEOUT: Duration = Duration::from_secs(10);
31
32/// Default grace added to the server's own keepalive interval before a silent
33/// stream is declared dead.
34///
35/// The specification requires a client-side "configurable timeout" on top of
36/// the keepalive interval but names no value
37/// [`docs/spec/02-session-lifecycle.md` §8.1]. Three seconds is this crate's
38/// choice.
39const DEFAULT_KEEPALIVE_SLACK: Duration = Duration::from_secs(3);
40
41/// Default delay before the first reconnection attempt. A choice of this crate.
42const DEFAULT_RETRY_INITIAL: Duration = Duration::from_millis(500);
43
44/// Default ceiling on the reconnection delay. A choice of this crate.
45const DEFAULT_RETRY_MAX: Duration = Duration::from_secs(30);
46
47/// Default cap on consecutive failed reconnection attempts. A choice of this
48/// crate.
49const DEFAULT_RETRY_ATTEMPTS: u32 = 8;
50
51/// Default capacity of the session-event stream.
52const DEFAULT_SESSION_EVENT_CAPACITY: usize = 256;
53
54/// Default capacity of each subscription's update stream.
55const DEFAULT_UPDATE_CAPACITY: usize = 1024;
56
57/// Which transport carries the session.
58///
59/// TLCP defines two: a WebSocket, and HTTP — the latter in a streaming and a
60/// long-polling flavour [`docs/spec/01-foundations.md` §6]. All three are
61/// implemented; the enum is `#[non_exhaustive]` so that a fourth could be added
62/// later as a minor version bump rather than a breaking one.
63///
64/// Forcing a transport is offered because a caller sometimes knows something
65/// this crate cannot discover — a proxy that mangles WebSocket upgrades, say,
66/// for which one of the HTTP variants is the way through.
67///
68/// The two HTTP variants differ in how a stream connection ends: streaming
69/// holds one response open until its content length is reached and then rebinds
70/// (transition T3), whereas long polling returns each cycle's notifications and
71/// rebinds every cycle (transition T4)
72/// [`docs/spec/02-session-lifecycle.md` §2.2, §7]. WebSocket does neither.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
74#[repr(u8)]
75#[non_exhaustive]
76pub enum Transport {
77    /// A single WebSocket carrying both the stream and the control requests
78    /// [`docs/spec/01-foundations.md` §6.2].
79    #[default]
80    WebSocket,
81    /// HTTP streaming: one long-lived response body of near-infinite length,
82    /// with each control request on its own connection
83    /// [`docs/spec/01-foundations.md` §3; §6.1].
84    HttpStreaming,
85    /// HTTP long polling: each connection returns the notifications ready now
86    /// and closes, and the session rebinds each cycle
87    /// [`docs/spec/02-session-lifecycle.md` §7].
88    HttpPolling,
89}
90
91impl Transport {
92    /// A short name, for logs and error messages.
93    #[must_use]
94    #[inline]
95    pub const fn as_str(self) -> &'static str {
96        match self {
97            Self::WebSocket => "websocket",
98            Self::HttpStreaming => "http-streaming",
99            Self::HttpPolling => "http-polling",
100        }
101    }
102}
103
104/// How reconnection attempts are spaced and how many are allowed.
105///
106/// When a stream connection breaks, this crate reconnects on its own — that is
107/// the whole point of a protocol client. What it will not do is hide the
108/// consequences: every attempt and every outcome is reported on the session
109/// event stream, and when this policy's budget runs out the session is
110/// reported definitively lost rather than retried forever in silence
111/// (`docs/adr/0005-recovery-is-visible-in-the-event-stream.md`).
112///
113/// Delays follow **full jitter**: the base delay doubles after each failure up
114/// to [`RetryPolicy::max_delay`], and the delay actually waited is drawn
115/// uniformly from zero to that base. Jitter is this crate's choice, not a
116/// protocol rule; it exists so that a fleet of clients knocked off a server at
117/// the same instant does not return in lockstep.
118///
119/// A successful bind resets the schedule, so a session that reconnects cleanly
120/// never carries delay over from an unrelated earlier failure.
121///
122/// # Examples
123///
124/// ```
125/// use std::time::Duration;
126/// use lightstreamer_rs::RetryPolicy;
127///
128/// // Give up after three failures instead of the default eight.
129/// let policy = RetryPolicy::default().with_max_attempts(std::num::NonZeroU32::new(3));
130/// assert_eq!(policy.max_attempts().map(|n| n.get()), Some(3));
131/// ```
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct RetryPolicy {
134    initial_delay: Duration,
135    max_delay: Duration,
136    max_attempts: Option<NonZeroU32>,
137}
138
139impl Default for RetryPolicy {
140    /// 500 ms growing to 30 s, giving up after 8 consecutive failures — which
141    /// spans roughly two minutes of retrying. Every number is a choice of this
142    /// crate.
143    fn default() -> Self {
144        Self {
145            initial_delay: DEFAULT_RETRY_INITIAL,
146            max_delay: DEFAULT_RETRY_MAX,
147            max_attempts: NonZeroU32::new(DEFAULT_RETRY_ATTEMPTS),
148        }
149    }
150}
151
152impl RetryPolicy {
153    /// Sets the delay before the first reconnection attempt.
154    #[must_use = "builders do nothing unless the result is used"]
155    pub const fn with_initial_delay(mut self, delay: Duration) -> Self {
156        self.initial_delay = delay;
157        self
158    }
159
160    /// Sets the ceiling the delay grows to and does not exceed.
161    #[must_use = "builders do nothing unless the result is used"]
162    pub const fn with_max_delay(mut self, delay: Duration) -> Self {
163        self.max_delay = delay;
164        self
165    }
166
167    /// Sets how many consecutive failures are allowed before the session is
168    /// reported definitively lost.
169    ///
170    /// `None` retries forever. That is permitted, but then nothing ever tells
171    /// the caller "this is not coming back" except the individual attempt
172    /// events, so choose it deliberately.
173    #[must_use = "builders do nothing unless the result is used"]
174    pub const fn with_max_attempts(mut self, attempts: Option<NonZeroU32>) -> Self {
175        self.max_attempts = attempts;
176        self
177    }
178
179    /// The delay before the first reconnection attempt.
180    #[must_use]
181    #[inline]
182    pub const fn initial_delay(&self) -> Duration {
183        self.initial_delay
184    }
185
186    /// The ceiling on the reconnection delay.
187    #[must_use]
188    #[inline]
189    pub const fn max_delay(&self) -> Duration {
190        self.max_delay
191    }
192
193    /// How many consecutive failures are allowed, or `None` for no limit.
194    #[must_use]
195    #[inline]
196    pub const fn max_attempts(&self) -> Option<NonZeroU32> {
197        self.max_attempts
198    }
199}
200
201/// Timeouts, buffer sizes and the reconnection policy for one client.
202///
203/// Every field has a default that works against a normally configured server;
204/// change only what you have a reason to change. See the module documentation
205/// for why these are checked when the [`ClientConfig`](crate::ClientConfig) is
206/// built rather than one at a time.
207///
208/// # Examples
209///
210/// ```
211/// use std::time::Duration;
212/// use lightstreamer_rs::ConnectionOptions;
213///
214/// let options = ConnectionOptions::default()
215///     .with_open_timeout(Duration::from_secs(5))
216///     .with_send_sync(false);
217/// assert_eq!(options.open_timeout(), Duration::from_secs(5));
218/// ```
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct ConnectionOptions {
221    open_timeout: Duration,
222    keepalive_slack: Duration,
223    keepalive_hint: Option<Duration>,
224    inactivity_commitment: Option<Duration>,
225    send_sync: bool,
226    content_length: Option<NonZeroU64>,
227    retry: RetryPolicy,
228    session_event_capacity: NonZeroUsize,
229    update_capacity: NonZeroUsize,
230}
231
232impl Default for ConnectionOptions {
233    fn default() -> Self {
234        Self {
235            open_timeout: DEFAULT_OPEN_TIMEOUT,
236            keepalive_slack: DEFAULT_KEEPALIVE_SLACK,
237            keepalive_hint: None,
238            inactivity_commitment: None,
239            send_sync: true,
240            content_length: None,
241            retry: RetryPolicy::default(),
242            session_event_capacity: NonZeroUsize::new(DEFAULT_SESSION_EVENT_CAPACITY)
243                .unwrap_or(NonZeroUsize::MIN),
244            update_capacity: NonZeroUsize::new(DEFAULT_UPDATE_CAPACITY)
245                .unwrap_or(NonZeroUsize::MIN),
246        }
247    }
248}
249
250impl ConnectionOptions {
251    /// How long a session-opening request may go unanswered before the attempt
252    /// is abandoned and retried.
253    ///
254    /// This is a limit of this crate: the protocol defines none, and without
255    /// one a server that accepts a connection and then says nothing would wedge
256    /// the client forever. Default: 10 s.
257    #[must_use = "builders do nothing unless the result is used"]
258    pub const fn with_open_timeout(mut self, timeout: Duration) -> Self {
259        self.open_timeout = timeout;
260        self
261    }
262
263    /// Extra grace beyond the server's own keepalive interval before a silent
264    /// stream is treated as dead and recovered.
265    ///
266    /// The server promises to send *something* — a keepalive probe if nothing
267    /// else — at least this often, and tells the client the interval when the
268    /// session binds [`docs/spec/02-session-lifecycle.md` §8.1]. This value is
269    /// added on top of that negotiated interval to absorb one late probe.
270    /// Default: 3 s.
271    ///
272    /// The negotiated interval always wins over
273    /// [`ConnectionOptions::with_keepalive_hint`]; this slack is the only part
274    /// of the liveness budget the caller controls outright.
275    #[must_use = "builders do nothing unless the result is used"]
276    pub const fn with_keepalive_slack(mut self, slack: Duration) -> Self {
277        self.keepalive_slack = slack;
278        self
279    }
280
281    /// Asks the server for a particular keepalive interval.
282    ///
283    /// A **hint**, not a setting: the server clamps it to its own configured
284    /// minimum and maximum and reports the interval it actually adopted, which
285    /// is what this client then enforces
286    /// [`docs/spec/02-session-lifecycle.md` §8.1]. Leave it unset to let the
287    /// server choose, which is the default.
288    #[must_use = "builders do nothing unless the result is used"]
289    pub const fn with_keepalive_hint(mut self, interval: Option<Duration>) -> Self {
290        self.keepalive_hint = interval;
291        self
292    }
293
294    /// Commits to sending the server something at least this often.
295    ///
296    /// If the server hears nothing from the client for longer than this it may
297    /// conclude the client is stuck and act on it
298    /// [`docs/spec/03-requests.md` §2.1]. Setting it makes this crate send a
299    /// heartbeat whenever nothing else has been sent, at half the committed
300    /// interval — half being this crate's choice, so that one lost heartbeat
301    /// does not break the commitment.
302    ///
303    /// Unset by default: without a commitment there is nothing to keep, and
304    /// the server applies no such check.
305    #[must_use = "builders do nothing unless the result is used"]
306    pub const fn with_inactivity_commitment(mut self, interval: Option<Duration>) -> Self {
307        self.inactivity_commitment = interval;
308        self
309    }
310
311    /// Whether to let the server send its periodic clock-synchronisation
312    /// notifications.
313    ///
314    /// They carry the seconds elapsed on the server since the session began,
315    /// which lets a client detect that its own machine has been suspended
316    /// [`docs/spec/03-requests.md` §2.1]. This crate surfaces them on the
317    /// session event stream and does not act on them. Default: enabled.
318    #[must_use = "builders do nothing unless the result is used"]
319    pub const fn with_send_sync(mut self, enabled: bool) -> Self {
320        self.send_sync = enabled;
321        self
322    }
323
324    /// Asks the server to end each stream connection after this many bytes,
325    /// forcing a clean rebind.
326    ///
327    /// Chiefly a defence against intermediaries that buffer an endless
328    /// response; the server raises a value it considers too low
329    /// [`docs/spec/03-requests.md` §2.1]. Leave it unset to let the server
330    /// choose, which is the default. A rebind triggered this way preserves the
331    /// session and every subscription.
332    #[must_use = "builders do nothing unless the result is used"]
333    pub const fn with_content_length(mut self, bytes: Option<NonZeroU64>) -> Self {
334        self.content_length = bytes;
335        self
336    }
337
338    /// Sets the reconnection policy.
339    #[must_use = "builders do nothing unless the result is used"]
340    pub const fn with_retry(mut self, retry: RetryPolicy) -> Self {
341        self.retry = retry;
342        self
343    }
344
345    /// Capacity of the client's session event stream.
346    ///
347    /// The stream is bounded and **never drops**: see
348    /// [`SessionEvents`](crate::SessionEvents) for what that means for a
349    /// consumer that falls behind. Default: 256 events.
350    #[must_use = "builders do nothing unless the result is used"]
351    pub const fn with_session_event_capacity(mut self, capacity: NonZeroUsize) -> Self {
352        self.session_event_capacity = capacity;
353        self
354    }
355
356    /// Capacity of each subscription's update stream.
357    ///
358    /// Bounded, and **never drops**: see [`Updates`](crate::Updates). Default:
359    /// 1024 events per subscription.
360    #[must_use = "builders do nothing unless the result is used"]
361    pub const fn with_update_capacity(mut self, capacity: NonZeroUsize) -> Self {
362        self.update_capacity = capacity;
363        self
364    }
365
366    /// How long a session-opening request may go unanswered.
367    #[must_use]
368    #[inline]
369    pub const fn open_timeout(&self) -> Duration {
370        self.open_timeout
371    }
372
373    /// Grace beyond the negotiated keepalive interval.
374    #[must_use]
375    #[inline]
376    pub const fn keepalive_slack(&self) -> Duration {
377        self.keepalive_slack
378    }
379
380    /// The keepalive interval asked of the server, if any.
381    #[must_use]
382    #[inline]
383    pub const fn keepalive_hint(&self) -> Option<Duration> {
384        self.keepalive_hint
385    }
386
387    /// The inactivity commitment made to the server, if any.
388    #[must_use]
389    #[inline]
390    pub const fn inactivity_commitment(&self) -> Option<Duration> {
391        self.inactivity_commitment
392    }
393
394    /// Whether clock-synchronisation notifications are enabled.
395    #[must_use]
396    #[inline]
397    pub const fn send_sync(&self) -> bool {
398        self.send_sync
399    }
400
401    /// The requested stream-connection byte budget, if any.
402    #[must_use]
403    #[inline]
404    pub const fn content_length(&self) -> Option<NonZeroU64> {
405        self.content_length
406    }
407
408    /// The reconnection policy.
409    #[must_use]
410    #[inline]
411    pub const fn retry(&self) -> RetryPolicy {
412        self.retry
413    }
414
415    /// Capacity of the session event stream.
416    #[must_use]
417    #[inline]
418    pub const fn session_event_capacity(&self) -> NonZeroUsize {
419        self.session_event_capacity
420    }
421
422    /// Capacity of each subscription's update stream.
423    #[must_use]
424    #[inline]
425    pub const fn update_capacity(&self) -> NonZeroUsize {
426        self.update_capacity
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_options_defaults_are_the_documented_ones() {
436        let options = ConnectionOptions::default();
437        assert_eq!(options.open_timeout(), Duration::from_secs(10));
438        assert_eq!(options.keepalive_slack(), Duration::from_secs(3));
439        assert_eq!(options.keepalive_hint(), None);
440        assert_eq!(options.inactivity_commitment(), None);
441        assert!(options.send_sync());
442        assert_eq!(options.content_length(), None);
443    }
444
445    #[test]
446    fn test_retry_defaults_are_the_documented_ones() {
447        let retry = RetryPolicy::default();
448        assert_eq!(retry.initial_delay(), Duration::from_millis(500));
449        assert_eq!(retry.max_delay(), Duration::from_secs(30));
450        assert_eq!(retry.max_attempts().map(NonZeroU32::get), Some(8));
451    }
452
453    #[test]
454    fn test_retry_max_attempts_can_be_removed() {
455        let retry = RetryPolicy::default().with_max_attempts(None);
456        assert_eq!(retry.max_attempts(), None);
457    }
458
459    #[test]
460    fn test_options_setters_round_trip() {
461        let options = ConnectionOptions::default()
462            .with_open_timeout(Duration::from_secs(4))
463            .with_keepalive_slack(Duration::from_secs(1))
464            .with_keepalive_hint(Some(Duration::from_secs(5)))
465            .with_inactivity_commitment(Some(Duration::from_secs(20)))
466            .with_send_sync(false)
467            .with_content_length(NonZeroU64::new(1_000_000));
468        assert_eq!(options.open_timeout(), Duration::from_secs(4));
469        assert_eq!(options.keepalive_slack(), Duration::from_secs(1));
470        assert_eq!(options.keepalive_hint(), Some(Duration::from_secs(5)));
471        assert_eq!(
472            options.inactivity_commitment(),
473            Some(Duration::from_secs(20))
474        );
475        assert!(!options.send_sync());
476        assert_eq!(
477            options.content_length().map(NonZeroU64::get),
478            Some(1_000_000)
479        );
480    }
481
482    #[test]
483    fn test_transport_default_is_websocket() {
484        assert_eq!(Transport::default(), Transport::WebSocket);
485        assert_eq!(Transport::default().as_str(), "websocket");
486    }
487
488    #[test]
489    fn test_transport_variants_name_themselves() {
490        assert_eq!(Transport::HttpStreaming.as_str(), "http-streaming");
491        assert_eq!(Transport::HttpPolling.as_str(), "http-polling");
492    }
493}