Skip to main content

futu_backend/
heartbeat.rs

1// 后端心跳保活
2//
3// C++ 分两个 cmd_id(`channel_impl.h:62-63`):
4// - `kCmdHeartbeatPlatform = 6003` —— Platform 通道心跳
5// - `kCmdHeartbeatBroker   = 1003` —— Broker 通道心跳
6//
7// v1.4.7 之前全部硬编码 1003 发到 Platform 通道,服务端返回
8// `code=-102 CONN can not find command service`。
9//
10// 心跳必须加密: body = encrypt(sec_data(4B BE) + HeartBeatReq proto)
11
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::conn::BackendConn;
16use crate::proto_internal::ft_conn_heart_beat::HeartBeatReq;
17
18/// Platform 通道心跳命令 ID(C++ `kCmdHeartbeatPlatform`)
19pub const CMD_HEARTBEAT_PLATFORM: u16 = 6003;
20/// Broker 通道心跳命令 ID(C++ `kCmdHeartbeatBroker`)
21pub const CMD_HEARTBEAT_BROKER: u16 = 1003;
22
23/// 启动 **Platform 通道**心跳任务(cmd=6003)
24pub fn start_heartbeat(conn: Arc<BackendConn>, interval: Duration) -> tokio::task::JoinHandle<()> {
25    start_heartbeat_with_cmd(conn, interval, CMD_HEARTBEAT_PLATFORM, "platform")
26}
27
28/// 启动 **Broker 通道**心跳任务(cmd=1003)
29pub fn start_broker_heartbeat(
30    conn: Arc<BackendConn>,
31    interval: Duration,
32) -> tokio::task::JoinHandle<()> {
33    start_heartbeat_with_cmd(conn, interval, CMD_HEARTBEAT_BROKER, "broker")
34}
35
36/// 通用心跳任务。连续 3 次失败后退出任务(触发重连逻辑)。
37fn start_heartbeat_with_cmd(
38    conn: Arc<BackendConn>,
39    interval: Duration,
40    cmd_id: u16,
41    channel_name: &'static str,
42) -> tokio::task::JoinHandle<()> {
43    tokio::spawn(async move {
44        let mut ticker = tokio::time::interval(interval);
45        ticker.tick().await; // 跳过首次
46        let mut fail_count = 0u32;
47        const MAX_FAILURES: u32 = 3;
48
49        loop {
50            ticker.tick().await;
51
52            let req = HeartBeatReq {
53                pre_time_delay: Some(0),
54            };
55            let body = prost::Message::encode_to_vec(&req);
56
57            match conn.request(cmd_id, body).await {
58                Ok(resp) => {
59                    tracing::trace!(
60                        channel = channel_name,
61                        cmd_id,
62                        body_len = resp.body.len(),
63                        "backend heartbeat ok"
64                    );
65                    fail_count = 0; // 重置失败计数
66                }
67                Err(e) => {
68                    fail_count += 1;
69                    tracing::warn!(
70                        channel = channel_name,
71                        cmd_id,
72                        error = %e,
73                        fail_count,
74                        max = MAX_FAILURES,
75                        "backend heartbeat failed"
76                    );
77                    if fail_count >= MAX_FAILURES {
78                        tracing::error!(
79                            channel = channel_name,
80                            "heartbeat failed {MAX_FAILURES} times, connection likely lost"
81                        );
82                        break;
83                    }
84                }
85            }
86        }
87    })
88}