Skip to main content

ecat_data/
routing.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2use std::sync::Arc;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::time::Duration;
5
6use async_trait::async_trait;
7use ecat_circuit_breaker::{Breaker, BreakerConfig, BreakerState};
8
9use crate::breaker::map_breaker_error;
10use crate::dialect::Dialect;
11use crate::rdbms::{RdbmsClient, RdbmsError, Row, SqlExecutor, Transaction};
12
13/// 一个端点:客户端 + **它自己的**熔断器。
14///
15/// 熔断器逐端点各一个 —— 不是包在路由外层:那样单个从库故障会误熔断整条链路
16/// (spec:225)。经由端点的每次调用都过它自己的熔断器(`transaction()` 除外,
17/// 见 [`RdbmsRouting`] 的 `RdbmsClient` 实现)。
18struct Endpoint {
19    client: Arc<dyn RdbmsClient>,
20    breaker: Breaker,
21}
22
23impl Endpoint {
24    fn new(client: Arc<dyn RdbmsClient>, cfg: &BreakerConfig) -> Self {
25        Self {
26            client,
27            breaker: Breaker::new(cfg.clone()),
28        }
29    }
30
31    /// 熔断打开 ⇒ 不参与轮询。这是「跳过已熔断端点」的唯一依据(spec:235-236)。
32    /// 配了延迟阈值时再按探针实测值过滤;探针无数据(`None`)视为可用。
33    async fn is_available(&self, max_lag: Option<Duration>) -> bool {
34        if self.breaker.state() == BreakerState::Open {
35            return false;
36        }
37        match max_lag {
38            Some(max) => self.client.replica_lag().await.is_none_or(|lag| lag <= max),
39            None => true,
40        }
41    }
42
43    fn dialect(&self) -> Dialect {
44        self.client.dialect()
45    }
46
47    async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
48        self.breaker
49            .call(|| self.client.execute(sql))
50            .await
51            .map_err(map_breaker_error)
52    }
53
54    async fn execute_with(
55        &self,
56        sql: &str,
57        params: &[serde_json::Value],
58    ) -> Result<u64, RdbmsError> {
59        self.breaker
60            .call(|| self.client.execute_with(sql, params))
61            .await
62            .map_err(map_breaker_error)
63    }
64
65    async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
66        self.breaker
67            .call(|| self.client.query(sql))
68            .await
69            .map_err(map_breaker_error)
70    }
71
72    async fn query_with(
73        &self,
74        sql: &str,
75        params: &[serde_json::Value],
76    ) -> Result<Vec<Row>, RdbmsError> {
77        self.breaker
78            .call(|| self.client.query_with(sql, params))
79            .await
80            .map_err(map_breaker_error)
81    }
82
83    async fn query_write(
84        &self,
85        sql: &str,
86        params: &[serde_json::Value],
87    ) -> Result<Vec<Row>, RdbmsError> {
88        self.breaker
89            .call(|| self.client.query_write(sql, params))
90            .await
91            .map_err(map_breaker_error)
92    }
93
94    async fn execute_then_query(
95        &self,
96        first: &str,
97        first_params: &[serde_json::Value],
98        second: &str,
99    ) -> Result<Vec<Row>, RdbmsError> {
100        self.breaker
101            .call(|| self.client.execute_then_query(first, first_params, second))
102            .await
103            .map_err(map_breaker_error)
104    }
105}
106
107/// 读写分离路由:写落主库、读落副本轮询,**跳过熔断打开(`Open`)的副本**。
108///
109/// 只给每个端点包一层熔断是不够的:从库挂掉后轮询仍会把 1/N 的读转过去、
110/// 靠熔断快速失败 —— 那不是故障隔离,是**稳定的 1/N 失败率**(spec:235-236)。
111pub struct RdbmsRouting {
112    primary: Endpoint,
113    replicas: Vec<Endpoint>,
114    /// 轮询起点;`fetch_add` 即可,无需锁。
115    next: AtomicUsize,
116    /// 副本全不可用时是否降级读主(默认 `true`)。
117    fallback_to_primary: bool,
118    /// 副本实测延迟超过它即跳过;`None` = 不探测(默认,保持原行为)。
119    max_replica_lag: Option<Duration>,
120}
121
122impl RdbmsRouting {
123    /// 每个端点一个熔断器,配置取 [`BreakerConfig::default`]。
124    pub fn new(primary: Arc<dyn RdbmsClient>, replicas: Vec<Arc<dyn RdbmsClient>>) -> Self {
125        Self::with_breaker_config(primary, replicas, BreakerConfig::default())
126    }
127
128    /// 显式指定各端点的熔断配置(冷却期、失败率阈值、窗口)。
129    pub fn with_breaker_config(
130        primary: Arc<dyn RdbmsClient>,
131        replicas: Vec<Arc<dyn RdbmsClient>>,
132        cfg: BreakerConfig,
133    ) -> Self {
134        Self {
135            primary: Endpoint::new(primary, &cfg),
136            replicas: replicas
137                .into_iter()
138                .map(|client| Endpoint::new(client, &cfg))
139                .collect(),
140            next: AtomicUsize::new(0),
141            fallback_to_primary: true,
142            max_replica_lag: None,
143        }
144    }
145
146    /// 副本全不可用时是否降级读主。默认 `true`;`false` 时读请求报
147    /// [`RdbmsError::NoAvailableReplica`]。
148    pub fn fallback_to_primary(mut self, yes: bool) -> Self {
149        self.fallback_to_primary = yes;
150        self
151    }
152
153    /// 跳过实测延迟超过 `lag` 的副本;延迟由 [`RdbmsClient::replica_lag`]
154    /// 探测,探不出结果时不跳过。默认不探测。
155    pub fn max_replica_lag(mut self, lag: Duration) -> Self {
156        self.max_replica_lag = Some(lag);
157        self
158    }
159
160    /// 从 `next` 开始轮询,跳过 `Open` 的副本,返回第一个可用的。
161    ///
162    /// `None` = 一个可用副本都没有(含「没配副本」);降级还是报错由调用方决定。
163    ///
164    /// ⚠️ 已知边界:`Open → HalfOpen` 的冷却转换发生在 `Breaker::call` 里,而这里
165    /// **跳过**了 `Open` 的端点 ⇒ 没人再去调它,冷却期过后也不会被重新放行,
166    /// 副本恢复后仍被永久排除。修法需要 breaker 侧暴露「冷却期是否已过」。
167    async fn pick_replica(&self) -> Option<&Endpoint> {
168        let n = self.replicas.len();
169        if n == 0 {
170            return None;
171        }
172        let start = self.next.fetch_add(1, Ordering::Relaxed);
173        for i in 0..n {
174            let ep = &self.replicas[(start + i) % n];
175            if ep.is_available(self.max_replica_lag).await {
176                return Some(ep);
177            }
178        }
179        None
180    }
181}
182
183#[async_trait]
184impl SqlExecutor for RdbmsRouting {
185    /// 写 → 主库。
186    async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
187        self.primary.execute(sql).await
188    }
189
190    /// 读 → 副本轮询;全不可用时按 [`RdbmsRouting::fallback_to_primary`] 处置。
191    async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
192        match self.pick_replica().await {
193            Some(replica) => replica.query(sql).await,
194            None if self.fallback_to_primary => self.primary.query(sql).await,
195            None => Err(RdbmsError::NoAvailableReplica),
196        }
197    }
198
199    /// 写 → 主库。
200    async fn execute_with(
201        &self,
202        sql: &str,
203        params: &[serde_json::Value],
204    ) -> Result<u64, RdbmsError> {
205        self.primary.execute_with(sql, params).await
206    }
207
208    /// 读 → 副本轮询;全不可用时按 [`RdbmsRouting::fallback_to_primary`] 处置。
209    async fn query_with(
210        &self,
211        sql: &str,
212        params: &[serde_json::Value],
213    ) -> Result<Vec<Row>, RdbmsError> {
214        match self.pick_replica().await {
215            Some(replica) => replica.query_with(sql, params).await,
216            None if self.fallback_to_primary => self.primary.query_with(sql, params).await,
217            None => Err(RdbmsError::NoAvailableReplica),
218        }
219    }
220
221    /// 写路径的返回行查询(`INSERT ... RETURNING`)→ 主库:
222    /// 落副本会读到**陈旧数据**。
223    async fn query_write(
224        &self,
225        sql: &str,
226        params: &[serde_json::Value],
227    ) -> Result<Vec<Row>, RdbmsError> {
228        self.primary.query_write(sql, params).await
229    }
230
231    /// 两步式插入(MySQL 的 `LAST_INSERT_ID()`)也是写路径 → 主库。
232    async fn execute_then_query(
233        &self,
234        first: &str,
235        first_params: &[serde_json::Value],
236        second: &str,
237    ) -> Result<Vec<Row>, RdbmsError> {
238        self.primary
239            .execute_then_query(first, first_params, second)
240            .await
241    }
242
243    /// 取主库的方言:它是路由对外承诺的写方言。
244    fn dialect(&self) -> Dialect {
245        self.primary.dialect()
246    }
247}
248
249#[async_trait]
250impl RdbmsClient for RdbmsRouting {
251    /// 事务天然读主(规避副本延迟),且**不经熔断**:事务失败由 SQL 层报错,
252    /// 熔断器在这里只会把「主库慢」升级成「主库不可用」(spec §2.5 组合顺序)。
253    async fn transaction(&self) -> Result<Transaction, RdbmsError> {
254        self.primary.client.transaction().await
255    }
256}
257
258/// 读己之写的显式句柄:读写与事务**全部**落主库,不参与副本轮询。
259///
260/// 只读副本天然有复制延迟,写完立刻读会读到旧值;需要「读己之写」的场景
261/// 用 [`RdbmsRouting::primary_only`] 显式声明,而不是把整条链路改成读主。
262pub struct PrimaryOnly<'a> {
263    router: &'a RdbmsRouting,
264}
265
266impl RdbmsRouting {
267    /// 返回读己之写句柄。它只借用路由、不改变路由状态,
268    /// 副本轮询与各自熔断器的既有行为不受影响。
269    pub fn primary_only(&self) -> PrimaryOnly<'_> {
270        PrimaryOnly { router: self }
271    }
272}
273
274#[async_trait]
275impl SqlExecutor for PrimaryOnly<'_> {
276    async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
277        self.router.primary.execute(sql).await
278    }
279
280    async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
281        self.router.primary.query(sql).await
282    }
283
284    async fn execute_with(
285        &self,
286        sql: &str,
287        params: &[serde_json::Value],
288    ) -> Result<u64, RdbmsError> {
289        self.router.primary.execute_with(sql, params).await
290    }
291
292    async fn query_with(
293        &self,
294        sql: &str,
295        params: &[serde_json::Value],
296    ) -> Result<Vec<Row>, RdbmsError> {
297        self.router.primary.query_with(sql, params).await
298    }
299
300    async fn query_write(
301        &self,
302        sql: &str,
303        params: &[serde_json::Value],
304    ) -> Result<Vec<Row>, RdbmsError> {
305        self.router.primary.query_write(sql, params).await
306    }
307
308    async fn execute_then_query(
309        &self,
310        first: &str,
311        first_params: &[serde_json::Value],
312        second: &str,
313    ) -> Result<Vec<Row>, RdbmsError> {
314        self.router
315            .primary
316            .execute_then_query(first, first_params, second)
317            .await
318    }
319
320    fn dialect(&self) -> Dialect {
321        self.router.primary.dialect()
322    }
323}
324
325#[async_trait]
326impl RdbmsClient for PrimaryOnly<'_> {
327    /// 与路由一致:事务走主库客户端,不经熔断。
328    async fn transaction(&self) -> Result<Transaction, RdbmsError> {
329        self.router.primary.client.transaction().await
330    }
331}
332
333#[cfg(test)]
334mod tests;