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}