longbridge_geo/lib.rs
1//! Geo-detection helper for Longbridge OpenAPI.
2//!
3//! Determines whether the current access point is in China Mainland so that
4//! callers can choose between `*.longbridge.cn` and `*.longbridge.com`
5//! endpoints.
6
7use std::{
8 sync::{
9 OnceLock,
10 atomic::{AtomicBool, Ordering},
11 },
12 time::Duration,
13};
14
15// Process-wide cache so the probe is done at most once regardless of which
16// tokio worker thread calls `is_cn()`.
17static IS_CN_DONE: OnceLock<bool> = OnceLock::new();
18
19// Used to prevent multiple concurrent probes racing at startup.
20static IS_CN_PROBING: AtomicBool = AtomicBool::new(false);
21
22/// Do the best to guess whether the access point is in China Mainland or not.
23///
24/// Detection priority:
25/// 1. `LONGBRIDGE_REGION` environment variable (takes precedence).
26/// 2. `LONGPORT_REGION` environment variable (fallback alias).
27/// 3. Process-wide cached result from a previous probe.
28/// 4. Live HTTP probe to `https://geotest.lbkrs.com` — HTTP 200 → CN, anything
29/// else (error or non-200) → not CN.
30pub async fn is_cn() -> bool {
31 // 1 & 2: explicit region override
32 let user_region = std::env::var("LONGBRIDGE_REGION")
33 .ok()
34 .or_else(|| std::env::var("LONGPORT_REGION").ok());
35 if let Some(region) = user_region {
36 return region.eq_ignore_ascii_case("CN");
37 }
38
39 // 3: already probed
40 if let Some(&cached) = IS_CN_DONE.get() {
41 return cached;
42 }
43
44 // 4: live probe — only one task does the actual probe; others fall back
45 // to `false` (global endpoint) which is safe and avoids a pile-up.
46 if IS_CN_PROBING
47 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
48 .is_ok()
49 {
50 let result = reqwest::Client::new()
51 .get("https://geotest.lbkrs.com")
52 .timeout(Duration::from_secs(5))
53 .send()
54 .await
55 .is_ok_and(|resp| resp.status().is_success());
56
57 let _ = IS_CN_DONE.set(result);
58 result
59 } else {
60 // Another task is probing; use the cached value if it finished in the
61 // meantime, otherwise default to global endpoint.
62 IS_CN_DONE.get().copied().unwrap_or(false)
63 }
64}