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}