futu_backend/
valid_brokers.rs1use futu_core::{
24 broker_discovery::ensure_broker_discovery_backend_success,
25 error::{FutuError, Result},
26};
27use prost::Message;
28use std::future::Future;
29
30use futu_command_runtime::CommandResponse;
31pub use futu_command_spec::CMD_FETCH_VALID_BROKER_LIST;
32use futu_command_spec::{BrokerDiscoveryOperation, broker_discovery_command};
33
34use crate::auth::redact::uid_log_fingerprint;
35use crate::command_runtime::execute_broker_discovery;
36use crate::conn::BackendConn;
37use crate::proto_internal::ft_conn_bind::{
38 CidStatusChangePush, GetValidBrokerListReq, GetValidBrokerListRsp,
39};
40
41mod store;
42pub use store::{load_last_good_valid_broker_ids, save_last_good_valid_broker_ids};
43
44pub fn decode_valid_broker_list_changed_push(body: &[u8]) -> Result<CidStatusChangePush> {
49 CidStatusChangePush::decode(body)
50 .map_err(|error| FutuError::Codec(format!("CMD20177 decode: {error}")))
51}
52
53pub async fn fetch_valid_broker_list(backend: &BackendConn, uid: u64) -> Result<Vec<u32>> {
56 fetch_valid_broker_list_with_executor(uid, |body| {
57 execute_broker_discovery(backend, BrokerDiscoveryOperation::ValidBrokerList, body)
58 })
59 .await
60}
61
62async fn fetch_valid_broker_list_with_executor<F, Fut>(uid: u64, mut execute: F) -> Result<Vec<u32>>
63where
64 F: FnMut(bytes::Bytes) -> Fut,
65 Fut: Future<Output = Result<CommandResponse>>,
66{
67 let req = GetValidBrokerListReq { uid: Some(uid) };
68 let body = bytes::Bytes::from(req.encode_to_vec());
69 tracing::debug!(
70 uid_fp = %uid_log_fingerprint(uid),
71 body_len = body.len(),
72 "sending CMD20176 GetValidBrokerListReq"
73 );
74
75 let max_attempts =
76 broker_discovery_command(BrokerDiscoveryOperation::ValidBrokerList).max_timeout_attempts;
77 let mut attempt = 0;
78 let resp = loop {
79 attempt += 1;
80 match execute(body.clone()).await {
81 Err(FutuError::Timeout) if attempt < max_attempts => {
82 tracing::warn!(attempt, max_attempts, "CMD20176 timeout; retrying like C++");
83 }
84 result => break result?,
85 }
86 };
87
88 let rsp = GetValidBrokerListRsp::decode(resp.body.as_ref())
89 .map_err(|e| FutuError::Codec(format!("CMD20176 decode: {e}")))?;
90
91 if let Err(err) = ensure_broker_discovery_backend_success(rsp.ret_code) {
92 let ret_code = err.ret_code();
93 return Err(FutuError::ServerError {
94 ret_type: ret_code,
95 msg: format!(
96 "CMD20176 ret_code={ret_code} msg={:?}",
97 rsp.ret_msg.as_deref().unwrap_or("")
98 ),
99 });
100 }
101
102 tracing::info!(
103 uid_fp = %uid_log_fingerprint(rsp.uid.unwrap_or(0)),
104 count = rsp.broker_ids.len(),
105 broker_ids = ?rsp.broker_ids,
106 "CMD20176 valid broker list received"
107 );
108 Ok(rsp.broker_ids)
109}
110
111pub fn diff_broker_sources(auth_code_broker_ids: &[u32], cmd20176_broker_ids: &[u32]) -> Vec<u32> {
117 use std::collections::HashSet;
118 let auth_set: HashSet<u32> = auth_code_broker_ids.iter().copied().collect();
119 let cmd_set: HashSet<u32> = cmd20176_broker_ids.iter().copied().collect();
120
121 let only_auth: Vec<u32> = auth_set.difference(&cmd_set).copied().collect();
122 let only_cmd: Vec<u32> = cmd_set.difference(&auth_set).copied().collect();
123
124 if !only_auth.is_empty() || !only_cmd.is_empty() {
125 tracing::warn!(
126 only_in_auth_code_list = ?only_auth,
127 only_in_cmd20176 = ?only_cmd,
128 "broker source mismatch: HTTP auth_code_list vs CMD20176 differ — \
129 using CMD20176 as authority"
130 );
131 } else {
132 tracing::debug!(
133 count = auth_code_broker_ids.len(),
134 "broker source consistent between auth_code_list and CMD20176"
135 );
136 }
137
138 cmd20176_broker_ids.to_vec()
139}
140
141#[cfg(test)]
142mod tests;