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    use super::*;
454    use std::thread;
455
456    #[test]
457    fn test_time_sync_config_default() {
458        let config = TimeSyncConfig::default();
459        assert_eq!(config.sync_interval, Duration::from_secs(30));
460        assert!(config.auto_sync);
461        assert_eq!(config.max_offset_drift, 5000);
462    }
463
464    #[test]
465    fn test_time_sync_config_with_interval() {
466        let config = TimeSyncConfig::with_interval(Duration::from_secs(60));
467        assert_eq!(config.sync_interval, Duration::from_secs(60));
468        assert!(config.auto_sync);
469    }
470
471    #[test]
472    fn test_time_sync_config_manual_sync_only() {
473        let config = TimeSyncConfig::manual_sync_only();
474        assert!(!config.auto_sync);
475    }
476
477    #[test]
478    fn test_time_sync_manager_new() {
479        let manager = TimeSyncManager::new();
480        assert!(!manager.is_initialized());
481        assert_eq!(manager.get_offset(), 0);
482        assert_eq!(manager.last_sync_time(), 0);
483    }
484
485    #[test]
486    fn test_time_sync_manager_with_config() {
487        let config = TimeSyncConfig {
488            sync_interval: Duration::from_secs(60),
489            auto_sync: false,
490            max_offset_drift: 3000,
491        };
492        let manager = TimeSyncManager::with_config(config);
493        assert_eq!(manager.config().sync_interval, Duration::from_secs(60));
494        assert!(!manager.config().auto_sync);
495        assert_eq!(manager.config().max_offset_drift, 3000);
496    }
497
498    #[test]
499    fn test_needs_resync_when_not_initialized() {
500        let manager = TimeSyncManager::new();
501        assert!(manager.needs_resync());
502    }
503
504    #[test]
505    fn test_needs_resync_after_initialization() {
506        let manager = TimeSyncManager::new();
507        let server_time = TimestampUtils::now_ms();
508        manager.update_offset(server_time);
509
510        // Should not need resync immediately after sync
511        assert!(!manager.needs_resync());
512    }
513
514    #[test]
515    fn test_needs_resync_with_auto_sync_disabled() {
516        let config = TimeSyncConfig::manual_sync_only();
517        let manager = TimeSyncManager::with_config(config);
518
519        let server_time = TimestampUtils::now_ms();
520        manager.update_offset(server_time);
521
522        // Should never need automatic resync when auto_sync is disabled
523        assert!(!manager.needs_resync());
524    }
525
526    #[test]
527    fn test_update_offset() {
528        let manager = TimeSyncManager::new();
529        let local_time = TimestampUtils::now_ms();
530
531        // Simulate server being 100ms ahead
532        let server_time = local_time + 100;
533        manager.update_offset(server_time);
534
535        assert!(manager.is_initialized());
536        // Offset should be approximately 100 (may vary slightly due to timing)
537        let offset = manager.get_offset();
538        assert!(offset >= 90 && offset <= 110, "Offset was: {}", offset);
539    }
540
541    #[test]
542    fn test_get_server_timestamp() {
543        let manager = TimeSyncManager::new();
544        let local_time = TimestampUtils::now_ms();
545
546        // Simulate server being 1000ms ahead
547        let server_time = local_time + 1000;
548        manager.update_offset(server_time);
549
550        let estimated = manager.get_server_timestamp();
551        // Estimated server time should be close to actual server time
552        let diff = (estimated - server_time).abs();
553        assert!(diff < 100, "Difference was: {}", diff);
554    }
555
556    #[test]
557    fn test_reset() {
558        let manager = TimeSyncManager::new();
559        manager.update_offset(TimestampUtils::now_ms());
560        assert!(manager.is_initialized());
561
562        manager.reset();
563        assert!(!manager.is_initialized());
564        assert_eq!(manager.get_offset(), 0);
565        assert_eq!(manager.last_sync_time(), 0);
566    }
567
568    #[test]
569    fn test_thread_safety_concurrent_reads() {
570        use std::sync::Arc;
571
572        let manager = Arc::new(TimeSyncManager::new());
573        manager.update_offset(TimestampUtils::now_ms() + 500);
574
575        let mut handles = vec![];
576
577        // Spawn multiple reader threads
578        for _ in 0..10 {
579            let manager_clone = Arc::clone(&manager);
580            let handle = thread::spawn(move || {
581                for _ in 0..100 {
582                    let _ = manager_clone.get_server_timestamp();
583                    let _ = manager_clone.get_offset();
584                    let _ = manager_clone.is_initialized();
585                }
586            });
587            handles.push(handle);
588        }
589
590        for handle in handles {
591            handle.join().unwrap();
592        }
593    }
594
595    #[test]
596    fn test_thread_safety_concurrent_writes() {
597        use std::sync::Arc;
598
599        let manager = Arc::new(TimeSyncManager::new());
600
601        let mut handles = vec![];
602
603        // Spawn multiple writer threads
604        for i in 0..5 {
605            let manager_clone = Arc::clone(&manager);
606            let handle = thread::spawn(move || {
607                for j in 0..20 {
608                    let server_time = TimestampUtils::now_ms() + (i * 100 + j) as i64;
609                    manager_clone.update_offset(server_time);
610                }
611            });
612            handles.push(handle);
613        }
614
615        for handle in handles {
616            handle.join().unwrap();
617        }
618
619        // Manager should be initialized after all writes
620        assert!(manager.is_initialized());
621    }
622
623    #[test]
624    fn test_thread_safety_concurrent_read_write() {
625        use std::sync::Arc;
626
627        let manager = Arc::new(TimeSyncManager::new());
628        manager.update_offset(TimestampUtils::now_ms());
629
630        let mut handles = vec![];
631
632        // Spawn reader threads
633        for _ in 0..5 {
634            let manager_clone = Arc::clone(&manager);
635            let handle = thread::spawn(move || {
636                for _ in 0..100 {
637                    let _ = manager_clone.get_server_timestamp();
638                    let _ = manager_clone.needs_resync();
639                }
640            });
641            handles.push(handle);
642        }
643
644        // Spawn writer threads
645        for i in 0..3 {
646            let manager_clone = Arc::clone(&manager);
647            let handle = thread::spawn(move || {
648                for j in 0..50 {
649                    let server_time = TimestampUtils::now_ms() + (i * 10 + j) as i64;
650                    manager_clone.update_offset(server_time);
651                }
652            });
653            handles.push(handle);
654        }
655
656        for handle in handles {
657            handle.join().unwrap();
658        }
659
660        // Verify manager is in a consistent state
661        assert!(manager.is_initialized());
662        assert!(manager.last_sync_time() > 0);
663    }
664
665    #[test]
666    fn test_offset_calculation_with_negative_offset() {
667        let manager = TimeSyncManager::new();
668        let local_time = TimestampUtils::now_ms();
669
670        // Simulate server being 500ms behind
671        let server_time = local_time - 500;
672        manager.update_offset(server_time);
673
674        let offset = manager.get_offset();
675        // Offset should be approximately -500
676        assert!(offset >= -600 && offset <= -400, "Offset was: {}", offset);
677    }
678
679    #[test]
680    fn test_saturating_arithmetic() {
681        let manager = TimeSyncManager::new();
682
683        // Test with extreme offset values
684        manager.time_offset.store(i64::MAX, Ordering::Release);
685        manager.initialized.store(true, Ordering::Release);
686
687        // Should not panic due to overflow
688        let timestamp = manager.get_server_timestamp();
689        assert!(timestamp > 0);
690    }
691}