Skip to main content

futu_backend/
valid_brokers.rs

1//! CMD 20176 `kCmdFetchValidBrokerList` —— 获取 cid 当前有效的券商列表。
2//!
3//! 对齐 C++ `FTLogin/Src/ftlogin/channel/impl/logger.cpp:1425-1500` 的
4//! `FetchValidBrokerList`。从 2024 起 C++ 用这个命令取代了老的 9419
5//! `kCmdFetchMainBroker`,作为 broker 通道**有效性判定**的权威源。
6//!
7//! ## 我们的用法(v1.4.22)
8//!
9//! Platform TCP login 成功后发一次 CMD 20176,返回的 `broker_ids` 用于
10//! 决定本轮应建立哪些 broker 通道;和 HTTP auth 拿到的 `auth_code_list`
11//! 里的 broker_id 集合对比,只是为了把差异打进日志。
12//!
13//! - 一致 → 正常建通道
14//! - 不一致 → 打 WARN,CMD20176 成功时以 CMD20176 为准
15//! - CMD 20176 失败(网络 / 服务端不支持)→ 不阻塞,调用方回退到
16//!   auth_code_list 旧路径
17//!
18//! C++ 10.6 `FetchValidBrokerList` 成功后直接
19//! `CreateBrokerChannel(cid_valid_broker_list)`;失败才使用本地缓存 /
20//! 全 broker fallback。本模块按同一方向收敛:成功响应是权威过滤源,
21//! 失败响应不改变旧行为。
22
23use 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
44/// Decode CMD20177 before scheduling any broker-list refresh.
45///
46/// C++ `FTlogin/Src/ftlogin/login/logger.cpp:1959-1984` rejects malformed
47/// `CidStatusChangePush`; its sole `reserved` field is intentionally not used.
48pub 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
53/// 向 Platform 通道发 CMD 20176,返回服务端认为该 cid 当前有效的 broker_id
54/// 列表。失败返回 `Err`(调用方通常 log + ignore)。
55pub 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
111/// 把 CMD 20176 返回的 broker_ids 和 HTTP auth 返回的 auth_code_list
112/// 做一致性 diff,不一致时打 WARN,返回 C++ 对齐的权威 broker_id 集。
113///
114/// 调用方只应在 CMD20176 成功时调用本函数;CMD20176 失败时沿用旧
115/// auth_code_list fallback。
116pub 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;