futu_gateway_core/bridge/push_health/push/
f2.rs1use std::sync::atomic::Ordering;
2
3use super::{PushHealth, unix_now_ms};
4
5pub(in crate::bridge::push_health) const F2_EXHAUST_WINDOW_MS: i64 = 30 * 60 * 1000;
21pub(in crate::bridge::push_health) const F2_TRIP_THRESHOLD: usize = 3;
23pub(in crate::bridge::push_health) const F2_COOLDOWN_MS: i64 = 60 * 60 * 1000;
25
26#[derive(Debug, Default, Clone)]
28pub(super) struct F2AccState {
29 exhausted_at_ms: Vec<i64>,
31 tripped_at_ms: i64,
33}
34
35impl F2AccState {
36 fn trim_window(&mut self, now_ms: i64) {
38 let cutoff = now_ms - F2_EXHAUST_WINDOW_MS;
39 self.exhausted_at_ms.retain(|&t| t >= cutoff);
40 }
41}
42
43impl PushHealth {
44 #[cfg(test)]
45 pub(in crate::bridge::push_health) fn set_f2_tripped_at_ms_for_test(
46 &self,
47 acc_id: u32,
48 tripped_at_ms: i64,
49 ) {
50 if let Ok(mut guard) = self.f2_acc_state.lock() {
51 guard.entry(acc_id).or_default().tripped_at_ms = tripped_at_ms;
52 }
53 }
54
55 #[cfg(test)]
56 pub(in crate::bridge::push_health) fn push_f2_exhausted_at_ms_for_test(
57 &self,
58 acc_id: u32,
59 exhausted_at_ms: i64,
60 ) {
61 if let Ok(mut guard) = self.f2_acc_state.lock() {
62 guard
63 .entry(acc_id)
64 .or_default()
65 .exhausted_at_ms
66 .push(exhausted_at_ms);
67 }
68 }
69
70 #[cfg(test)]
71 pub(in crate::bridge::push_health) fn f2_state_snapshot_for_test(
72 &self,
73 acc_id: u32,
74 ) -> Option<(usize, i64)> {
75 self.f2_acc_state.lock().ok().and_then(|guard| {
76 guard
77 .get(&acc_id)
78 .map(|state| (state.exhausted_at_ms.len(), state.tripped_at_ms))
79 })
80 }
81
82 pub fn record_f2_exhausted(&self, acc_id: u32) -> bool {
89 let now_ms = unix_now_ms();
90 let mut guard = match self.f2_acc_state.lock() {
91 Ok(g) => g,
92 Err(poisoned) => poisoned.into_inner(),
93 };
94 let st = guard.entry(acc_id).or_default();
95 st.trim_window(now_ms);
96 st.exhausted_at_ms.push(now_ms);
97 let already_tripped =
98 st.tripped_at_ms != 0 && now_ms.saturating_sub(st.tripped_at_ms) < F2_COOLDOWN_MS;
99 if already_tripped {
100 return false;
102 }
103 if st.exhausted_at_ms.len() >= F2_TRIP_THRESHOLD {
104 st.tripped_at_ms = now_ms;
105 drop(guard);
107 self.f2_circuit_tripped_total
108 .fetch_add(1, Ordering::Relaxed);
109 return true;
110 }
111 false
112 }
113
114 pub fn is_f2_circuit_tripped(&self, acc_id: u32) -> bool {
120 let now_ms = unix_now_ms();
121 let mut guard = match self.f2_acc_state.lock() {
122 Ok(g) => g,
123 Err(poisoned) => poisoned.into_inner(),
124 };
125 let Some(st) = guard.get_mut(&acc_id) else {
126 return false;
127 };
128 if st.tripped_at_ms == 0 {
129 return false;
130 }
131 let elapsed = now_ms.saturating_sub(st.tripped_at_ms);
132 if elapsed >= F2_COOLDOWN_MS {
133 st.tripped_at_ms = 0;
135 st.exhausted_at_ms.clear();
136 return false;
137 }
138 true
139 }
140
141 #[cfg(test)]
143 pub fn reset_f2_circuit(&self, acc_id: u32) {
144 if let Ok(mut guard) = self.f2_acc_state.lock()
145 && let Some(st) = guard.get_mut(&acc_id)
146 {
147 st.tripped_at_ms = 0;
148 st.exhausted_at_ms.clear();
149 }
150 }
151}