futu_core/conn_ip/
address_plan.rs1use std::collections::BTreeMap;
2use std::time::Duration;
3
4pub const CHANNEL_CONCURRENT_ADDRESS_LIMIT: usize = 4;
7pub const CPP_CONCURRENCY_STAGE_DELAY: Duration = Duration::from_secs(3);
9pub const CPP_GUARANTEED_STAGE_DELAY: Duration = Duration::from_secs(1);
11pub const CPP_PLATFORM_HARDCODED_PRIMARY_PORT: u16 = 443;
16pub const CPP_PLATFORM_HARDCODED_BACKUP_PORT: u16 = 9595;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ChannelAddressCandidate {
24 pub endpoint: String,
25 pub region: u32,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ChannelAddressPlanFacts {
30 pub server_candidates: Vec<ChannelAddressCandidate>,
31 pub concurrency_fallback: Vec<String>,
32 pub guaranteed_candidates: Vec<String>,
33 pub anti_ddos: Option<String>,
34 pub emergency_fallback: Option<String>,
35 pub forced: Option<String>,
36 pub rotation: usize,
37 pub backup_endpoints: BTreeMap<String, String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ChannelAddressPlan {
42 pub forced: Option<String>,
43 pub concurrency: Vec<String>,
44 pub guaranteed: Option<String>,
45 pub anti_ddos: Option<String>,
46 pub emergency: Option<String>,
47 pub backup_endpoints: BTreeMap<String, String>,
48}
49
50impl ChannelAddressPlan {
51 #[must_use]
52 pub fn backup_endpoint_for(&self, primary: &str) -> Option<&str> {
53 self.backup_endpoints.get(primary).map(String::as_str)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ChannelAddressAttempt {
59 pub endpoint: String,
60 pub start_after: Duration,
61}
62
63#[must_use]
72pub fn plan_channel_addresses_like_cpp(facts: ChannelAddressPlanFacts) -> ChannelAddressPlan {
73 if let Some(forced) = facts.forced {
74 return ChannelAddressPlan {
75 forced: Some(forced),
76 concurrency: Vec::new(),
77 guaranteed: None,
78 anti_ddos: None,
79 emergency: None,
80 backup_endpoints: BTreeMap::new(),
81 };
82 }
83
84 let concurrency = if facts.server_candidates.is_empty() {
85 rotated_limit(facts.concurrency_fallback, facts.rotation)
86 } else {
87 server_concurrency(facts.server_candidates, facts.rotation)
88 };
89 let guaranteed = rotated_one(facts.guaranteed_candidates, facts.rotation);
90 let anti_ddos = facts.anti_ddos;
91 let emergency = (concurrency.is_empty() && guaranteed.is_none() && anti_ddos.is_none())
92 .then_some(facts.emergency_fallback)
93 .flatten();
94 let mut backup_endpoints = facts.backup_endpoints;
95 backup_endpoints.retain(|primary, _| {
96 concurrency.iter().any(|endpoint| endpoint == primary)
97 || guaranteed.as_ref() == Some(primary)
98 || anti_ddos.as_ref() == Some(primary)
99 || emergency.as_ref() == Some(primary)
100 });
101
102 ChannelAddressPlan {
103 forced: None,
104 concurrency,
105 guaranteed,
106 anti_ddos,
107 emergency,
108 backup_endpoints,
109 }
110}
111
112#[must_use]
120pub fn channel_address_attempt_schedule_like_cpp(
121 plan: &ChannelAddressPlan,
122) -> Vec<ChannelAddressAttempt> {
123 if let Some(forced) = plan.forced.as_ref() {
124 return vec![attempt(forced, Duration::from_secs(0))];
125 }
126
127 let mut attempts: Vec<_> = plan
128 .concurrency
129 .iter()
130 .map(|endpoint| attempt(endpoint, Duration::from_secs(0)))
131 .collect();
132 let mut next_stage_at = if attempts.is_empty() {
133 Duration::from_secs(0)
134 } else {
135 CPP_CONCURRENCY_STAGE_DELAY
136 };
137 if let Some(guaranteed) = plan.guaranteed.as_ref() {
138 attempts.push(attempt(guaranteed, next_stage_at));
139 next_stage_at += CPP_GUARANTEED_STAGE_DELAY;
140 }
141 if let Some(anti_ddos) = plan.anti_ddos.as_ref() {
142 attempts.push(attempt(anti_ddos, next_stage_at));
143 }
144 if attempts.is_empty()
145 && let Some(emergency) = plan.emergency.as_ref()
146 {
147 attempts.push(attempt(emergency, Duration::from_secs(0)));
148 }
149 attempts
150}
151
152fn attempt(endpoint: &str, start_after: Duration) -> ChannelAddressAttempt {
153 ChannelAddressAttempt {
154 endpoint: endpoint.to_string(),
155 start_after,
156 }
157}
158
159fn server_concurrency(candidates: Vec<ChannelAddressCandidate>, rotation: usize) -> Vec<String> {
160 let mut by_region: BTreeMap<u32, Vec<String>> = BTreeMap::new();
161 for candidate in candidates {
162 by_region
163 .entry(candidate.region)
164 .or_default()
165 .push(candidate.endpoint);
166 }
167
168 let mut selected: Vec<_> = by_region
169 .into_values()
170 .map(|addresses| addresses[rotation % addresses.len()].clone())
171 .collect();
172 if !selected.is_empty() {
173 let offset = rotation % selected.len();
174 selected.rotate_left(offset);
175 }
176 selected.truncate(CHANNEL_CONCURRENT_ADDRESS_LIMIT);
177 selected
178}
179
180fn rotated_limit(mut addresses: Vec<String>, rotation: usize) -> Vec<String> {
181 if !addresses.is_empty() {
182 let offset = rotation % addresses.len();
183 addresses.rotate_left(offset);
184 }
185 addresses.truncate(CHANNEL_CONCURRENT_ADDRESS_LIMIT);
186 addresses
187}
188
189fn rotated_one(addresses: Vec<String>, rotation: usize) -> Option<String> {
190 (!addresses.is_empty()).then(|| addresses[rotation % addresses.len()].clone())
191}