futu_gateway_core/bridge/
broker_runtime.rs1use std::collections::BTreeSet;
9use std::fmt::Write as _;
10use std::sync::Arc;
11
12use arc_swap::ArcSwap;
13use futu_backend::auth::BrokerAuth;
14use futu_backend::conn::BackendConn;
15use futu_backend::overnight_whitelist::OvernightWhitelistCache;
16
17use super::clock::wall_clock_since_unix_epoch_or_zero;
18use super::types::BrokerStatus;
19use super::{BrokerConnMeta, SharedBackend};
20
21pub const BROKER_RECONNECT_MAX_CONSECUTIVE_FAILURES: u32 = 6;
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24enum BrokerChannelState {
25 Connected,
26 SustainDisconnected,
27 #[default]
28 Reconnecting,
29}
30
31impl BrokerChannelState {
32 fn status_label(self, backend_present: bool, connected: bool) -> &'static str {
33 match self {
34 Self::Connected if backend_present && connected => "ready",
35 Self::Connected if backend_present => "disconnected",
36 Self::Connected | Self::Reconnecting => "reconnecting",
37 Self::SustainDisconnected => "sustain_disconnected",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
43struct BrokerHealthState {
44 channel_state: BrokerChannelState,
45 consecutive_failures: u32,
46 max_consecutive_failures: u32,
47 last_reconnect_attempt_at_ms: i64,
48 last_reconnect_success_at_ms: i64,
49 last_reconnect_failure_at_ms: i64,
50}
51
52#[derive(Clone)]
53pub struct BrokerRuntime {
54 backend: SharedBackend,
55 brokers: Arc<dashmap::DashMap<u32, Arc<BackendConn>>>,
56 broker_conn_meta: Arc<dashmap::DashMap<u32, BrokerConnMeta>>,
57 broker_health: Arc<dashmap::DashMap<u32, BrokerHealthState>>,
58 overnight_whitelist: OvernightWhitelistCache,
59 overnight_whitelist_workers: Arc<dashmap::DashSet<u32>>,
60}
61
62impl BrokerRuntime {
63 pub fn empty() -> Self {
64 let backend: SharedBackend = Arc::new(ArcSwap::new(Arc::new(None)));
65 Self::new(
66 backend,
67 Arc::new(dashmap::DashMap::new()),
68 Arc::new(dashmap::DashMap::new()),
69 )
70 }
71
72 pub fn new(
73 backend: SharedBackend,
74 brokers: Arc<dashmap::DashMap<u32, Arc<BackendConn>>>,
75 broker_conn_meta: Arc<dashmap::DashMap<u32, BrokerConnMeta>>,
76 ) -> Self {
77 Self {
78 backend,
79 brokers,
80 broker_conn_meta,
81 broker_health: Arc::new(dashmap::DashMap::new()),
82 overnight_whitelist: OvernightWhitelistCache::new(),
83 overnight_whitelist_workers: Arc::new(dashmap::DashSet::new()),
84 }
85 }
86
87 pub fn platform_backend(&self) -> Option<Arc<BackendConn>> {
88 self.backend.load().as_ref().clone()
89 }
90
91 pub fn set_platform_backend(&self, backend: Arc<BackendConn>) {
92 self.backend.store(Arc::new(Some(backend)));
93 }
94
95 pub fn overnight_whitelist(&self) -> OvernightWhitelistCache {
100 self.overnight_whitelist.clone()
101 }
102
103 pub fn mark_overnight_whitelist_worker_started(&self, broker_id: u32) -> bool {
104 self.overnight_whitelist_workers.insert(broker_id)
105 }
106
107 pub fn platform_connected(&self) -> bool {
108 self.platform_backend()
109 .as_ref()
110 .is_some_and(|backend| backend.is_connected())
111 }
112
113 pub fn clear_platform_backend(&self) {
114 self.backend.store(Arc::new(None));
115 }
116
117 pub fn broker_backend(&self, broker_id: u32) -> Option<Arc<BackendConn>> {
118 self.brokers.get(&broker_id).and_then(|conn| {
119 if conn.is_connected() {
120 Some(Arc::clone(conn.value()))
121 } else {
122 None
123 }
124 })
125 }
126
127 pub fn broker_customer_id(&self, broker_id: u32) -> Option<u64> {
128 self.broker_conn_meta
129 .get(&broker_id)
130 .map(|meta| meta.broker_auth.customer_id)
131 }
132
133 pub fn broker_meta(&self, broker_id: u32) -> Option<BrokerConnMeta> {
134 self.broker_conn_meta
135 .get(&broker_id)
136 .map(|meta| meta.value().clone())
137 }
138
139 pub fn active_broker_ids(&self) -> Vec<u32> {
140 self.brokers.iter().map(|entry| *entry.key()).collect()
141 }
142
143 pub fn all_broker_backends(&self) -> Vec<(u32, Arc<BackendConn>)> {
144 let mut out: Vec<_> = self
145 .brokers
146 .iter()
147 .filter_map(|r| {
148 if r.value().is_connected() {
149 Some((*r.key(), Arc::clone(r.value())))
150 } else {
151 None
152 }
153 })
154 .collect();
155 out.sort_by_key(|(id, _)| *id);
156 out
157 }
158
159 pub fn mark_all_broker_backends_disconnected(&self, reason: &'static str) -> usize {
167 let mut count = 0;
168 for entry in self.brokers.iter() {
169 self.mark_broker_sustain_disconnected(*entry.key(), reason);
170 entry.value().mark_disconnected_for_reconnect(reason);
171 count += 1;
172 }
173 if count > 0 {
174 tracing::warn!(
175 broker_count = count,
176 reason,
177 "marked broker backends disconnected for reconnect"
178 );
179 }
180 count
181 }
182
183 pub fn remove_broker_channel(&self, broker_id: u32) {
184 self.brokers.remove(&broker_id);
185 self.broker_conn_meta.remove(&broker_id);
186 self.broker_health.remove(&broker_id);
187 }
188
189 pub fn remove_broker_backend(&self, broker_id: u32) {
190 self.brokers.remove(&broker_id);
191 }
192
193 pub fn mark_broker_sustain_disconnected(&self, broker_id: u32, reason: &'static str) {
194 self.update_broker_health(broker_id, |state| {
195 state.channel_state = BrokerChannelState::SustainDisconnected;
196 if state.max_consecutive_failures == 0 {
197 state.max_consecutive_failures = BROKER_RECONNECT_MAX_CONSECUTIVE_FAILURES;
198 }
199 });
200 tracing::warn!(
201 broker_id,
202 reason,
203 "broker channel entered C++ sustain-disconnected state"
204 );
205 }
206
207 pub fn update_broker_auth(&self, broker_id: u32, broker_auth: BrokerAuth) -> bool {
208 if let Some(mut entry) = self.broker_conn_meta.get_mut(&broker_id) {
209 entry.broker_auth = broker_auth;
210 true
211 } else {
212 false
213 }
214 }
215
216 pub fn register_broker_channel(
217 &self,
218 broker_id: u32,
219 conn: Arc<BackendConn>,
220 meta: BrokerConnMeta,
221 ) {
222 self.broker_conn_meta.insert(broker_id, meta);
223 self.brokers.insert(broker_id, conn);
224 self.mark_broker_reconnect_success(broker_id);
225 }
226
227 pub fn set_broker_meta(&self, broker_id: u32, meta: BrokerConnMeta) {
228 self.broker_conn_meta.insert(broker_id, meta);
229 }
230
231 pub fn set_broker_backend(&self, broker_id: u32, conn: Arc<BackendConn>) {
232 self.brokers.insert(broker_id, conn);
233 self.mark_broker_reconnect_success(broker_id);
234 }
235
236 pub fn mark_broker_reconnecting(&self, broker_id: u32, max_failures: u32) {
237 let now_ms = unix_now_ms("broker_reconnect_attempt");
238 self.update_broker_health(broker_id, |state| {
239 state.channel_state = BrokerChannelState::Reconnecting;
240 state.max_consecutive_failures = max_failures;
241 state.last_reconnect_attempt_at_ms = now_ms;
242 });
243 }
244
245 pub fn mark_broker_reconnect_attempt(&self, broker_id: u32, max_failures: u32) {
246 self.mark_broker_reconnecting(broker_id, max_failures);
247 }
248
249 pub fn mark_broker_reconnect_failure(
250 &self,
251 broker_id: u32,
252 consecutive_failures: u32,
253 max_failures: u32,
254 ) {
255 let now_ms = unix_now_ms("broker_reconnect_failure");
256 self.update_broker_health(broker_id, |state| {
257 state.channel_state = BrokerChannelState::Reconnecting;
258 state.consecutive_failures = consecutive_failures;
259 state.max_consecutive_failures = max_failures;
260 state.last_reconnect_failure_at_ms = now_ms;
261 });
262 }
263
264 pub fn mark_broker_reconnect_success(&self, broker_id: u32) {
265 let now_ms = unix_now_ms("broker_reconnect_success");
266 self.update_broker_health(broker_id, |state| {
267 state.channel_state = BrokerChannelState::Connected;
268 state.consecutive_failures = 0;
269 if state.max_consecutive_failures == 0 {
270 state.max_consecutive_failures = BROKER_RECONNECT_MAX_CONSECUTIVE_FAILURES;
271 }
272 state.last_reconnect_success_at_ms = now_ms;
273 });
274 }
275
276 fn update_broker_health(&self, broker_id: u32, update: impl FnOnce(&mut BrokerHealthState)) {
277 let mut entry = self.broker_health.entry(broker_id).or_default();
278 update(&mut entry);
279 }
280
281 pub fn broker_statuses(&self) -> Vec<BrokerStatus> {
282 let mut ids = BTreeSet::new();
283 ids.extend(self.brokers.iter().map(|entry| *entry.key()));
284 ids.extend(self.broker_conn_meta.iter().map(|entry| *entry.key()));
285 ids.extend(self.broker_health.iter().map(|entry| *entry.key()));
286
287 ids.into_iter()
288 .map(|broker_id| {
289 let name = futu_backend::auth::broker_config(broker_id)
290 .map(|c| c.name)
291 .unwrap_or("unknown");
292 let meta = self.broker_conn_meta.get(&broker_id);
293 let backend = self.broker_backend(broker_id);
294 let connected = backend.as_ref().is_some_and(|conn| conn.is_connected());
295 let health = self
296 .broker_health
297 .get(&broker_id)
298 .map(|entry| entry.value().clone())
299 .unwrap_or_default();
300 let backend_present = self.brokers.contains_key(&broker_id);
301 let status = health
302 .channel_state
303 .status_label(backend_present, connected)
304 .to_string();
305
306 BrokerStatus {
307 broker_id,
308 name: name.to_string(),
309 status,
310 connected,
311 addr: meta.as_ref().map(|m| m.connect_addr.clone()),
312 keep_alive_secs: meta.as_ref().map(|m| m.keep_alive_secs),
313 consecutive_failures: health.consecutive_failures,
314 max_consecutive_failures: health.max_consecutive_failures,
315 last_reconnect_attempt_at_ms: non_zero_ms(health.last_reconnect_attempt_at_ms),
316 last_reconnect_success_at_ms: non_zero_ms(health.last_reconnect_success_at_ms),
317 last_reconnect_failure_at_ms: non_zero_ms(health.last_reconnect_failure_at_ms),
318 }
319 })
320 .collect()
321 }
322
323 #[must_use]
329 pub fn render_prometheus(&self) -> String {
330 let statuses = self.broker_statuses();
331 let mut s = String::with_capacity(1024 + statuses.len() * 512);
332
333 s.push_str(
334 "# HELP futu_gateway_broker_tcp_connected Broker TCP connected state (1=connected,0=not connected)\n# TYPE futu_gateway_broker_tcp_connected gauge\n",
335 );
336 for status in &statuses {
337 let labels = broker_metric_labels(status);
338 let _ = writeln!(
339 s,
340 "futu_gateway_broker_tcp_connected{{{labels}}} {}",
341 u8::from(status.connected)
342 );
343 }
344
345 s.push_str(
346 "# HELP futu_gateway_broker_tcp_reconnect_consecutive_failures Consecutive broker TCP reconnect failures\n# TYPE futu_gateway_broker_tcp_reconnect_consecutive_failures gauge\n",
347 );
348 for status in &statuses {
349 let labels = broker_metric_labels(status);
350 let _ = writeln!(
351 s,
352 "futu_gateway_broker_tcp_reconnect_consecutive_failures{{{labels}}} {}",
353 status.consecutive_failures
354 );
355 }
356
357 s.push_str(
358 "# HELP futu_gateway_broker_tcp_reconnect_max_consecutive_failures Broker TCP reconnect failure count before RepullAuthCode self-heal\n# TYPE futu_gateway_broker_tcp_reconnect_max_consecutive_failures gauge\n",
359 );
360 for status in &statuses {
361 let labels = broker_metric_labels(status);
362 let _ = writeln!(
363 s,
364 "futu_gateway_broker_tcp_reconnect_max_consecutive_failures{{{labels}}} {}",
365 status.max_consecutive_failures
366 );
367 }
368
369 s.push_str(
370 "# HELP futu_gateway_broker_tcp_reconnect_last_attempt_at_ms Last broker TCP reconnect attempt timestamp in Unix milliseconds (0=never)\n# TYPE futu_gateway_broker_tcp_reconnect_last_attempt_at_ms gauge\n",
371 );
372 for status in &statuses {
373 let labels = broker_metric_labels(status);
374 let _ = writeln!(
375 s,
376 "futu_gateway_broker_tcp_reconnect_last_attempt_at_ms{{{labels}}} {}",
377 status.last_reconnect_attempt_at_ms.unwrap_or(0)
378 );
379 }
380
381 s.push_str(
382 "# HELP futu_gateway_broker_tcp_reconnect_last_success_at_ms Last broker TCP reconnect success timestamp in Unix milliseconds (0=never)\n# TYPE futu_gateway_broker_tcp_reconnect_last_success_at_ms gauge\n",
383 );
384 for status in &statuses {
385 let labels = broker_metric_labels(status);
386 let _ = writeln!(
387 s,
388 "futu_gateway_broker_tcp_reconnect_last_success_at_ms{{{labels}}} {}",
389 status.last_reconnect_success_at_ms.unwrap_or(0)
390 );
391 }
392
393 s.push_str(
394 "# HELP futu_gateway_broker_tcp_reconnect_last_failure_at_ms Last broker TCP reconnect failure timestamp in Unix milliseconds (0=never)\n# TYPE futu_gateway_broker_tcp_reconnect_last_failure_at_ms gauge\n",
395 );
396 for status in &statuses {
397 let labels = broker_metric_labels(status);
398 let _ = writeln!(
399 s,
400 "futu_gateway_broker_tcp_reconnect_last_failure_at_ms{{{labels}}} {}",
401 status.last_reconnect_failure_at_ms.unwrap_or(0)
402 );
403 }
404
405 s
406 }
407}
408
409fn non_zero_ms(value: i64) -> Option<i64> {
410 (value > 0).then_some(value)
411}
412
413fn unix_now_ms(context: &'static str) -> i64 {
414 let elapsed = wall_clock_since_unix_epoch_or_zero(context).as_millis();
415 elapsed.min(i64::MAX as u128) as i64
416}
417
418fn broker_metric_labels(status: &BrokerStatus) -> String {
419 format!(
420 "broker_id=\"{}\",broker={}",
421 status.broker_id,
422 prom_label(&status.name)
423 )
424}
425
426fn prom_label(value: &str) -> String {
427 let mut out = String::with_capacity(value.len() + 2);
428 out.push('"');
429 for c in value.chars() {
430 match c {
431 '\\' => out.push_str("\\\\"),
432 '"' => out.push_str("\\\""),
433 '\n' => out.push_str("\\n"),
434 _ => out.push(c),
435 }
436 }
437 out.push('"');
438 out
439}
440
441impl Default for BrokerRuntime {
442 fn default() -> Self {
443 Self::empty()
444 }
445}
446
447#[cfg(test)]
448mod tests;