Skip to main content

ecat_data/
breaker.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2use async_trait::async_trait;
3use ecat_circuit_breaker::{Breaker, BreakerConfig, BreakerError, BreakerState};
4use ecat_errors::{Error, ErrorCode};
5
6use crate::dialect::Dialect;
7use crate::rdbms::{RdbmsClient, RdbmsError, Row, SqlExecutor, Transaction};
8
9/// 给任意 [`SqlExecutor`] 包一层熔断。
10///
11/// **每个端点各包一个** —— 不是包在路由外层:那样单个从库故障会误熔断整条链路
12/// (spec:225)。熔断打开时**内层一次都不会被调用**。
13pub struct CircuitBreakerExecutor<S> {
14    inner: S,
15    breaker: Breaker,
16}
17
18impl<S> CircuitBreakerExecutor<S> {
19    pub fn new(inner: S, cfg: BreakerConfig) -> Self {
20        Self {
21            inner,
22            breaker: Breaker::new(cfg),
23        }
24    }
25
26    /// 供 `RdbmsRouting` 跳过已熔断的端点。
27    pub fn state(&self) -> BreakerState {
28        self.breaker.state()
29    }
30
31    pub fn inner(&self) -> &S {
32        &self.inner
33    }
34}
35
36/// 熔断器错误的映射:后端自身的错误**原样透出** —— 熔断只决定「调不调用」,
37/// 不改写后端报错。熔断打开/探测耗尽时后端根本没被调用,报「连接不可用」。
38/// (`RdbmsRouting` 的端点与 Redis / ClickHouse 的包装层复用同一套映射,故公开。)
39pub fn map_breaker_error(e: BreakerError<RdbmsError>) -> RdbmsError {
40    match e {
41        BreakerError::Inner(inner) => inner,
42        other => RdbmsError::Connection(other.to_string()),
43    }
44}
45
46/// `ecat_errors::Error` 侧的同一套映射(六个非 RDBMS trait 用的错误类型)。
47///
48/// 与 [`map_breaker_error`] 分开是因为 `ecat_errors::Error` 与 `BreakerError`
49/// **都是外部类型**,写不出统一的 `From` 实现(孤儿规则),只能各来一个函数。
50/// `reason` 是后端标识(如 `"redis"`),进 `Error::reason`。
51pub fn breaker_error_to_backend_error(e: BreakerError<Error>, reason: &'static str) -> Error {
52    match e {
53        BreakerError::Inner(inner) => inner,
54        other => Error::new(ErrorCode::Unavailable, reason, other.to_string()),
55    }
56}
57
58#[async_trait]
59impl<S: SqlExecutor> SqlExecutor for CircuitBreakerExecutor<S> {
60    async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
61        // 闭包**按需构造 future**:熔断打开时内层根本没被碰。
62        self.breaker
63            .call(|| self.inner.execute(sql))
64            .await
65            .map_err(map_breaker_error)
66    }
67
68    async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
69        self.breaker
70            .call(|| self.inner.query(sql))
71            .await
72            .map_err(map_breaker_error)
73    }
74
75    async fn execute_with(
76        &self,
77        sql: &str,
78        params: &[serde_json::Value],
79    ) -> Result<u64, RdbmsError> {
80        self.breaker
81            .call(|| self.inner.execute_with(sql, params))
82            .await
83            .map_err(map_breaker_error)
84    }
85
86    async fn query_with(
87        &self,
88        sql: &str,
89        params: &[serde_json::Value],
90    ) -> Result<Vec<Row>, RdbmsError> {
91        self.breaker
92            .call(|| self.inner.query_with(sql, params))
93            .await
94            .map_err(map_breaker_error)
95    }
96
97    /// 写路径的返回行查询:同样受熔断保护 —— 熔断打开的端点连写也不该碰。
98    async fn query_write(
99        &self,
100        sql: &str,
101        params: &[serde_json::Value],
102    ) -> Result<Vec<Row>, RdbmsError> {
103        self.breaker
104            .call(|| self.inner.query_write(sql, params))
105            .await
106            .map_err(map_breaker_error)
107    }
108
109    async fn execute_then_query(
110        &self,
111        first: &str,
112        first_params: &[serde_json::Value],
113        second: &str,
114    ) -> Result<Vec<Row>, RdbmsError> {
115        self.breaker
116            .call(|| self.inner.execute_then_query(first, first_params, second))
117            .await
118            .map_err(map_breaker_error)
119    }
120
121    /// 纯本地判断,不会失败,也不该被「熔断」影响 —— 直接委托。
122    fn dialect(&self) -> Dialect {
123        self.inner.dialect()
124    }
125}
126
127#[async_trait]
128impl<S: RdbmsClient> RdbmsClient for CircuitBreakerExecutor<S> {
129    /// 事务**不经熔断**,直接委托内层。
130    ///
131    /// 两件事要分清:
132    /// - **把熔断套到事务上** —— 要在 `TransactionInner` 层面再包一层,代价大于收益(不做)
133    /// - **把 `transaction()` 委托给内层** —— 就是这三行(要做):端点要能装进
134    ///   `Arc<dyn RdbmsClient>`,而事务天然走 primary,失败由 SQL 层报错。
135    async fn transaction(&self) -> Result<Transaction, RdbmsError> {
136        self.inner.transaction().await
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::dialect::Dialect;
144    use crate::rdbms::{RdbmsClient, RdbmsError, Row, SqlExecutor, Transaction};
145    use async_trait::async_trait;
146    use ecat_circuit_breaker::{BreakerConfig, BreakerError, BreakerState};
147    use ecat_errors::{Error, ErrorCode};
148    use std::sync::Arc;
149    use std::sync::atomic::{AtomicUsize, Ordering};
150
151    /// 假执行器:**每一次真正落到内层的调用**都记一笔;
152    /// `fail` 决定它报错还是成功。`dialect()` 是纯本地判断,不计入。
153    struct FakeExecutor {
154        calls: Arc<AtomicUsize>,
155        fail: bool,
156    }
157
158    impl FakeExecutor {
159        fn new(fail: bool) -> (Self, Arc<AtomicUsize>) {
160            let calls = Arc::new(AtomicUsize::new(0));
161            (
162                Self {
163                    calls: Arc::clone(&calls),
164                    fail,
165                },
166                calls,
167            )
168        }
169
170        fn record<T>(&self, ok: T) -> Result<T, RdbmsError> {
171            self.calls.fetch_add(1, Ordering::SeqCst);
172            if self.fail {
173                Err(RdbmsError::Connection("backend down".into()))
174            } else {
175                Ok(ok)
176            }
177        }
178    }
179
180    #[async_trait]
181    impl SqlExecutor for FakeExecutor {
182        async fn execute(&self, _sql: &str) -> Result<u64, RdbmsError> {
183            self.record(1)
184        }
185        async fn query(&self, _sql: &str) -> Result<Vec<Row>, RdbmsError> {
186            self.record(vec![])
187        }
188        async fn execute_with(
189            &self,
190            _sql: &str,
191            _params: &[serde_json::Value],
192        ) -> Result<u64, RdbmsError> {
193            self.record(1)
194        }
195        async fn query_with(
196            &self,
197            _sql: &str,
198            _params: &[serde_json::Value],
199        ) -> Result<Vec<Row>, RdbmsError> {
200            self.record(vec![])
201        }
202        async fn query_write(
203            &self,
204            _sql: &str,
205            _params: &[serde_json::Value],
206        ) -> Result<Vec<Row>, RdbmsError> {
207            self.record(vec![])
208        }
209        async fn execute_then_query(
210            &self,
211            _first: &str,
212            _first_params: &[serde_json::Value],
213            _second: &str,
214        ) -> Result<Vec<Row>, RdbmsError> {
215            self.record(vec![])
216        }
217        fn dialect(&self) -> Dialect {
218            Dialect::Sqlite
219        }
220    }
221
222    #[async_trait]
223    impl RdbmsClient for FakeExecutor {
224        async fn transaction(&self) -> Result<Transaction, RdbmsError> {
225            self.record(Transaction::new())
226        }
227    }
228
229    /// `transaction()` **委托内层、不经熔断** —— 委托 ≠ 熔断。
230    ///
231    /// 端点要能装进 `Arc<dyn RdbmsClient>`,所以这个委托必须在;
232    /// 而把熔断套到事务上要在 `TransactionInner` 层面再包一层(代价大于收益,不做)。
233    #[tokio::test]
234    async fn transaction_delegates_without_going_through_the_breaker() {
235        let (inner, calls) = FakeExecutor::new(true);
236        let exec = CircuitBreakerExecutor::new(inner, BreakerConfig::default());
237
238        assert!(exec.transaction().await.is_err());
239        assert_eq!(calls.load(Ordering::SeqCst), 1, "必须委托给内层");
240        assert_eq!(exec.state(), BreakerState::Closed);
241
242        // 累计 6 次事务失败:若 transaction() 走了熔断,状态早该 Open
243        for _ in 0..5 {
244            assert!(exec.transaction().await.is_err());
245        }
246        assert_eq!(calls.load(Ordering::SeqCst), 6);
247        assert_eq!(
248            exec.state(),
249            BreakerState::Closed,
250            "事务失败不得计入熔断窗口(熔断器根本没被碰)"
251        );
252    }
253
254    /// 熔断打开后,内层**一次都不该被调用** —— 这才叫熔断,
255    /// 否则只是「快速失败的转发」。
256    #[tokio::test]
257    async fn open_breaker_never_reaches_the_inner_executor() {
258        let (inner, calls) = FakeExecutor::new(true);
259        let exec = CircuitBreakerExecutor::new(inner, BreakerConfig::default());
260
261        // 5 次失败 → 窗口总数 5、失败率 1.0 ≥ 0.5 → Open
262        for _ in 0..5 {
263            assert!(exec.query("SELECT 1").await.is_err());
264        }
265        assert_eq!(exec.state(), BreakerState::Open);
266
267        // 清零:接下来六个方法只要有一次落到内层,计数就不再是 0
268        calls.store(0, Ordering::SeqCst);
269        assert!(exec.execute("UPDATE t SET x = 1").await.is_err());
270        assert!(exec.query("SELECT 1").await.is_err());
271        assert!(exec.execute_with("UPDATE t SET x = ?", &[]).await.is_err());
272        assert!(exec.query_with("SELECT ?", &[]).await.is_err());
273        assert!(
274            exec.query_write("INSERT INTO t (v) VALUES (?) RETURNING id", &[])
275                .await
276                .is_err()
277        );
278        let err = exec
279            .execute_then_query(
280                "INSERT INTO t (v) VALUES (?)",
281                &[],
282                "SELECT LAST_INSERT_ID()",
283            )
284            .await
285            .unwrap_err();
286        // 熔断打开时后端根本没被调用,报「连接不可用」
287        assert!(err.to_string().contains("circuit breaker"), "got: {err}");
288
289        assert_eq!(
290            calls.load(Ordering::SeqCst),
291            0,
292            "熔断打开后内层不得被调用(否则只是快速失败的转发)"
293        );
294    }
295
296    /// 熔断只看「后端答不答」:`Ok` 记成功,后端报错记失败。
297    #[tokio::test]
298    async fn inner_errors_count_as_failures() {
299        let (inner, calls) = FakeExecutor::new(false);
300        let ok = CircuitBreakerExecutor::new(inner, BreakerConfig::default());
301        for _ in 0..6 {
302            ok.query_write("INSERT INTO t (v) VALUES (?) RETURNING id", &[])
303                .await
304                .unwrap();
305        }
306        assert_eq!(ok.state(), BreakerState::Closed, "成功不得触发熔断");
307        assert_eq!(calls.load(Ordering::SeqCst), 6);
308
309        let (inner, _) = FakeExecutor::new(true);
310        let bad = CircuitBreakerExecutor::new(inner, BreakerConfig::default());
311        for _ in 0..5 {
312            assert!(bad.execute_with("UPDATE t SET x = ?", &[]).await.is_err());
313        }
314        assert_eq!(bad.state(), BreakerState::Open, "后端报错必须计入失败");
315    }
316
317    /// 熔断状态可被外部读取(`RdbmsRouting` 靠它跳端点);
318    /// `dialect()` 不走熔断,熔断打开也照常返回。
319    #[tokio::test]
320    async fn breaker_state_is_readable() {
321        let (inner, _) = FakeExecutor::new(true);
322        let exec = CircuitBreakerExecutor::new(inner, BreakerConfig::default());
323        assert_eq!(exec.state(), BreakerState::Closed);
324
325        for _ in 0..5 {
326            assert!(exec.execute("UPDATE t SET x = 1").await.is_err());
327        }
328        assert_eq!(exec.state(), BreakerState::Open);
329        assert_eq!(exec.dialect(), Dialect::Sqlite);
330        assert_eq!(exec.inner().dialect(), Dialect::Sqlite);
331    }
332
333    /// `ecat_errors::Error` 侧的映射,三个分支各一条:`Inner` **原样透出**
334    /// (熔断只决定「调不调用」,不改写后端报错);`Open` / `ProbesExhausted`
335    /// 归成 `Unavailable` + 组件标识 —— 这两种情况内层根本没被调用,
336    /// 拿不到内层错误,也不该让调用方以为后端答了话。
337    #[test]
338    fn breaker_error_to_backend_error_maps_all_three_variants() {
339        let inner = Error::new(ErrorCode::DeadlineExceeded, "redis", "connect timed out");
340        let passthrough = breaker_error_to_backend_error(BreakerError::Inner(inner), "redis");
341        assert_eq!(passthrough.code, ErrorCode::DeadlineExceeded);
342        assert_eq!(passthrough.reason, "redis");
343        assert_eq!(passthrough.message, "connect timed out");
344
345        let open = breaker_error_to_backend_error(BreakerError::Open, "clickhouse");
346        assert_eq!(open.code, ErrorCode::Unavailable);
347        assert_eq!(open.reason, "clickhouse");
348        assert_eq!(open.message, "circuit breaker is open");
349
350        let probes = breaker_error_to_backend_error(BreakerError::ProbesExhausted, "clickhouse");
351        assert_eq!(probes.code, ErrorCode::Unavailable);
352        assert_eq!(probes.reason, "clickhouse");
353        assert_eq!(probes.message, "circuit breaker: too many probes");
354    }
355}