Skip to main content

load_balancer/
proxy_pool.rs

1use crate::{
2    LoadBalancer,
3    round_robin::{Entry, Inner, RoundRobin},
4};
5use reqwest::Proxy;
6use std::{
7    ops::Range,
8    sync::{Arc, atomic::Ordering},
9    time::Duration,
10};
11use tokio::{
12    spawn,
13    sync::Semaphore,
14    task::JoinHandle,
15    time::{Instant, sleep},
16};
17
18/// An advanced proxy pool that measures latency, removes dead proxies,
19/// and sorts proxies by response time in ascending order.
20#[derive(Clone)]
21pub struct ProxyPool {
22    code_range: Range<u16>,
23    test_url: String,
24    timeout: Duration,
25    proxy: Option<Proxy>,
26    max_check_concurrency: usize,
27    lb: RoundRobin<Arc<str>>,
28}
29
30impl ProxyPool {
31    /// Create a new `ProxyPool` from a list of proxy URLs.
32    pub fn new<T: IntoIterator<Item = impl AsRef<str>>>(url: T) -> Self {
33        Self {
34            code_range: (200..300),
35            test_url: "https://apple.com".to_string(),
36            timeout: Duration::from_secs(5),
37            proxy: None,
38            max_check_concurrency: 1000,
39            lb: RoundRobin::new(url.into_iter().map(|v| v.as_ref().into()).collect()),
40        }
41    }
42
43    /// Set the range of HTTP status codes that are considered successful.
44    pub fn code_range(mut self, code_range: Range<u16>) -> Self {
45        self.code_range = code_range;
46        self
47    }
48
49    /// Set the URL used for testing proxy connectivity.
50    pub fn test_url(mut self, test_url: String) -> Self {
51        self.test_url = test_url;
52        self
53    }
54
55    /// Set the request timeout for proxy testing.
56    pub fn timeout(mut self, timeout: Duration) -> Self {
57        self.timeout = timeout;
58        self
59    }
60
61    /// Set an optional upstream proxy for proxy validation.
62    pub fn proxy(mut self, proxy: Proxy) -> Self {
63        self.proxy = Some(proxy);
64        self
65    }
66
67    /// Set the maximum number of concurrent proxy checks during health validation.
68    pub fn max_check_concurrency(mut self, max_check_concurrency: usize) -> Self {
69        self.max_check_concurrency = max_check_concurrency;
70        self
71    }
72
73    /// Get the number of currently available (healthy) proxies.
74    pub async fn available_count(&self) -> usize {
75        self.lb
76            .update(async |v| Ok(v.entries.read().await.len()))
77            .await
78            .unwrap()
79    }
80
81    /// Get available proxies.
82    pub async fn available(&self) -> Vec<String> {
83        self.lb
84            .update(async |v| {
85                Ok(v.entries
86                    .read()
87                    .await
88                    .iter()
89                    .map(|v| v.value.to_string())
90                    .collect::<Vec<_>>())
91            })
92            .await
93            .unwrap()
94    }
95
96    /// Add new proxies to the pool without performing immediate validation.
97    ///
98    /// New entries are appended, the cursor is reset, and the available count is updated.
99    /// Validation occurs on the next `check()` call.
100    pub async fn extend<T: IntoIterator<Item = impl AsRef<str>>>(&self, urls: T) {
101        let new_entries = urls
102            .into_iter()
103            .map(|v| Entry {
104                value: Arc::from(v.as_ref()),
105            })
106            .collect::<Vec<_>>();
107
108        self.lb
109            .update(async |v| {
110                let mut lock = v.entries.write().await;
111
112                lock.extend(new_entries.clone());
113                v.cursor.store(0, Ordering::Relaxed);
114
115                Ok(())
116            })
117            .await
118            .unwrap();
119    }
120
121    /// Add new proxies and immediately perform connectivity and latency checks.
122    ///
123    /// Proxies are validated, failed ones are removed, and remaining entries
124    /// are sorted by latency (ascending).
125    pub async fn extend_check<T: IntoIterator<Item = impl AsRef<str>>>(
126        &self,
127        url: T,
128        retry_count: usize,
129    ) -> anyhow::Result<()> {
130        let new_entries = url
131            .into_iter()
132            .map(|v| Entry {
133                value: Arc::from(v.as_ref()),
134            })
135            .collect::<Vec<Entry<Arc<str>>>>();
136
137        self.lb
138            .update(async |v| {
139                let old_entries = {
140                    let lock = v.entries.read().await;
141                    let mut result = Vec::with_capacity(lock.len() + new_entries.len());
142
143                    result.extend_from_slice(&new_entries);
144                    result.extend(lock.iter().cloned());
145
146                    result
147                };
148
149                let result = self.internal_check(&old_entries, retry_count).await?;
150
151                let mut new_entries = Vec::with_capacity(result.len());
152
153                for (index, _) in result {
154                    new_entries.push(old_entries[index].clone());
155                }
156
157                let mut lock = v.entries.write().await;
158
159                *lock = new_entries;
160                v.cursor.store(0, Ordering::Relaxed);
161
162                Ok(())
163            })
164            .await
165    }
166
167    /// Validate all proxies, remove dead ones, and sort by latency.
168    pub async fn check(&self, retry_count: usize) -> anyhow::Result<()> {
169        self.lb
170            .update(async |v| {
171                let old_entries = v.entries.read().await;
172
173                let result = self.internal_check(&old_entries, retry_count).await?;
174
175                let mut new_entries = Vec::with_capacity(result.len());
176
177                for (index, _) in result {
178                    new_entries.push(old_entries[index].clone());
179                }
180
181                drop(old_entries);
182
183                let mut lock = v.entries.write().await;
184
185                *lock = new_entries;
186                v.cursor.store(0, Ordering::Relaxed);
187
188                Ok(())
189            })
190            .await
191    }
192
193    /// Spawn a background task to periodically validate proxies and update order by latency.
194    ///
195    /// Returns a `JoinHandle` to allow cancellation or awaiting of the task.
196    pub async fn spawn_check(
197        &self,
198        check_interval: Duration,
199        retry_count: usize,
200    ) -> anyhow::Result<JoinHandle<()>> {
201        self.check(retry_count).await?;
202
203        let this = self.clone();
204
205        Ok(spawn(async move {
206            loop {
207                sleep(check_interval).await;
208                _ = this.check(retry_count).await;
209            }
210        }))
211    }
212
213    /// Spawn a background task that periodically checks proxies and invokes a callback.
214    ///
215    /// Like [`spawn_check`](Self::spawn_check), but calls `callback` after every
216    /// check cycle completes. The callback runs synchronously within the loop —
217    /// use it to log, notify, or update external state.
218    pub async fn spawn_check_callback<F, R>(
219        &self,
220        check_interval: Duration,
221        retry_count: usize,
222        callback: F,
223    ) -> anyhow::Result<JoinHandle<anyhow::Result<()>>>
224    where
225        F: Fn() -> R + Send + 'static,
226        R: Future<Output = anyhow::Result<()>> + Send,
227    {
228        self.check(retry_count).await?;
229        callback().await?;
230
231        let this = self.clone();
232
233        Ok(spawn(async move {
234            loop {
235                sleep(check_interval).await;
236                _ = this.check(retry_count).await;
237                callback().await?;
238            }
239        }))
240    }
241
242    /// Update the load balancer using a custom async handler.
243    pub async fn update<F, R>(&self, handler: F) -> anyhow::Result<()>
244    where
245        F: Fn(Arc<Inner<Arc<str>>>) -> R,
246        R: Future<Output = anyhow::Result<()>>,
247    {
248        self.lb.update(handler).await
249    }
250
251    /// Spawn a background task that calls `handler` every `interval`.
252    ///
253    /// The handler is called once immediately; if that initial call fails
254    /// the error is returned and no background task is spawned.
255    pub async fn update_timer<F, R>(
256        &self,
257        handler: F,
258        interval: Duration,
259    ) -> anyhow::Result<JoinHandle<()>>
260    where
261        F: Fn(Arc<Inner<Arc<str>>>) -> R + Send + Sync + 'static,
262        R: Future<Output = anyhow::Result<()>> + Send,
263    {
264        self.lb.update_timer(handler, interval).await
265    }
266
267    async fn internal_check(
268        &self,
269        entries: &Vec<Entry<Arc<str>>>,
270        retry_count: usize,
271    ) -> anyhow::Result<Vec<(usize, u128)>> {
272        let semaphore = Arc::new(Semaphore::new(self.max_check_concurrency));
273        let mut task = Vec::with_capacity(entries.len());
274
275        for (index, entry) in entries.iter().enumerate() {
276            let permit = semaphore.clone().acquire_owned().await.unwrap();
277            let entry = entry.clone();
278            let code_range = self.code_range.clone();
279            let test_url = self.test_url.clone();
280            let timeout = self.timeout;
281            let upstream_proxy = self.proxy.clone();
282            let entry_value = entry.value.clone();
283
284            task.push(tokio::spawn(async move {
285                let _permit = permit;
286                let mut latency = None;
287
288                for _ in 0..=retry_count {
289                    let client = if let Some(proxy) = upstream_proxy.clone() {
290                        reqwest::ClientBuilder::new()
291                            .proxy(proxy)
292                            .proxy(Proxy::all(&*entry_value)?)
293                            .timeout(timeout)
294                            .build()?
295                    } else {
296                        reqwest::ClientBuilder::new()
297                            .proxy(Proxy::all(&*entry_value)?)
298                            .timeout(timeout)
299                            .build()?
300                    };
301
302                    let start = Instant::now();
303
304                    if let Ok(v) = client.get(&test_url).send().await {
305                        if code_range.contains(&v.status().as_u16()) {
306                            latency = Some(start.elapsed().as_millis());
307                            break;
308                        }
309                    }
310                }
311
312                anyhow::Ok(latency.map(|v| (index, v)))
313            }));
314        }
315
316        let mut result = Vec::new();
317
318        for i in task {
319            if let Ok(Ok(Some(r))) = i.await {
320                result.push(r);
321            }
322        }
323
324        result.sort_by_key(|(_, latency)| *latency);
325
326        Ok(result)
327    }
328}
329
330impl LoadBalancer<String> for ProxyPool {
331    async fn alloc(&self) -> String {
332        LoadBalancer::alloc(&self.lb).await.to_string()
333    }
334
335    fn try_alloc(&self) -> Option<String> {
336        LoadBalancer::try_alloc(&self.lb).map(|v| v.to_string())
337    }
338}