Skip to main content

ccxt_exchanges/binance/
time_sync.rs

1//! Time synchronization manager for Binance API.
2//!
3//! This module provides a thread-safe time synchronization mechanism that caches
4//! the time offset between local system time and Binance server time. This optimization
5//! reduces the number of network round-trips for signed API requests from 2 to 1.
6//!
7//! # Overview
8//!
9//! When making signed requests to Binance, a timestamp is required. Previously,
10//! each signed request required fetching the server time first. With `TimeSyncManager`,
11//! the time offset is cached and used to calculate server timestamps locally.
12//!
13//! # Example
14//!
15//! ```rust
16//! use ccxt_exchanges::binance::time_sync::{TimeSyncConfig, TimeSyncManager};
17//! use std::time::Duration;
18//!
19//! // Create with default configuration
20//! let manager = TimeSyncManager::new();
21//!
22//! // Or with custom configuration
23//! let config = TimeSyncConfig {
24//!     sync_interval: Duration::from_secs(60),
25//!     auto_sync: true,
26//!     max_offset_drift: 3000,
27//! };
28//! let manager = TimeSyncManager::with_config(config);
29//!
30//! // Simulate receiving server time and updating offset
31//! let server_time = 1704110400000i64; // Example server timestamp
32//! manager.update_offset(server_time);
33//!
34//! // Get estimated server timestamp using cached offset
35//! let estimated_server_time = manager.get_server_timestamp();
36//! ```
37
38use ccxt_core::time::TimestampUtils;
39use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
40use std::time::Duration;
41
42/// Time synchronization configuration.
43///
44/// Controls the behavior of the `TimeSyncManager` including sync intervals,
45/// automatic sync, and drift tolerance.
46///
47/// # Example
48///
49/// ```rust
50/// use ccxt_exchanges::binance::time_sync::TimeSyncConfig;
51/// use std::time::Duration;
52///
53/// let config = TimeSyncConfig {
54///     sync_interval: Duration::from_secs(30),
55///     auto_sync: true,
56///     max_offset_drift: 5000,
57/// };
58/// ```
59#[derive(Debug, Clone)]
60pub struct TimeSyncConfig {
61    /// Sync interval duration.
62    ///
63    /// The time between automatic resyncs when `auto_sync` is enabled.
64    /// Default: 30 seconds.
65    pub sync_interval: Duration,
66
67    /// Enable automatic periodic sync.
68    ///
69    /// When enabled, `needs_resync()` will return `true` after the sync interval
70    /// has elapsed since the last sync.
71    /// Default: true.
72    pub auto_sync: bool,
73
74    /// Maximum allowed time offset drift in milliseconds.
75    ///
76    /// This value represents the maximum acceptable drift before forcing a resync.
77    /// Should be less than Binance's `recvWindow` (default 5000ms) to ensure
78    /// signed requests are accepted.
79    /// Default: 5000ms.
80    pub max_offset_drift: i64,
81}
82
83impl Default for TimeSyncConfig {
84    fn default() -> Self {
85        Self {
86            sync_interval: Duration::from_secs(30),
87            auto_sync: true,
88            max_offset_drift: 5000,
89        }
90    }
91}
92
93impl TimeSyncConfig {
94    /// Creates a new configuration with the specified sync interval.
95    ///
96    /// # Arguments
97    ///
98    /// * `sync_interval` - Duration between automatic resyncs
99    ///
100    /// # Example
101    ///
102    /// ```rust
103    /// use ccxt_exchanges::binance::time_sync::TimeSyncConfig;
104    /// use std::time::Duration;
105    ///
106    /// let config = TimeSyncConfig::with_interval(Duration::from_secs(60));
107    /// assert_eq!(config.sync_interval, Duration::from_secs(60));
108    /// ```
109    pub fn with_interval(sync_interval: Duration) -> Self {
110        Self {
111            sync_interval,
112            ..Default::default()
113        }
114    }
115
116    /// Creates a configuration with automatic sync disabled.
117    ///
118    /// Useful when you want to control sync timing manually.
119    ///
120    /// # Example
121    ///
122    /// ```rust
123    /// use ccxt_exchanges::binance::time_sync::TimeSyncConfig;
124    ///
125    /// let config = TimeSyncConfig::manual_sync_only();
126    /// assert!(!config.auto_sync);
127    /// ```
128    pub fn manual_sync_only() -> Self {
129        Self {
130            auto_sync: false,
131            ..Default::default()
132        }
133    }
134}
135
136/// Thread-safe time synchronization manager.
137///
138/// Maintains a cached time offset between local system time and Binance server time.
139/// Uses atomic operations for thread-safe access without locks.
140///
141/// # Thread Safety
142///
143/// All operations use atomic memory ordering:
144/// - `Ordering::Acquire` for reads to ensure visibility of prior writes
145/// - `Ordering::Release` for writes to ensure visibility to subsequent reads
146///
147/// # Example
148///
149/// ```rust
150/// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
151///
152/// let manager = TimeSyncManager::new();
153///
154/// // Check if sync is needed (always true initially)
155/// assert!(manager.needs_resync());
156///
157/// // Simulate server time update
158/// let server_time = 1704110400000i64;
159/// manager.update_offset(server_time);
160///
161/// // Now initialized
162/// assert!(manager.is_initialized());
163///
164/// // Get estimated server timestamp
165/// let timestamp = manager.get_server_timestamp();
166/// assert!(timestamp > 0);
167/// ```
168#[derive(Debug)]
169pub struct TimeSyncManager {
170    /// Cached time offset: server_time - local_time (in milliseconds).
171    ///
172    /// Positive value means server is ahead of local time.
173    /// Negative value means server is behind local time.
174    time_offset: AtomicI64,
175
176    /// Timestamp of last successful sync (local time in milliseconds).
177    last_sync_time: AtomicI64,
178
179    /// Whether initial sync has been performed.
180    initialized: AtomicBool,
181
182    /// Sync configuration.
183    config: TimeSyncConfig,
184}
185
186impl TimeSyncManager {
187    /// Creates a new `TimeSyncManager` with default configuration.
188    ///
189    /// # Example
190    ///
191    /// ```rust
192    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
193    ///
194    /// let manager = TimeSyncManager::new();
195    /// assert!(!manager.is_initialized());
196    /// ```
197    pub fn new() -> Self {
198        Self::with_config(TimeSyncConfig::default())
199    }
200
201    /// Creates a new `TimeSyncManager` with custom configuration.
202    ///
203    /// # Arguments
204    ///
205    /// * `config` - Time sync configuration
206    ///
207    /// # Example
208    ///
209    /// ```rust
210    /// use ccxt_exchanges::binance::time_sync::{TimeSyncConfig, TimeSyncManager};
211    /// use std::time::Duration;
212    ///
213    /// let config = TimeSyncConfig {
214    ///     sync_interval: Duration::from_secs(60),
215    ///     auto_sync: true,
216    ///     max_offset_drift: 3000,
217    /// };
218    /// let manager = TimeSyncManager::with_config(config);
219    /// ```
220    pub fn with_config(config: TimeSyncConfig) -> Self {
221        Self {
222            time_offset: AtomicI64::new(0),
223            last_sync_time: AtomicI64::new(0),
224            initialized: AtomicBool::new(false),
225            config,
226        }
227    }
228
229    /// Returns whether initial sync has been performed.
230    ///
231    /// # Returns
232    ///
233    /// `true` if `update_offset()` has been called at least once successfully.
234    ///
235    /// # Example
236    ///
237    /// ```rust
238    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
239    ///
240    /// let manager = TimeSyncManager::new();
241    /// assert!(!manager.is_initialized());
242    ///
243    /// manager.update_offset(1704110400000);
244    /// assert!(manager.is_initialized());
245    /// ```
246    #[inline]
247    pub fn is_initialized(&self) -> bool {
248        self.initialized.load(Ordering::Acquire)
249    }
250
251    /// Returns whether a resync is needed based on sync interval.
252    ///
253    /// Returns `true` if:
254    /// - The manager is not initialized, OR
255    /// - Auto sync is enabled AND the time since last sync exceeds the sync interval
256    ///
257    /// # Returns
258    ///
259    /// `true` if resync is needed, `false` otherwise.
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
265    ///
266    /// let manager = TimeSyncManager::new();
267    ///
268    /// // Always needs resync when not initialized
269    /// assert!(manager.needs_resync());
270    ///
271    /// // After initialization, depends on sync interval
272    /// manager.update_offset(1704110400000);
273    /// assert!(!manager.needs_resync()); // Just synced
274    /// ```
275    pub fn needs_resync(&self) -> bool {
276        // Always need sync if not initialized
277        if !self.is_initialized() {
278            return true;
279        }
280
281        // If auto sync is disabled, never need automatic resync
282        if !self.config.auto_sync {
283            return false;
284        }
285
286        // Check if sync interval has elapsed
287        let last_sync = self.last_sync_time.load(Ordering::Acquire);
288        let now = TimestampUtils::now_ms();
289        let elapsed = now.saturating_sub(last_sync);
290
291        elapsed >= self.config.sync_interval.as_millis() as i64
292    }
293
294    /// Gets the current cached time offset.
295    ///
296    /// The offset represents: `server_time - local_time` in milliseconds.
297    ///
298    /// # Returns
299    ///
300    /// The cached time offset in milliseconds.
301    ///
302    /// # Example
303    ///
304    /// ```rust
305    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
306    ///
307    /// let manager = TimeSyncManager::new();
308    /// assert_eq!(manager.get_offset(), 0); // Default offset
309    ///
310    /// // After sync, offset reflects the difference
311    /// // (actual value depends on local vs server time)
312    /// ```
313    #[inline]
314    pub fn get_offset(&self) -> i64 {
315        self.time_offset.load(Ordering::Acquire)
316    }
317
318    /// Calculates the estimated server timestamp using cached offset.
319    ///
320    /// Formula: `server_timestamp = local_time + offset`
321    ///
322    /// Uses saturating arithmetic to prevent overflow.
323    ///
324    /// # Returns
325    ///
326    /// Estimated server timestamp in milliseconds.
327    ///
328    /// # Example
329    ///
330    /// ```rust
331    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
332    ///
333    /// let manager = TimeSyncManager::new();
334    /// let timestamp = manager.get_server_timestamp();
335    /// assert!(timestamp > 0);
336    /// ```
337    #[inline]
338    pub fn get_server_timestamp(&self) -> i64 {
339        let local_time = TimestampUtils::now_ms();
340        let offset = self.get_offset();
341        local_time.saturating_add(offset)
342    }
343
344    /// Updates the time offset based on server time.
345    ///
346    /// This method should be called after fetching server time from the API.
347    /// It calculates the offset as: `offset = server_time - local_time`
348    ///
349    /// # Arguments
350    ///
351    /// * `server_time` - The server timestamp in milliseconds
352    ///
353    /// # Example
354    ///
355    /// ```rust
356    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
357    ///
358    /// let manager = TimeSyncManager::new();
359    ///
360    /// // Simulate receiving server time
361    /// let server_time = 1704110400000i64;
362    /// manager.update_offset(server_time);
363    ///
364    /// assert!(manager.is_initialized());
365    /// ```
366    pub fn update_offset(&self, server_time: i64) {
367        let local_time = TimestampUtils::now_ms();
368        let offset = server_time.saturating_sub(local_time);
369
370        // Update all fields atomically (in terms of visibility)
371        // Using Release ordering ensures these writes are visible to
372        // subsequent Acquire reads
373        self.time_offset.store(offset, Ordering::Release);
374        self.last_sync_time.store(local_time, Ordering::Release);
375        self.initialized.store(true, Ordering::Release);
376    }
377
378    /// Returns the last sync timestamp (local time).
379    ///
380    /// # Returns
381    ///
382    /// The local timestamp when the last sync occurred, in milliseconds.
383    /// Returns 0 if never synced.
384    ///
385    /// # Example
386    ///
387    /// ```rust
388    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
389    ///
390    /// let manager = TimeSyncManager::new();
391    /// assert_eq!(manager.last_sync_time(), 0);
392    ///
393    /// manager.update_offset(1704110400000);
394    /// assert!(manager.last_sync_time() > 0);
395    /// ```
396    #[inline]
397    pub fn last_sync_time(&self) -> i64 {
398        self.last_sync_time.load(Ordering::Acquire)
399    }
400
401    /// Returns a reference to the sync configuration.
402    ///
403    /// # Returns
404    ///
405    /// Reference to the `TimeSyncConfig`.
406    ///
407    /// # Example
408    ///
409    /// ```rust
410    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
411    /// use std::time::Duration;
412    ///
413    /// let manager = TimeSyncManager::new();
414    /// assert_eq!(manager.config().sync_interval, Duration::from_secs(30));
415    /// ```
416    #[inline]
417    pub fn config(&self) -> &TimeSyncConfig {
418        &self.config
419    }
420
421    /// Resets the manager to uninitialized state.
422    ///
423    /// This clears the cached offset and marks the manager as needing resync.
424    /// Useful for testing or when a forced resync is needed.
425    ///
426    /// # Example
427    ///
428    /// ```rust
429    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
430    ///
431    /// let manager = TimeSyncManager::new();
432    /// manager.update_offset(1704110400000);
433    /// assert!(manager.is_initialized());
434    ///
435    /// manager.reset();
436    /// assert!(!manager.is_initialized());
437    /// ```
438    pub fn reset(&self) {
439        self.time_offset.store(0, Ordering::Release);
440        self.last_sync_time.store(0, Ordering::Release);
441        self.initialized.store(false, Ordering::Release);
442    }
443}
444
445impl Default for TimeSyncManager {
446    fn default() -> Self {
447        Self::new()
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    #![allow(clippy::disallowed_methods)]
454    use super::*;
455    use std::thread;
456
457    #[test]
458    fn test_time_sync_config_default() {
459        let config = TimeSyncConfig::default();
460        assert_eq!(config.sync_interval, Duration::from_secs(30));
461        assert!(config.auto_sync);
462        assert_eq!(config.max_offset_drift, 5000);
463    }
464
465    #[test]
466    fn test_time_sync_config_with_interval() {
467        let config = TimeSyncConfig::with_interval(Duration::from_secs(60));
468        assert_eq!(config.sync_interval, Duration::from_secs(60));
469        assert!(config.auto_sync);
470    }
471
472    #[test]
473    fn test_time_sync_config_manual_sync_only() {
474        let config = TimeSyncConfig::manual_sync_only();
475        assert!(!config.auto_sync);
476    }
477
478    #[test]
479    fn test_time_sync_manager_new() {
480        let manager = TimeSyncManager::new();
481        assert!(!manager.is_initialized());
482        assert_eq!(manager.get_offset(), 0);
483        assert_eq!(manager.last_sync_time(), 0);
484    }
485
486    #[test]
487    fn test_time_sync_manager_with_config() {
488        let config = TimeSyncConfig {
489            sync_interval: Duration::from_secs(60),
490            auto_sync: false,
491            max_offset_drift: 3000,
492        };
493        let manager = TimeSyncManager::with_config(config);
494        assert_eq!(manager.config().sync_interval, Duration::from_secs(60));
495        assert!(!manager.config().auto_sync);
496        assert_eq!(manager.config().max_offset_drift, 3000);
497    }
498
499    #[test]
500    fn test_needs_resync_when_not_initialized() {
501        let manager = TimeSyncManager::new();
502        assert!(manager.needs_resync());
503    }
504
505    #[test]
506    fn test_needs_resync_after_initialization() {
507        let manager = TimeSyncManager::new();
508        let server_time = TimestampUtils::now_ms();
509        manager.update_offset(server_time);
510
511        // Should not need resync immediately after sync
512        assert!(!manager.needs_resync());
513    }
514
515    #[test]
516    fn test_needs_resync_with_auto_sync_disabled() {
517        let config = TimeSyncConfig::manual_sync_only();
518        let manager = TimeSyncManager::with_config(config);
519
520        let server_time = TimestampUtils::now_ms();
521        manager.update_offset(server_time);
522
523        // Should never need automatic resync when auto_sync is disabled
524        assert!(!manager.needs_resync());
525    }
526
527    #[test]
528    fn test_update_offset() {
529        let manager = TimeSyncManager::new();
530        let local_time = TimestampUtils::now_ms();
531
532        // Simulate server being 100ms ahead
533        let server_time = local_time + 100;
534        manager.update_offset(server_time);
535
536        assert!(manager.is_initialized());
537        // Offset should be approximately 100 (may vary slightly due to timing)
538        let offset = manager.get_offset();
539        assert!((90..=110).contains(&offset), "Offset was: {}", offset);
540    }
541
542    #[test]
543    fn test_get_server_timestamp() {
544        let manager = TimeSyncManager::new();
545        let local_time = TimestampUtils::now_ms();
546
547        // Simulate server being 1000ms ahead
548        let server_time = local_time + 1000;
549        manager.update_offset(server_time);
550
551        let estimated = manager.get_server_timestamp();
552        // Estimated server time should be close to actual server time
553        let diff = (estimated - server_time).abs();
554        assert!(diff < 100, "Difference was: {}", diff);
555    }
556
557    #[test]
558    fn test_reset() {
559        let manager = TimeSyncManager::new();
560        manager.update_offset(TimestampUtils::now_ms());
561        assert!(manager.is_initialized());
562
563        manager.reset();
564        assert!(!manager.is_initialized());
565        assert_eq!(manager.get_offset(), 0);
566        assert_eq!(manager.last_sync_time(), 0);
567    }
568
569    #[test]
570    fn test_thread_safety_concurrent_reads() {
571        use std::sync::Arc;
572
573        let manager = Arc::new(TimeSyncManager::new());
574        manager.update_offset(TimestampUtils::now_ms() + 500);
575
576        let mut handles = vec![];
577
578        // Spawn multiple reader threads
579        for _ in 0..10 {
580            let manager_clone = Arc::clone(&manager);
581            let handle = thread::spawn(move || {
582                for _ in 0..100 {
583                    let _ = manager_clone.get_server_timestamp();
584                    let _ = manager_clone.get_offset();
585                    let _ = manager_clone.is_initialized();
586                }
587            });
588            handles.push(handle);
589        }
590
591        for handle in handles {
592            handle.join().unwrap();
593        }
594    }
595
596    #[test]
597    fn test_thread_safety_concurrent_writes() {
598        use std::sync::Arc;
599
600        let manager = Arc::new(TimeSyncManager::new());
601
602        let mut handles = vec![];
603
604        // Spawn multiple writer threads
605        for i in 0..5 {
606            let manager_clone = Arc::clone(&manager);
607            let handle = thread::spawn(move || {
608                for j in 0..20 {
609                    let server_time = TimestampUtils::now_ms() + (i * 100 + j) as i64;
610                    manager_clone.update_offset(server_time);
611                }
612            });
613            handles.push(handle);
614        }
615
616        for handle in handles {
617            handle.join().unwrap();
618        }
619
620        // Manager should be initialized after all writes
621        assert!(manager.is_initialized());
622    }
623
624    #[test]
625    fn test_thread_safety_concurrent_read_write() {
626        use std::sync::Arc;
627
628        let manager = Arc::new(TimeSyncManager::new());
629        manager.update_offset(TimestampUtils::now_ms());
630
631        let mut handles = vec![];
632
633        // Spawn reader threads
634        for _ in 0..5 {
635            let manager_clone = Arc::clone(&manager);
636            let handle = thread::spawn(move || {
637                for _ in 0..100 {
638                    let _ = manager_clone.get_server_timestamp();
639                    let _ = manager_clone.needs_resync();
640                }
641            });
642            handles.push(handle);
643        }
644
645        // Spawn writer threads
646        for i in 0..3 {
647            let manager_clone = Arc::clone(&manager);
648            let handle = thread::spawn(move || {
649                for j in 0..50 {
650                    let server_time = TimestampUtils::now_ms() + (i * 10 + j) as i64;
651                    manager_clone.update_offset(server_time);
652                }
653            });
654            handles.push(handle);
655        }
656
657        for handle in handles {
658            handle.join().unwrap();
659        }
660
661        // Verify manager is in a consistent state
662        assert!(manager.is_initialized());
663        assert!(manager.last_sync_time() > 0);
664    }
665
666    #[test]
667    fn test_offset_calculation_with_negative_offset() {
668        let manager = TimeSyncManager::new();
669        let local_time = TimestampUtils::now_ms();
670
671        // Simulate server being 500ms behind
672        let server_time = local_time - 500;
673        manager.update_offset(server_time);
674
675        let offset = manager.get_offset();
676        // Offset should be approximately -500
677        assert!((-600..=-400).contains(&offset), "Offset was: {}", offset);
678    }
679
680    #[test]
681    fn test_saturating_arithmetic() {
682        let manager = TimeSyncManager::new();
683
684        // Test with extreme offset values
685        manager.time_offset.store(i64::MAX, Ordering::Release);
686        manager.initialized.store(true, Ordering::Release);
687
688        // Should not panic due to overflow
689        let timestamp = manager.get_server_timestamp();
690        assert!(timestamp > 0);
691    }
692}