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        if elapsed >= self.config.sync_interval.as_millis() as i64 {
292            return true;
293        }
294
295        // Check if estimated drift exceeds the configured maximum
296        // The longer since last sync, the more the offset may have drifted
297        if elapsed >= self.config.max_offset_drift {
298            return true;
299        }
300
301        false
302    }
303
304    /// Gets the current cached time offset.
305    ///
306    /// The offset represents: `server_time - local_time` in milliseconds.
307    ///
308    /// # Returns
309    ///
310    /// The cached time offset in milliseconds.
311    ///
312    /// # Example
313    ///
314    /// ```rust
315    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
316    ///
317    /// let manager = TimeSyncManager::new();
318    /// assert_eq!(manager.get_offset(), 0); // Default offset
319    ///
320    /// // After sync, offset reflects the difference
321    /// // (actual value depends on local vs server time)
322    /// ```
323    #[inline]
324    pub fn get_offset(&self) -> i64 {
325        self.time_offset.load(Ordering::Acquire)
326    }
327
328    /// Calculates the estimated server timestamp using cached offset.
329    ///
330    /// Formula: `server_timestamp = local_time + offset`
331    ///
332    /// Uses saturating arithmetic to prevent overflow.
333    ///
334    /// # Returns
335    ///
336    /// Estimated server timestamp in milliseconds.
337    ///
338    /// # Example
339    ///
340    /// ```rust
341    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
342    ///
343    /// let manager = TimeSyncManager::new();
344    /// let timestamp = manager.get_server_timestamp();
345    /// assert!(timestamp > 0);
346    /// ```
347    #[inline]
348    pub fn get_server_timestamp(&self) -> i64 {
349        let local_time = TimestampUtils::now_ms();
350        let offset = self.get_offset();
351        local_time.saturating_add(offset)
352    }
353
354    /// Updates the time offset based on server time.
355    ///
356    /// This method should be called after fetching server time from the API.
357    /// It calculates the offset as: `offset = server_time - local_time`
358    ///
359    /// # Arguments
360    ///
361    /// * `server_time` - The server timestamp in milliseconds
362    ///
363    /// # Example
364    ///
365    /// ```rust
366    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
367    ///
368    /// let manager = TimeSyncManager::new();
369    ///
370    /// // Simulate receiving server time
371    /// let server_time = 1704110400000i64;
372    /// manager.update_offset(server_time);
373    ///
374    /// assert!(manager.is_initialized());
375    /// ```
376    pub fn update_offset(&self, server_time: i64) {
377        let local_time = TimestampUtils::now_ms();
378        let offset = server_time.saturating_sub(local_time);
379
380        // Update all fields atomically (in terms of visibility)
381        // Using Release ordering ensures these writes are visible to
382        // subsequent Acquire reads
383        self.time_offset.store(offset, Ordering::Release);
384        self.last_sync_time.store(local_time, Ordering::Release);
385        self.initialized.store(true, Ordering::Release);
386    }
387
388    /// Returns the last sync timestamp (local time).
389    ///
390    /// # Returns
391    ///
392    /// The local timestamp when the last sync occurred, in milliseconds.
393    /// Returns 0 if never synced.
394    ///
395    /// # Example
396    ///
397    /// ```rust
398    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
399    ///
400    /// let manager = TimeSyncManager::new();
401    /// assert_eq!(manager.last_sync_time(), 0);
402    ///
403    /// manager.update_offset(1704110400000);
404    /// assert!(manager.last_sync_time() > 0);
405    /// ```
406    #[inline]
407    pub fn last_sync_time(&self) -> i64 {
408        self.last_sync_time.load(Ordering::Acquire)
409    }
410
411    /// Returns a reference to the sync configuration.
412    ///
413    /// # Returns
414    ///
415    /// Reference to the `TimeSyncConfig`.
416    ///
417    /// # Example
418    ///
419    /// ```rust
420    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
421    /// use std::time::Duration;
422    ///
423    /// let manager = TimeSyncManager::new();
424    /// assert_eq!(manager.config().sync_interval, Duration::from_secs(30));
425    /// ```
426    #[inline]
427    pub fn config(&self) -> &TimeSyncConfig {
428        &self.config
429    }
430
431    /// Resets the manager to uninitialized state.
432    ///
433    /// This clears the cached offset and marks the manager as needing resync.
434    /// Useful for testing or when a forced resync is needed.
435    ///
436    /// # Example
437    ///
438    /// ```rust
439    /// use ccxt_exchanges::binance::time_sync::TimeSyncManager;
440    ///
441    /// let manager = TimeSyncManager::new();
442    /// manager.update_offset(1704110400000);
443    /// assert!(manager.is_initialized());
444    ///
445    /// manager.reset();
446    /// assert!(!manager.is_initialized());
447    /// ```
448    pub fn reset(&self) {
449        self.time_offset.store(0, Ordering::Release);
450        self.last_sync_time.store(0, Ordering::Release);
451        self.initialized.store(false, Ordering::Release);
452    }
453}
454
455impl Default for TimeSyncManager {
456    fn default() -> Self {
457        Self::new()
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    #![allow(clippy::disallowed_methods)]
464    use super::*;
465    use std::thread;
466
467    #[test]
468    fn test_time_sync_config_default() {
469        let config = TimeSyncConfig::default();
470        assert_eq!(config.sync_interval, Duration::from_secs(30));
471        assert!(config.auto_sync);
472        assert_eq!(config.max_offset_drift, 5000);
473    }
474
475    #[test]
476    fn test_time_sync_config_with_interval() {
477        let config = TimeSyncConfig::with_interval(Duration::from_secs(60));
478        assert_eq!(config.sync_interval, Duration::from_secs(60));
479        assert!(config.auto_sync);
480    }
481
482    #[test]
483    fn test_time_sync_config_manual_sync_only() {
484        let config = TimeSyncConfig::manual_sync_only();
485        assert!(!config.auto_sync);
486    }
487
488    #[test]
489    fn test_time_sync_manager_new() {
490        let manager = TimeSyncManager::new();
491        assert!(!manager.is_initialized());
492        assert_eq!(manager.get_offset(), 0);
493        assert_eq!(manager.last_sync_time(), 0);
494    }
495
496    #[test]
497    fn test_time_sync_manager_with_config() {
498        let config = TimeSyncConfig {
499            sync_interval: Duration::from_secs(60),
500            auto_sync: false,
501            max_offset_drift: 3000,
502        };
503        let manager = TimeSyncManager::with_config(config);
504        assert_eq!(manager.config().sync_interval, Duration::from_secs(60));
505        assert!(!manager.config().auto_sync);
506        assert_eq!(manager.config().max_offset_drift, 3000);
507    }
508
509    #[test]
510    fn test_needs_resync_when_not_initialized() {
511        let manager = TimeSyncManager::new();
512        assert!(manager.needs_resync());
513    }
514
515    #[test]
516    fn test_needs_resync_after_initialization() {
517        let manager = TimeSyncManager::new();
518        let server_time = TimestampUtils::now_ms();
519        manager.update_offset(server_time);
520
521        // Should not need resync immediately after sync
522        assert!(!manager.needs_resync());
523    }
524
525    #[test]
526    fn test_needs_resync_with_auto_sync_disabled() {
527        let config = TimeSyncConfig::manual_sync_only();
528        let manager = TimeSyncManager::with_config(config);
529
530        let server_time = TimestampUtils::now_ms();
531        manager.update_offset(server_time);
532
533        // Should never need automatic resync when auto_sync is disabled
534        assert!(!manager.needs_resync());
535    }
536
537    #[test]
538    fn test_update_offset() {
539        let manager = TimeSyncManager::new();
540        let local_time = TimestampUtils::now_ms();
541
542        // Simulate server being 100ms ahead
543        let server_time = local_time + 100;
544        manager.update_offset(server_time);
545
546        assert!(manager.is_initialized());
547        // Offset should be approximately 100 (may vary slightly due to timing)
548        let offset = manager.get_offset();
549        assert!((90..=110).contains(&offset), "Offset was: {}", offset);
550    }
551
552    #[test]
553    fn test_get_server_timestamp() {
554        let manager = TimeSyncManager::new();
555        let local_time = TimestampUtils::now_ms();
556
557        // Simulate server being 1000ms ahead
558        let server_time = local_time + 1000;
559        manager.update_offset(server_time);
560
561        let estimated = manager.get_server_timestamp();
562        // Estimated server time should be close to actual server time
563        let diff = (estimated - server_time).abs();
564        assert!(diff < 100, "Difference was: {}", diff);
565    }
566
567    #[test]
568    fn test_reset() {
569        let manager = TimeSyncManager::new();
570        manager.update_offset(TimestampUtils::now_ms());
571        assert!(manager.is_initialized());
572
573        manager.reset();
574        assert!(!manager.is_initialized());
575        assert_eq!(manager.get_offset(), 0);
576        assert_eq!(manager.last_sync_time(), 0);
577    }
578
579    #[test]
580    fn test_thread_safety_concurrent_reads() {
581        use std::sync::Arc;
582
583        let manager = Arc::new(TimeSyncManager::new());
584        manager.update_offset(TimestampUtils::now_ms() + 500);
585
586        let mut handles = vec![];
587
588        // Spawn multiple reader threads
589        for _ in 0..10 {
590            let manager_clone = Arc::clone(&manager);
591            let handle = thread::spawn(move || {
592                for _ in 0..100 {
593                    let _ = manager_clone.get_server_timestamp();
594                    let _ = manager_clone.get_offset();
595                    let _ = manager_clone.is_initialized();
596                }
597            });
598            handles.push(handle);
599        }
600
601        for handle in handles {
602            handle.join().unwrap();
603        }
604    }
605
606    #[test]
607    fn test_thread_safety_concurrent_writes() {
608        use std::sync::Arc;
609
610        let manager = Arc::new(TimeSyncManager::new());
611
612        let mut handles = vec![];
613
614        // Spawn multiple writer threads
615        for i in 0..5 {
616            let manager_clone = Arc::clone(&manager);
617            let handle = thread::spawn(move || {
618                for j in 0..20 {
619                    let server_time = TimestampUtils::now_ms() + (i * 100 + j) as i64;
620                    manager_clone.update_offset(server_time);
621                }
622            });
623            handles.push(handle);
624        }
625
626        for handle in handles {
627            handle.join().unwrap();
628        }
629
630        // Manager should be initialized after all writes
631        assert!(manager.is_initialized());
632    }
633
634    #[test]
635    fn test_thread_safety_concurrent_read_write() {
636        use std::sync::Arc;
637
638        let manager = Arc::new(TimeSyncManager::new());
639        manager.update_offset(TimestampUtils::now_ms());
640
641        let mut handles = vec![];
642
643        // Spawn reader threads
644        for _ in 0..5 {
645            let manager_clone = Arc::clone(&manager);
646            let handle = thread::spawn(move || {
647                for _ in 0..100 {
648                    let _ = manager_clone.get_server_timestamp();
649                    let _ = manager_clone.needs_resync();
650                }
651            });
652            handles.push(handle);
653        }
654
655        // Spawn writer threads
656        for i in 0..3 {
657            let manager_clone = Arc::clone(&manager);
658            let handle = thread::spawn(move || {
659                for j in 0..50 {
660                    let server_time = TimestampUtils::now_ms() + (i * 10 + j) as i64;
661                    manager_clone.update_offset(server_time);
662                }
663            });
664            handles.push(handle);
665        }
666
667        for handle in handles {
668            handle.join().unwrap();
669        }
670
671        // Verify manager is in a consistent state
672        assert!(manager.is_initialized());
673        assert!(manager.last_sync_time() > 0);
674    }
675
676    #[test]
677    fn test_offset_calculation_with_negative_offset() {
678        let manager = TimeSyncManager::new();
679        let local_time = TimestampUtils::now_ms();
680
681        // Simulate server being 500ms behind
682        let server_time = local_time - 500;
683        manager.update_offset(server_time);
684
685        let offset = manager.get_offset();
686        // Offset should be approximately -500
687        assert!((-600..=-400).contains(&offset), "Offset was: {}", offset);
688    }
689
690    #[test]
691    fn test_saturating_arithmetic() {
692        let manager = TimeSyncManager::new();
693
694        // Test with extreme offset values
695        manager.time_offset.store(i64::MAX, Ordering::Release);
696        manager.initialized.store(true, Ordering::Release);
697
698        // Should not panic due to overflow
699        let timestamp = manager.get_server_timestamp();
700        assert!(timestamp > 0);
701    }
702}