Skip to main content

futu_server/
metrics.rs

1// 网关运行时监控指标
2//
3// 使用原子计数器,零开销采集,无需外部依赖。
4// 通过 telnet `show_metrics` 命令查看。
5// v1.4.90 P1-B: 也通过 [`GatewayMetrics::render_prometheus`] 暴露到
6// `/metrics` HTTP 端点 (经 [`futu_auth::metrics::Registry`] extension renderer
7// 注册). 之前 v1.4.83/84 声称这些 counter 在 Prometheus 但仅在 telnet 输出.
8
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Instant;
11
12use chrono::{Timelike, Utc};
13use parking_lot::RwLock;
14
15mod prometheus;
16pub use prometheus::install_prometheus_extension;
17
18/// v1.4.84 §14: per-cmd_id per-UTC-hour breakdown for monitoring.
19///
20/// 让 tester CI 长窗口 job 能做时段异常检测 (CMD14716 UTC 15-18 window).
21/// 单独结构, 各 cmd counter 独立持有一组 24 bucket.
22#[derive(Debug)]
23pub struct HourBreakdown {
24    counters: [AtomicU64; 24],
25}
26
27impl HourBreakdown {
28    pub const fn new() -> Self {
29        Self {
30            counters: [const { AtomicU64::new(0) }; 24],
31        }
32    }
33
34    /// Bump the counter for current UTC hour (0..23).
35    pub fn bump_now(&self) {
36        let hour = Utc::now().hour() as usize;
37        if hour < 24 {
38            self.counters[hour].fetch_add(1, Ordering::Relaxed);
39        }
40    }
41
42    /// Read counter for specific hour (0..23). Out-of-range returns 0.
43    pub fn get(&self, hour: usize) -> u64 {
44        self.counters
45            .get(hour)
46            .map(|a| a.load(Ordering::Relaxed))
47            .unwrap_or(0)
48    }
49
50    /// Return snapshot of all 24 hours as array.
51    pub fn snapshot(&self) -> [u64; 24] {
52        let mut out = [0u64; 24];
53        for (i, c) in self.counters.iter().enumerate() {
54            out[i] = c.load(Ordering::Relaxed);
55        }
56        out
57    }
58}
59
60impl Default for HourBreakdown {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66/// 网关运行时监控指标
67pub struct GatewayMetrics {
68    /// 网关启动时间
69    pub start_time: Instant,
70
71    // ===== 连接指标 =====
72    /// 累计接受的客户端连接数
73    pub total_connections: AtomicU64,
74    /// 累计客户端断开数
75    pub total_disconnections: AtomicU64,
76    /// 被拒绝的连接数(超过上限)
77    pub rejected_connections: AtomicU64,
78
79    // ===== 请求指标 =====
80    /// 累计处理的请求数
81    pub total_requests: AtomicU64,
82    /// 累计请求错误数(handler 返回 None 或解密失败)
83    pub total_request_errors: AtomicU64,
84    /// 累计响应字节数
85    pub total_response_bytes: AtomicU64,
86
87    // ===== 后端指标 =====
88    /// 后端重连次数
89    pub backend_reconnects: AtomicU64,
90    /// 后端重连失败次数
91    pub backend_reconnect_failures: AtomicU64,
92    /// 最近一次重连时间 (Unix 毫秒, 0=未重连过)
93    pub last_reconnect_ms: AtomicU64,
94    /// 后端是否在线 (1=online, 0=offline)
95    pub backend_online: AtomicU64,
96
97    // ===== 推送指标 =====
98    /// 后端收到的推送数 (CMD 6212 / 4716 / 5300 等)
99    pub backend_pushes_received: AtomicU64,
100    /// 向客户端发送的推送数
101    pub client_pushes_sent: AtomicU64,
102    /// 向客户端推送时 client channel 已关闭导致发送失败的次数
103    pub client_push_send_failures: AtomicU64,
104    /// 行情推送 fanout 时 client channel 已满、仅丢该慢客户端本帧的次数。
105    ///
106    /// 注意: `qot_push_dropped_total` 统计 backend → dispatcher 队列 drop;
107    /// 本 counter 统计 dispatcher → client channel backpressure,语义不同。
108    pub qot_client_push_backpressure_drops: AtomicU64,
109    /// 按 SubType 拆分的 dispatcher → client channel backpressure drop 计数。
110    /// 桶 0 = 未知 / 首推未携带 SubType;桶 1..=17 = 对应 Qot_Common.SubType。
111    pub qot_client_push_backpressure_drops_by_sub_type: [AtomicU64; 18],
112    // v1.4.83 §14 Phase 4: per-cmd_id push 细分计数 (monitoring)
113    /// CMD 6212 quote push 计数
114    pub backend_pushes_cmd_quote: AtomicU64,
115    /// CMD 4716 trade notify (legacy channel) 计数
116    pub backend_pushes_cmd_trade_legacy: AtomicU64,
117    /// CMD 14716 trade notify (v1.4.41 new channel) 计数 — tester §14 追踪
118    pub backend_pushes_cmd_trade_new: AtomicU64,
119    /// CMD 5300 msg-center push 计数
120    pub backend_pushes_cmd_msg_center: AtomicU64,
121    /// 其他未路由 CMD push 计数
122    pub backend_pushes_cmd_other: AtomicU64,
123
124    // v1.4.84 §14: per-cmd_id × UTC-hour 分桶 (监控深化 — tester CI 长窗口
125    // job 用于 "CMD14716 UTC 15-18 异常时段" 检测). cmd_other 不分时段.
126    /// CMD 6212 quote push per-UTC-hour 计数
127    pub backend_pushes_cmd_quote_by_hour: HourBreakdown,
128    /// CMD 4716 trade notify (legacy) per-UTC-hour 计数
129    pub backend_pushes_cmd_trade_legacy_by_hour: HourBreakdown,
130    /// CMD 14716 trade notify (new) per-UTC-hour 计数 — tester §14 主角
131    pub backend_pushes_cmd_trade_new_by_hour: HourBreakdown,
132    /// CMD 5300 msg-center push per-UTC-hour 计数
133    pub backend_pushes_cmd_msg_center_by_hour: HourBreakdown,
134
135    // ===== 订阅指标 =====
136    /// 行情订阅操作次数
137    pub qot_subscribe_ops: AtomicU64,
138    /// 行情退订操作次数
139    pub qot_unsubscribe_ops: AtomicU64,
140
141    // ===== v1.4.110 codex audit Round2 P3 #19: cold-cache wait 监控 =====
142    //
143    // GetBasicQot / GetOrderBook cache miss + 已订阅 → cold-cache wait 路径
144    // (Pull_SubData 主动拉 + 最多 3s 等 push). ops 用 hit/total 比看 backend
145    // push 延迟健康度, timeout/total 比看 cold-cache wait 是否常超时.
146    /// cold-cache wait 进入次数 (cache miss + IsSub, 触发 wait)
147    pub cold_cache_wait_total: AtomicU64,
148    /// cold-cache wait 命中次数 (3s 内 push 写 cache → re-read 拿到值)
149    pub cold_cache_wait_hit: AtomicU64,
150    /// cold-cache wait 超时次数 (3s timeout 仍 cache miss)
151    pub cold_cache_wait_timeout: AtomicU64,
152    /// 重连后重新订阅次数 (legacy, == applied_keys 累加; v1.4.106 codex 0631
153    /// F5 起仍 bump 向后兼容旧 dashboard, 等价新 `resubscribe_applied_keys`).
154    pub resubscribe_ops: AtomicU64,
155
156    // ===== v1.4.106 codex 0631 F5 [P3]: dual resubscribe counter =====
157    //
158    // 老 `resubscribe_ops_total` 把"触发数"和"真生效 keys 数"混一桶, 看不出
159    // partial / cache miss 的 silent loss. 拆 dual:
160    //   - resubscribe_attempts_total: 触发次数 (本 reconnect / staleness 触
161    //     发了 N 次 resubscribe, 不论结果).
162    //   - resubscribe_applied_keys_total: 真生效 keys 数 (cache resolve OK +
163    //     backend ack OK 的 (sec_key, sub_type) 对). 部分失败时 < attempts.
164    //
165    // ratio applied/attempts < 1.0 显著 → ghost subs / cache miss / backend
166    // partial reject 信号. legacy resubscribe_ops_total 仍 bump (向后兼容).
167    /// **v1.4.106 codex 0631 F5 [P3]**: resubscribe 触发次数 (每次 reconnect /
168    /// staleness loop 触发 += 1). 与 applied_keys 对比 ratio 看 partial 程度.
169    pub resubscribe_attempts: AtomicU64,
170    /// **v1.4.106 codex 0631 F5 [P3]**: resubscribe 真生效 keys 数 (累积).
171    /// applied < attempts × global_keys → 部分 partial.
172    pub resubscribe_applied_keys: AtomicU64,
173
174    // ===== v1.4.106 codex 1140 F8: 行情 push 投递失败计数 =====
175    //
176    // 之前 `bridge::push_parser` 用 `let _ = push_tx.try_send(event)` 静默吞错,
177    // cache 已更新但 subscriber 收不到 push (audit Finding 8). 加 metric +
178    // warn log 让 channel full / closed 立即可观测.
179    /// CMD 6212 行情 push (BasicQot/OrderBook/Ticker/RT/KL/Broker/...) 因
180    /// `push_tx` 队列满或关闭被 drop 的总次数 (累积).
181    pub qot_push_dropped_total: AtomicU64,
182    /// 按 SubType 拆分的 drop 计数 (proto Qot_Common.SubType: 0..=17, 共 18 桶).
183    /// 桶 0 = "未知 / 不属于任何已知 SubType" (兜底, e.g. 拼错的 sub_type).
184    /// 桶 1..=17 = 对应 SubType. v1.4.106 codex 1140 F8 加.
185    pub qot_push_dropped_by_sub_type: [AtomicU64; 18],
186
187    // ===== KeepAlive 指标 =====
188    /// KeepAlive 超时断开数
189    pub keepalive_timeouts: AtomicU64,
190
191    // ===== 延迟采样 =====
192    /// 最近 N 个请求延迟的环形缓冲 (纳秒)
193    latency_ring: RwLock<LatencyRing>,
194}
195
196/// 延迟环形缓冲 — 保留最近 1000 个采样
197struct LatencyRing {
198    buf: Vec<u64>,
199    pos: usize,
200    count: u64,
201    total_ns: u64,
202}
203
204const LATENCY_RING_SIZE: usize = 1000;
205
206impl LatencyRing {
207    fn new() -> Self {
208        Self {
209            buf: vec![0u64; LATENCY_RING_SIZE],
210            pos: 0,
211            count: 0,
212            total_ns: 0,
213        }
214    }
215
216    fn push(&mut self, ns: u64) {
217        // 减去被覆盖的旧值
218        if self.count >= LATENCY_RING_SIZE as u64 {
219            self.total_ns = self.total_ns.saturating_sub(self.buf[self.pos]);
220        }
221        self.buf[self.pos] = ns;
222        self.total_ns += ns;
223        self.pos = (self.pos + 1) % LATENCY_RING_SIZE;
224        self.count += 1;
225    }
226
227    fn stats(&self) -> LatencyStats {
228        let n = self.count.min(LATENCY_RING_SIZE as u64) as usize;
229        if n == 0 {
230            return LatencyStats::default();
231        }
232
233        let mut samples: Vec<u64> = if self.count >= LATENCY_RING_SIZE as u64 {
234            self.buf.clone()
235        } else {
236            self.buf[..n].to_vec()
237        };
238        samples.sort_unstable();
239
240        LatencyStats {
241            count: self.count,
242            avg_us: (self.total_ns / n as u64) / 1000,
243            p50_us: samples[n / 2] / 1000,
244            p95_us: samples[(n as f64 * 0.95) as usize] / 1000,
245            p99_us: samples[(n as f64 * 0.99).min((n - 1) as f64) as usize] / 1000,
246            max_us: samples[n - 1] / 1000,
247        }
248    }
249}
250
251/// 延迟统计摘要 (微秒)
252#[derive(Default)]
253pub struct LatencyStats {
254    /// 总采样数
255    pub count: u64,
256    /// 平均延迟 (微秒)
257    pub avg_us: u64,
258    /// P50 延迟
259    pub p50_us: u64,
260    /// P95 延迟
261    pub p95_us: u64,
262    /// P99 延迟
263    pub p99_us: u64,
264    /// 最大延迟
265    pub max_us: u64,
266}
267
268/// v1.4.84 §14: 把 24 小时 counters snapshot 格式化为空格分隔的单行.
269///
270/// 输出格式: `"h00=N h01=N ... h23=N"` — 便于人眼 scan 时段异常,
271/// 同时保持 parseable (awk / grep / Prometheus textfile).
272fn format_hour_row(hb: &HourBreakdown) -> String {
273    let snap = hb.snapshot();
274    let mut out = String::with_capacity(24 * 10);
275    for (i, v) in snap.iter().enumerate() {
276        if i > 0 {
277            out.push(' ');
278        }
279        out.push_str(&format!("h{:02}={}", i, v));
280    }
281    out
282}
283
284fn qot_sub_type_bucket(sub_type: i32) -> usize {
285    if (0..18).contains(&sub_type) {
286        sub_type as usize
287    } else {
288        0
289    }
290}
291
292impl GatewayMetrics {
293    pub fn new() -> Self {
294        Self {
295            start_time: Instant::now(),
296            total_connections: AtomicU64::new(0),
297            total_disconnections: AtomicU64::new(0),
298            rejected_connections: AtomicU64::new(0),
299            total_requests: AtomicU64::new(0),
300            total_request_errors: AtomicU64::new(0),
301            total_response_bytes: AtomicU64::new(0),
302            backend_reconnects: AtomicU64::new(0),
303            backend_reconnect_failures: AtomicU64::new(0),
304            last_reconnect_ms: AtomicU64::new(0),
305            backend_online: AtomicU64::new(1),
306            backend_pushes_received: AtomicU64::new(0),
307            client_pushes_sent: AtomicU64::new(0),
308            client_push_send_failures: AtomicU64::new(0),
309            qot_client_push_backpressure_drops: AtomicU64::new(0),
310            qot_client_push_backpressure_drops_by_sub_type: [const { AtomicU64::new(0) }; 18],
311            backend_pushes_cmd_quote: AtomicU64::new(0),
312            backend_pushes_cmd_trade_legacy: AtomicU64::new(0),
313            backend_pushes_cmd_trade_new: AtomicU64::new(0),
314            backend_pushes_cmd_msg_center: AtomicU64::new(0),
315            backend_pushes_cmd_other: AtomicU64::new(0),
316            backend_pushes_cmd_quote_by_hour: HourBreakdown::new(),
317            backend_pushes_cmd_trade_legacy_by_hour: HourBreakdown::new(),
318            backend_pushes_cmd_trade_new_by_hour: HourBreakdown::new(),
319            backend_pushes_cmd_msg_center_by_hour: HourBreakdown::new(),
320            qot_subscribe_ops: AtomicU64::new(0),
321            qot_unsubscribe_ops: AtomicU64::new(0),
322            cold_cache_wait_total: AtomicU64::new(0),
323            cold_cache_wait_hit: AtomicU64::new(0),
324            cold_cache_wait_timeout: AtomicU64::new(0),
325            resubscribe_ops: AtomicU64::new(0),
326            resubscribe_attempts: AtomicU64::new(0),
327            resubscribe_applied_keys: AtomicU64::new(0),
328            // v1.4.106 codex 1140 F8: qot push drop counter init.
329            qot_push_dropped_total: AtomicU64::new(0),
330            qot_push_dropped_by_sub_type: [const { AtomicU64::new(0) }; 18],
331            keepalive_timeouts: AtomicU64::new(0),
332            latency_ring: RwLock::new(LatencyRing::new()),
333        }
334    }
335
336    /// 记录一次请求延迟 (纳秒)
337    pub fn record_latency_ns(&self, ns: u64) {
338        self.latency_ring.write().push(ns);
339    }
340
341    /// v1.4.106 codex 1140 F8: 记录一次 qot push 被 drop (channel full / closed).
342    ///
343    /// `sub_type` 范围 0..=17 (proto Qot_Common.SubType). 越界值归桶 0
344    /// (未知). 同时 bump 总计数 + per-sub-type 桶, 保证 dashboard 可分维度.
345    pub fn record_qot_push_dropped(&self, sub_type: i32) {
346        self.qot_push_dropped_total.fetch_add(1, Ordering::Relaxed);
347        let bucket = qot_sub_type_bucket(sub_type);
348        self.qot_push_dropped_by_sub_type[bucket].fetch_add(1, Ordering::Relaxed);
349    }
350
351    /// 记录一次 QOT fanout 因单个客户端 channel 满而丢给该客户端的帧。
352    pub fn record_qot_client_push_backpressure_drop(&self, sub_type: i32) {
353        self.qot_client_push_backpressure_drops
354            .fetch_add(1, Ordering::Relaxed);
355        let bucket = qot_sub_type_bucket(sub_type);
356        self.qot_client_push_backpressure_drops_by_sub_type[bucket].fetch_add(1, Ordering::Relaxed);
357    }
358
359    /// v1.4.106 codex 1140 F8: 读取每个 sub_type 桶的 drop 计数 (snapshot).
360    /// 用于 metrics endpoint render.
361    pub fn qot_push_dropped_per_sub_type(&self) -> [u64; 18] {
362        let mut out = [0u64; 18];
363        for (i, slot) in self.qot_push_dropped_by_sub_type.iter().enumerate() {
364            out[i] = slot.load(Ordering::Relaxed);
365        }
366        out
367    }
368
369    pub fn qot_client_push_backpressure_drops_per_sub_type(&self) -> [u64; 18] {
370        let mut out = [0u64; 18];
371        for (i, slot) in self
372            .qot_client_push_backpressure_drops_by_sub_type
373            .iter()
374            .enumerate()
375        {
376            out[i] = slot.load(Ordering::Relaxed);
377        }
378        out
379    }
380
381    /// 获取延迟统计
382    pub fn latency_stats(&self) -> LatencyStats {
383        self.latency_ring.read().stats()
384    }
385
386    /// 格式化运行时间
387    pub fn uptime_str(&self) -> String {
388        let elapsed = self.start_time.elapsed();
389        let secs = elapsed.as_secs();
390        let days = secs / 86400;
391        let hours = (secs % 86400) / 3600;
392        let mins = (secs % 3600) / 60;
393        let s = secs % 60;
394        if days > 0 {
395            format!("{days}d {hours}h {mins}m {s}s")
396        } else if hours > 0 {
397            format!("{hours}h {mins}m {s}s")
398        } else {
399            format!("{mins}m {s}s")
400        }
401    }
402
403    /// 生成 telnet 可展示的指标报告
404    pub fn report(&self) -> String {
405        let lat = self.latency_stats();
406        let backend_status = if self.backend_online.load(Ordering::Relaxed) == 1 {
407            "ONLINE"
408        } else {
409            "OFFLINE"
410        };
411
412        let total_req = self.total_requests.load(Ordering::Relaxed);
413        let uptime_secs = self.start_time.elapsed().as_secs_f64();
414        let avg_rps = if uptime_secs > 0.0 {
415            total_req as f64 / uptime_secs
416        } else {
417            0.0
418        };
419
420        format!(
421            "=== Gateway Metrics ===\r\n\
422             Uptime: {uptime}\r\n\
423             \r\n\
424             [Connections]\r\n\
425             total_accepted: {total_conn}\r\n\
426             total_disconnected: {total_disconn}\r\n\
427             rejected (limit): {rejected}\r\n\
428             keepalive_timeouts: {ka_timeout}\r\n\
429             \r\n\
430             [Requests]\r\n\
431             total_requests: {total_req}\r\n\
432             total_errors: {total_err}\r\n\
433             avg_rps: {avg_rps:.1}\r\n\
434             response_bytes: {resp_bytes}\r\n\
435             \r\n\
436             [Latency (recent {lat_count} samples)]\r\n\
437             avg: {lat_avg}us  p50: {lat_p50}us  p95: {lat_p95}us  p99: {lat_p99}us  max: {lat_max}us\r\n\
438             \r\n\
439             [Backend]\r\n\
440             status: {backend_status}\r\n\
441             reconnects: {reconnects}\r\n\
442             reconnect_failures: {reconnect_fail}\r\n\
443             pushes_received: {push_recv}\r\n\
444             pushes_sent_to_clients: {push_sent}\r\n\
445             push_send_failures_to_clients: {push_send_failures}\r\n\
446             qot_client_push_backpressure_drops: {qot_client_backpressure_drops}\r\n\
447             \r\n\
448             [Pushes by CMD (v1.4.83 §14)]\r\n\
449             cmd_6212_quote: {push_cmd_quote}\r\n\
450             cmd_4716_trade_legacy: {push_cmd_trade_legacy}\r\n\
451             cmd_14716_trade_new: {push_cmd_trade_new}\r\n\
452             cmd_5300_msg_center: {push_cmd_msg_center}\r\n\
453             cmd_other: {push_cmd_other}\r\n\
454             \r\n\
455             [Pushes by CMD × UTC hour (v1.4.84 §14)]\r\n\
456             cmd_14716_trade_new_hour_0..23: {hour_trade_new}\r\n\
457             cmd_6212_quote_hour_0..23: {hour_quote}\r\n\
458             cmd_4716_trade_legacy_hour_0..23: {hour_trade_legacy}\r\n\
459             cmd_5300_msg_center_hour_0..23: {hour_msg_center}\r\n\
460             \r\n\
461             [Subscriptions]\r\n\
462             subscribe_ops: {sub_ops}\r\n\
463             unsubscribe_ops: {unsub_ops}\r\n\
464             resubscribe_ops: {resub_ops}\r\n\
465             \r\n\
466             [Cold-cache wait (v1.4.110 §P3 #19)]\r\n\
467             total: {cc_total}  hit: {cc_hit}  timeout: {cc_timeout}\r\n",
468            uptime = self.uptime_str(),
469            total_conn = self.total_connections.load(Ordering::Relaxed),
470            total_disconn = self.total_disconnections.load(Ordering::Relaxed),
471            rejected = self.rejected_connections.load(Ordering::Relaxed),
472            ka_timeout = self.keepalive_timeouts.load(Ordering::Relaxed),
473            total_req = total_req,
474            total_err = self.total_request_errors.load(Ordering::Relaxed),
475            resp_bytes = self.total_response_bytes.load(Ordering::Relaxed),
476            lat_count = lat.count.min(LATENCY_RING_SIZE as u64),
477            lat_avg = lat.avg_us,
478            lat_p50 = lat.p50_us,
479            lat_p95 = lat.p95_us,
480            lat_p99 = lat.p99_us,
481            lat_max = lat.max_us,
482            reconnects = self.backend_reconnects.load(Ordering::Relaxed),
483            reconnect_fail = self.backend_reconnect_failures.load(Ordering::Relaxed),
484            push_recv = self.backend_pushes_received.load(Ordering::Relaxed),
485            push_sent = self.client_pushes_sent.load(Ordering::Relaxed),
486            push_send_failures = self.client_push_send_failures.load(Ordering::Relaxed),
487            qot_client_backpressure_drops = self
488                .qot_client_push_backpressure_drops
489                .load(Ordering::Relaxed),
490            push_cmd_quote = self.backend_pushes_cmd_quote.load(Ordering::Relaxed),
491            push_cmd_trade_legacy = self.backend_pushes_cmd_trade_legacy.load(Ordering::Relaxed),
492            push_cmd_trade_new = self.backend_pushes_cmd_trade_new.load(Ordering::Relaxed),
493            push_cmd_msg_center = self.backend_pushes_cmd_msg_center.load(Ordering::Relaxed),
494            push_cmd_other = self.backend_pushes_cmd_other.load(Ordering::Relaxed),
495            hour_trade_new = format_hour_row(&self.backend_pushes_cmd_trade_new_by_hour),
496            hour_quote = format_hour_row(&self.backend_pushes_cmd_quote_by_hour),
497            hour_trade_legacy = format_hour_row(&self.backend_pushes_cmd_trade_legacy_by_hour),
498            hour_msg_center = format_hour_row(&self.backend_pushes_cmd_msg_center_by_hour),
499            sub_ops = self.qot_subscribe_ops.load(Ordering::Relaxed),
500            unsub_ops = self.qot_unsubscribe_ops.load(Ordering::Relaxed),
501            resub_ops = self.resubscribe_ops.load(Ordering::Relaxed),
502            cc_total = self.cold_cache_wait_total.load(Ordering::Relaxed),
503            cc_hit = self.cold_cache_wait_hit.load(Ordering::Relaxed),
504            cc_timeout = self.cold_cache_wait_timeout.load(Ordering::Relaxed),
505        )
506    }
507}
508
509impl Default for GatewayMetrics {
510    fn default() -> Self {
511        Self::new()
512    }
513}
514
515#[cfg(test)]
516mod tests;