Skip to main content

ccxt_core/ws_client/
config.rs

1//! WebSocket configuration types.
2
3use rand::Rng;
4use std::time::Duration;
5
6/// Exponential backoff configuration for reconnection.
7#[derive(Debug, Clone)]
8pub struct BackoffConfig {
9    /// Base delay for first retry (default: 1 second)
10    pub base_delay: Duration,
11    /// Maximum delay cap (default: 60 seconds)
12    pub max_delay: Duration,
13    /// Jitter factor (0.0 - 1.0, default: 0.25 for 25%)
14    pub jitter_factor: f64,
15    /// Multiplier for exponential growth (default: 2.0)
16    pub multiplier: f64,
17}
18
19impl Default for BackoffConfig {
20    fn default() -> Self {
21        Self {
22            base_delay: Duration::from_secs(1),
23            max_delay: Duration::from_secs(60),
24            jitter_factor: 0.25,
25            multiplier: 2.0,
26        }
27    }
28}
29
30/// Calculates retry delay with exponential backoff and jitter.
31#[derive(Debug, Clone)]
32pub struct BackoffStrategy {
33    config: BackoffConfig,
34}
35
36impl BackoffStrategy {
37    /// Creates a new backoff strategy with the given configuration.
38    pub fn new(config: BackoffConfig) -> Self {
39        Self { config }
40    }
41
42    /// Creates a new backoff strategy with default configuration.
43    pub fn with_defaults() -> Self {
44        Self::new(BackoffConfig::default())
45    }
46
47    /// Returns a reference to the underlying configuration.
48    pub fn config(&self) -> &BackoffConfig {
49        &self.config
50    }
51
52    /// Calculates delay for the given attempt number.
53    #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
54    pub fn calculate_delay(&self, attempt: u32) -> Duration {
55        let base_ms = self.config.base_delay.as_millis() as f64;
56        let multiplier = self.config.multiplier;
57        let max_ms = self.config.max_delay.as_millis() as f64;
58
59        let exponential_delay_ms = base_ms * multiplier.powi(attempt as i32);
60        let capped_delay_ms = exponential_delay_ms.min(max_ms);
61
62        let jitter_ms = if self.config.jitter_factor > 0.0 {
63            let jitter_range = capped_delay_ms * self.config.jitter_factor;
64            rand::rng().random::<f64>() * jitter_range
65        } else {
66            0.0
67        };
68
69        Duration::from_millis((capped_delay_ms + jitter_ms) as u64)
70    }
71
72    /// Calculates the base delay (without jitter) for the given attempt number.
73    #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
74    pub fn calculate_delay_without_jitter(&self, attempt: u32) -> Duration {
75        let base_ms = self.config.base_delay.as_millis() as f64;
76        let multiplier = self.config.multiplier;
77        let max_ms = self.config.max_delay.as_millis() as f64;
78
79        let exponential_delay_ms = base_ms * multiplier.powi(attempt as i32);
80        let capped_delay_ms = exponential_delay_ms.min(max_ms);
81
82        Duration::from_millis(capped_delay_ms as u64)
83    }
84}
85
86/// Default maximum number of subscriptions.
87pub const DEFAULT_MAX_SUBSCRIPTIONS: usize = 100;
88
89/// Default shutdown timeout in milliseconds.
90pub const DEFAULT_SHUTDOWN_TIMEOUT: u64 = 5000;
91
92/// Default message channel capacity.
93///
94/// This is the maximum number of messages that can be buffered before
95/// backpressure is applied. A value of 1000 provides a good balance
96/// between memory usage and handling burst traffic.
97pub const DEFAULT_MESSAGE_CHANNEL_CAPACITY: usize = 1000;
98
99/// Default write channel capacity.
100///
101/// This is the maximum number of outgoing messages that can be buffered.
102/// A smaller value (100) is used since writes are typically less frequent
103/// than incoming messages.
104pub const DEFAULT_WRITE_CHANNEL_CAPACITY: usize = 100;
105
106/// Backpressure strategy when message channel is full.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub enum BackpressureStrategy {
109    /// Drop the oldest message in the queue (default).
110    /// This ensures the most recent data is always available.
111    #[default]
112    DropOldest,
113    /// Drop the newest message (the one being sent).
114    /// This preserves message ordering but may lose recent updates.
115    DropNewest,
116    /// Block until space is available.
117    /// Warning: This can cause the WebSocket read loop to stall.
118    Block,
119}
120
121/// WebSocket connection configuration.
122#[derive(Debug, Clone)]
123pub struct WsConfig {
124    /// WebSocket server URL
125    pub url: String,
126    /// Connection timeout in milliseconds
127    pub connect_timeout: u64,
128    /// Ping interval in milliseconds
129    pub ping_interval: u64,
130    /// Reconnection delay in milliseconds (legacy, use `backoff_config` for exponential backoff)
131    pub reconnect_interval: u64,
132    /// Maximum reconnection attempts before giving up
133    pub max_reconnect_attempts: u32,
134    /// Enable automatic reconnection on disconnect
135    pub auto_reconnect: bool,
136    /// Enable message compression
137    pub enable_compression: bool,
138    /// Pong timeout in milliseconds
139    pub pong_timeout: u64,
140    /// Exponential backoff configuration for reconnection.
141    pub backoff_config: BackoffConfig,
142    /// Maximum number of subscriptions allowed.
143    pub max_subscriptions: usize,
144    /// Shutdown timeout in milliseconds.
145    pub shutdown_timeout: u64,
146    /// Message channel capacity (incoming messages buffer size).
147    ///
148    /// When this limit is reached, backpressure is applied according to
149    /// `backpressure_strategy`. Default: 1000 messages.
150    pub message_channel_capacity: usize,
151    /// Write channel capacity (outgoing messages buffer size).
152    ///
153    /// Default: 100 messages.
154    pub write_channel_capacity: usize,
155    /// Strategy to use when message channel is full.
156    ///
157    /// Default: `DropOldest` to ensure most recent data is available.
158    pub backpressure_strategy: BackpressureStrategy,
159    /// Maximum message size in bytes.
160    ///
161    /// Default: 128 MiB (134,217,728 bytes) if None.
162    pub max_message_size: Option<usize>,
163    /// Maximum frame size in bytes.
164    ///
165    /// Default: 32 MiB (33,554,432 bytes) if None.
166    pub max_frame_size: Option<usize>,
167}
168
169impl Default for WsConfig {
170    fn default() -> Self {
171        Self {
172            url: String::new(),
173            connect_timeout: 10000,
174            ping_interval: 30000,
175            reconnect_interval: 5000,
176            max_reconnect_attempts: 5,
177            auto_reconnect: true,
178            enable_compression: false,
179            pong_timeout: 30000, // 30 seconds default (detects zombie connections faster)
180            backoff_config: BackoffConfig::default(),
181            max_subscriptions: DEFAULT_MAX_SUBSCRIPTIONS,
182            shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
183            message_channel_capacity: DEFAULT_MESSAGE_CHANNEL_CAPACITY,
184            write_channel_capacity: DEFAULT_WRITE_CHANNEL_CAPACITY,
185            backpressure_strategy: BackpressureStrategy::default(),
186            max_message_size: Some(128 * 1024 * 1024),
187            max_frame_size: Some(32 * 1024 * 1024),
188        }
189    }
190}