futu_backend/
heartbeat.rs1use std::sync::Arc;
2use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
3
4use bytes::Bytes;
5use futu_command_spec::HeartbeatOperation;
6use futu_core::heartbeat::{
7 HeartbeatChannel, HeartbeatFailureAction, HeartbeatMachine, HeartbeatSuccessFacts,
8};
9use futu_core::server_time::{ServerTimeAnchorStore, ServerTimeAnchorUpdate};
10use prost::Message;
11
12use crate::command_runtime::execute_heartbeat;
13use crate::conn::BackendConn;
14use crate::proto_internal::ft_conn_heart_beat::{HeartBeatReq, HeartBeatRsp};
15
16pub use futu_command_spec::{CMD_HEARTBEAT_BROKER, CMD_HEARTBEAT_PLATFORM};
17
18const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
19
20pub fn start_heartbeat(conn: Arc<BackendConn>, interval: Duration) -> tokio::task::JoinHandle<()> {
21 start_heartbeat_task(
22 conn,
23 interval,
24 HeartbeatOperation::Platform,
25 HeartbeatChannel::Platform,
26 None,
27 )
28}
29
30pub fn start_heartbeat_with_server_clock(
31 conn: Arc<BackendConn>,
32 interval: Duration,
33 server_time_store: Arc<ServerTimeAnchorStore>,
34) -> tokio::task::JoinHandle<()> {
35 start_heartbeat_task(
36 conn,
37 interval,
38 HeartbeatOperation::Platform,
39 HeartbeatChannel::Platform,
40 Some(server_time_store),
41 )
42}
43
44pub fn start_broker_heartbeat(
45 conn: Arc<BackendConn>,
46 interval: Duration,
47) -> tokio::task::JoinHandle<()> {
48 start_heartbeat_task(
49 conn,
50 interval,
51 HeartbeatOperation::Broker,
52 HeartbeatChannel::Broker,
53 None,
54 )
55}
56
57fn start_heartbeat_task(
58 conn: Arc<BackendConn>,
59 interval: Duration,
60 operation: HeartbeatOperation,
61 channel: HeartbeatChannel,
62 server_time_store: Option<Arc<ServerTimeAnchorStore>>,
63) -> tokio::task::JoinHandle<()> {
64 tokio::spawn(async move {
65 let interval = if interval.is_zero() {
66 DEFAULT_HEARTBEAT_INTERVAL
68 } else {
69 interval
70 };
71 let mut ticker = tokio::time::interval(interval);
72 ticker.tick().await;
73 let mut machine = HeartbeatMachine::new(channel);
74
75 loop {
76 ticker.tick().await;
77 if !conn.is_connected() {
78 tracing::debug!(
79 ?operation,
80 "heartbeat observed disconnected backend channel"
81 );
82 break;
83 }
84
85 let request_plan = machine.request_plan();
86 let request = HeartBeatReq {
87 pre_time_delay: Some(request_plan.pre_time_delay_ms),
88 };
89 let started_at = Instant::now();
90 let result = execute_heartbeat(
91 conn.as_ref(),
92 operation,
93 Bytes::from(request.encode_to_vec()),
94 )
95 .await;
96 let response_captured_at = Instant::now();
97 let rtt_ms = u64::try_from(
98 response_captured_at
99 .saturating_duration_since(started_at)
100 .as_millis(),
101 )
102 .unwrap_or(u64::MAX);
103 let local_recv_unix_micros = local_unix_micros();
104
105 let response = match result {
106 Ok(response) => response,
107 Err(error) => {
108 tracing::warn!(?operation, %error, "backend heartbeat failed");
109 match machine.record_failure() {
110 HeartbeatFailureAction::DisconnectAndStop => {
111 conn.mark_disconnected_for_reconnect("heartbeat failure");
112 break;
113 }
114 }
115 }
116 };
117
118 let response = match HeartBeatRsp::decode(response.body) {
119 Ok(response) => response,
120 Err(error) => {
121 tracing::warn!(?operation, %error, "failed to decode heartbeat response");
124 continue;
125 }
126 };
127
128 let success = machine.record_success(HeartbeatSuccessFacts {
129 rtt_ms,
130 server_time_secs: response.server_time,
131 server_time_usec: response.server_time_usec,
132 local_recv_unix_micros,
133 });
134 if let (Some(projection), Some(server_time_store)) =
135 (success.server_time_projection, server_time_store.as_ref())
136 {
137 server_time_store.store_update(ServerTimeAnchorUpdate {
138 projection,
139 captured_at: response_captured_at,
140 });
141 }
142 tracing::trace!(
143 ?operation,
144 rtt_ms,
145 result_code = ?response.result_code,
146 next_pre_time_delay_ms = success.next_pre_time_delay_ms,
147 "backend heartbeat ok"
148 );
149 }
150 })
151}
152
153fn local_unix_micros() -> Option<u64> {
154 match SystemTime::now().duration_since(UNIX_EPOCH) {
155 Ok(elapsed) => u64::try_from(elapsed.as_micros()).ok(),
156 Err(error) => {
157 tracing::warn!(%error, "system clock is before UNIX_EPOCH; heartbeat clock not updated");
158 None
159 }
160 }
161}