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}