Skip to main content

futu_gateway_core/bridge/
clock.rs

1// v1.4.110 P0-5 prep (T12 P1): server-clock abstraction 从
2// `handlers/mod.rs` 移到 `bridge::clock`. 移因 bridge / handlers 拆 3 crate 后
3// `ServerClock` 是 cross-domain abstraction (trd + qot 都需要 svr-time), 应
4// 在 gateway-core 同时被 trd/qot 子 crate cross-import. 原 `handlers/mod.rs`
5// 保留 `pub use` re-export 不破 sub-module use site (P3 move 后 T15 wire 一次性
6// sed).
7//
8// codex 21st-round Finding 2 (P2, v1.4.102, 2026-04-28 11:57): server-time
9// provider trait — handlers 用 server-now 替代 `Utc::now()` 本机时钟, 跟 C++
10// `INNBiz_SvrTime::GetSvrTimeStamp()` 对齐. tests 用 FixedServerClock 注入
11// 固定时间避免 wall-clock dependent (codex F3 配套).
12//
13// **Use case**: option-chain default begin/end + time-only `HH:MM:SS` 解析 +
14// 30d+1h check 都需要 "服务端今天" 而非 "本机今天" — 跨日窗口 / 时钟漂移
15// 场景能差 1 天.
16
17use std::sync::Arc;
18use std::sync::atomic::{AtomicI64, Ordering};
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21use parking_lot::RwLock;
22
23/// codex 21st F2/F3: server clock abstraction. production 用 `OffsetServerClock`
24/// 包 svr_time_offset_secs (从 AuthResult 派生), 测试用 `FixedServerClock`.
25pub trait ServerClock: Send + Sync + std::fmt::Debug {
26    /// 返当前 "服务端 unix timestamp (seconds)".
27    fn now_secs(&self) -> i64;
28}
29
30/// Convert a server unix timestamp into UTC DateTime.
31///
32/// `DateTime::from_timestamp` is fallible only for values outside chrono's
33/// representable range. Keep the existing epoch fallback centralized so
34/// handlers do not duplicate an infallible-looking `expect`.
35pub fn utc_datetime_from_unix_secs_or_epoch(secs: i64) -> chrono::DateTime<chrono::Utc> {
36    chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0).unwrap_or(chrono::DateTime::UNIX_EPOCH)
37}
38
39/// Return daemon wall-clock elapsed time since Unix epoch.
40///
41/// These timestamps are used for local ops/metrics bookkeeping, not
42/// backend-time-aligned business semantics. If the host clock is anomalously
43/// before Unix epoch, preserve the historical zero fallback but make it visible
44/// in logs.
45pub fn wall_clock_since_unix_epoch_or_zero(context: &'static str) -> Duration {
46    match SystemTime::now().duration_since(UNIX_EPOCH) {
47        Ok(elapsed) => elapsed,
48        Err(err) => {
49            tracing::warn!(
50                context,
51                error = %err,
52                "system wall clock is before UNIX_EPOCH; using zero duration fallback"
53            );
54            Duration::ZERO
55        }
56    }
57}
58
59/// Gateway-level server-clock runtime boundary.
60///
61/// This owns both the login-derived offset and the injectable clock provider.
62/// Startup and handler registration paths should use this bundle instead of
63/// reaching into `GatewayBridge` for raw offset/clock fields.
64#[derive(Debug)]
65pub struct ServerClockRuntime {
66    offset_secs: Arc<AtomicI64>,
67    clock: RwLock<Arc<dyn ServerClock>>,
68}
69
70impl ServerClockRuntime {
71    pub fn new() -> Self {
72        let offset_secs = Arc::new(AtomicI64::new(0));
73        let clock: Arc<dyn ServerClock> =
74            Arc::new(OffsetServerClock::new(Arc::clone(&offset_secs)));
75        Self {
76            offset_secs,
77            clock: RwLock::new(clock),
78        }
79    }
80
81    pub fn offset_secs(&self) -> Arc<AtomicI64> {
82        Arc::clone(&self.offset_secs)
83    }
84
85    pub fn store_offset_secs(&self, offset_secs: i64) {
86        self.offset_secs.store(offset_secs, Ordering::Relaxed);
87    }
88
89    pub fn clock(&self) -> Arc<dyn ServerClock> {
90        self.clock.read().clone()
91    }
92
93    pub fn set_clock_for_tests(&self, clock: Arc<dyn ServerClock>) {
94        *self.clock.write() = clock;
95    }
96}
97
98impl Default for ServerClockRuntime {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// Production server clock: 本机 `Utc::now()` + `svr_time_offset_secs` (从
105/// `AuthResult.svr_time_offset` 派生, AtomicI64 lock-free 跨线程读).
106#[derive(Debug, Clone)]
107pub struct OffsetServerClock {
108    offset: Arc<AtomicI64>,
109}
110
111impl OffsetServerClock {
112    pub fn new(offset: Arc<AtomicI64>) -> Self {
113        Self { offset }
114    }
115}
116
117impl ServerClock for OffsetServerClock {
118    fn now_secs(&self) -> i64 {
119        chrono::Utc::now().timestamp() + self.offset.load(Ordering::Relaxed)
120    }
121}
122
123/// Test server clock: 固定 unix ts. 用于 deterministic test assertions
124/// (e.g. "HKT 与 NY 跨日时 default 是 NY 日历日, 不是 HKT").
125#[derive(Debug, Clone)]
126pub struct FixedServerClock(pub i64);
127
128impl ServerClock for FixedServerClock {
129    fn now_secs(&self) -> i64 {
130        self.0
131    }
132}
133
134#[cfg(test)]
135mod tests;