futu_backend/auth/commconfig/
runner.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use futu_domain_auth::{
8 COMMCONFIG_RETRY_DELAY_MS_LIKE_CPP, CommConfigPageAction, plan_commconfig_page_action_like_cpp,
9 plan_commconfig_runtime_failure_like_cpp,
10};
11
12use super::accessors::delay_until_next_refresh;
13use super::clock::server_now_ts;
14use super::fetch_page::{CommConfigPageFetchError, fetch_page};
15use super::projection::project_common_config;
16use super::snapshot::SharedCommConfig;
17use super::store::{CommConfigStoreError, save_last_good};
18use super::transaction::{CommConfigTransaction, CommConfigTransactionStep, CommonConfigDocument};
19use super::types::CommConfigSource;
20use super::wire::decode_commconfig_page;
21
22pub const COMMCONFIG_PRE_LOGIN_TIMEOUT: Duration = Duration::from_secs(30);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CommConfigPersistence {
33 Persisted,
34 MemoryOnly,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum CommConfigBootstrapOutcome {
39 Published { persistence: CommConfigPersistence },
40 Dropped,
41 TimedOut,
42}
43
44#[async_trait]
45pub(super) trait CommConfigPageSource: Send + Sync {
46 async fn fetch_page(
47 &self,
48 begin_id: i32,
49 ) -> Result<serde_json::Value, CommConfigPageFetchError>;
50}
51
52struct ReqwestCommConfigPageSource {
53 http: reqwest::Client,
54 client_type: u8,
55 device_id: String,
56 user_id: u64,
57 svr_time_offset: i64,
58}
59
60#[async_trait]
61impl CommConfigPageSource for ReqwestCommConfigPageSource {
62 async fn fetch_page(
63 &self,
64 begin_id: i32,
65 ) -> Result<serde_json::Value, CommConfigPageFetchError> {
66 fetch_page(
67 &self.http,
68 self.client_type,
69 &self.device_id,
70 self.user_id,
71 begin_id,
72 self.svr_time_offset,
73 )
74 .await
75 }
76}
77
78#[derive(Debug, PartialEq)]
79pub(super) enum CommConfigRunOutcome {
80 Dropped,
81 Committed {
82 document: CommonConfigDocument,
83 next_refresh_ts: i64,
84 },
85}
86
87pub(super) async fn run_transaction<S>(source: &S, svr_time_offset: i64) -> CommConfigRunOutcome
88where
89 S: CommConfigPageSource + ?Sized,
90{
91 let mut transaction = CommConfigTransaction::new();
92 loop {
93 let begin_id = transaction.current_begin_id();
94 let json = match source.fetch_page(begin_id).await {
95 Ok(json) => json,
96 Err(error) => {
97 let action = plan_commconfig_runtime_failure_like_cpp(error.runtime_failure());
98 let CommConfigPageAction::RetrySamePage { delay_ms, reason } = action else {
99 unreachable!("runtime failure policy must retry the same page");
100 };
101 tracing::warn!(
102 error = %error,
103 begin_id,
104 ?reason,
105 delay_ms,
106 "commconfig: page fetch failed; retrying same page"
107 );
108 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
109 continue;
110 }
111 };
112 let page = decode_commconfig_page(&json);
113 let ret_code = page.facts.ret_code;
114 let ret_msg = page.ret_msg.clone();
115 let action = plan_commconfig_page_action_like_cpp(page.facts);
116 let step = transaction.apply_page(page, action, server_now_ts(svr_time_offset));
117 match step {
118 CommConfigTransactionStep::RetainedForRetry { begin_id } => {
119 let CommConfigPageAction::RetrySamePage { delay_ms, reason } = action else {
120 unreachable!("retained transaction requires retry action");
121 };
122 tracing::warn!(
123 ?ret_code,
124 ?ret_msg,
125 begin_id,
126 ?reason,
127 delay_ms,
128 "commconfig: retryable business response; retrying same page"
129 );
130 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
131 }
132 CommConfigTransactionStep::Dropped => {
133 tracing::warn!(
134 ?ret_code,
135 ?ret_msg,
136 ?action,
137 "commconfig: dropping incomplete transaction"
138 );
139 return CommConfigRunOutcome::Dropped;
140 }
141 CommConfigTransactionStep::FetchNext { .. } => {}
142 CommConfigTransactionStep::Committed {
143 document,
144 next_refresh_ts,
145 } => {
146 return CommConfigRunOutcome::Committed {
147 document,
148 next_refresh_ts,
149 };
150 }
151 }
152 }
153}
154
155pub(super) fn publish_live_snapshot(
156 shared: &SharedCommConfig,
157 document: &CommonConfigDocument,
158 next_refresh_ts: i64,
159) {
160 let generation = shared.load().generation.saturating_add(1);
161 let snapshot = project_common_config(
162 document,
163 next_refresh_ts,
164 CommConfigSource::Live,
165 generation,
166 );
167 shared.store(Arc::new(snapshot));
168}
169
170fn persist_and_publish_complete_transaction(
171 shared: &SharedCommConfig,
172 document: &CommonConfigDocument,
173 next_refresh_ts: i64,
174 persist: impl FnOnce(&CommonConfigDocument) -> Result<(), CommConfigStoreError>,
175) -> CommConfigPersistence {
176 let persistence = match persist(document) {
177 Ok(()) => CommConfigPersistence::Persisted,
178 Err(error) => {
179 tracing::warn!(
180 error = %error,
181 "commconfig: failed to persist complete last-good; publishing in memory"
182 );
183 CommConfigPersistence::MemoryOnly
184 }
185 };
186 publish_live_snapshot(shared, document, next_refresh_ts);
187 let current = shared.load();
188 tracing::info!(
189 generation = current.generation,
190 platform_pools = current.guaranteed_ip.len(),
191 broker_pools = current.guaranteed_ip_broker.len(),
192 web_pools = current.guaranteed_ip_web.len(),
193 next_refresh_ts,
194 ?persistence,
195 "commconfig: fetched complete transaction and published"
196 );
197 persistence
198}
199
200pub(super) async fn bootstrap_from_source_with_persistence<S>(
201 shared: &SharedCommConfig,
202 source: &S,
203 svr_time_offset: i64,
204 deadline: Duration,
205 persist: impl FnOnce(&CommonConfigDocument) -> Result<(), CommConfigStoreError>,
206) -> CommConfigBootstrapOutcome
207where
208 S: CommConfigPageSource + ?Sized,
209{
210 match tokio::time::timeout(deadline, run_transaction(source, svr_time_offset)).await {
211 Err(_) => {
212 tracing::warn!(
213 deadline_secs = deadline.as_secs(),
214 "commconfig: pre-login transaction timed out; retaining last-good"
215 );
216 CommConfigBootstrapOutcome::TimedOut
217 }
218 Ok(CommConfigRunOutcome::Dropped) => CommConfigBootstrapOutcome::Dropped,
219 Ok(CommConfigRunOutcome::Committed {
220 document,
221 next_refresh_ts,
222 }) => CommConfigBootstrapOutcome::Published {
223 persistence: persist_and_publish_complete_transaction(
224 shared,
225 &document,
226 next_refresh_ts,
227 persist,
228 ),
229 },
230 }
231}
232
233pub async fn bootstrap_before_login(
234 shared: &SharedCommConfig,
235 http: reqwest::Client,
236 client_type: u8,
237 device_id: String,
238 user_id: u64,
239 svr_time_offset: i64,
240) -> CommConfigBootstrapOutcome {
241 let source = ReqwestCommConfigPageSource {
242 http,
243 client_type,
244 device_id,
245 user_id,
246 svr_time_offset,
247 };
248 bootstrap_from_source_with_persistence(
249 shared,
250 &source,
251 svr_time_offset,
252 COMMCONFIG_PRE_LOGIN_TIMEOUT,
253 save_last_good,
254 )
255 .await
256}
257
258pub fn spawn_actor(
259 shared: SharedCommConfig,
260 http: reqwest::Client,
261 client_type: u8,
262 device_id: String,
263 user_id: u64,
264 svr_time_offset: i64,
265) -> tokio::task::JoinHandle<()> {
266 tokio::spawn(async move {
267 let source = ReqwestCommConfigPageSource {
268 http,
269 client_type,
270 device_id,
271 user_id,
272 svr_time_offset,
273 };
274 tokio::time::sleep(Duration::from_millis(COMMCONFIG_RETRY_DELAY_MS_LIKE_CPP)).await;
275
276 loop {
277 let sleep_secs = match run_transaction(&source, svr_time_offset).await {
278 CommConfigRunOutcome::Committed {
279 document,
280 next_refresh_ts,
281 } => {
282 persist_and_publish_complete_transaction(
283 &shared,
284 &document,
285 next_refresh_ts,
286 save_last_good,
287 );
288 delay_until_next_refresh(next_refresh_ts, server_now_ts(svr_time_offset))
289 }
290 CommConfigRunOutcome::Dropped => {
291 delay_until_next_refresh(0, server_now_ts(svr_time_offset))
292 }
293 };
294 tokio::time::sleep(Duration::from_secs(sleep_secs)).await;
295 }
296 })
297}