Skip to main content

dtmrs_server/
api.rs

1//! TC 的对外操作,**与协议无关**。
2//!
3//! HTTP 和 gRPC 两套接口都只做「协议 ↔ 这一层」的转换,业务判断全在这里。
4//! 分成两处写迟早会漂移 —— 而这一层漂移的后果是「同一个请求走 HTTP 被拒、
5//! 走 gRPC 却受理了」,这种不一致在事务系统里是要命的。
6//!
7//! 错误用 [`ApiError`] 表达,由各协议层翻译成自己的表示:
8//!
9//! | ApiError | HTTP | gRPC |
10//! |---|---|---|
11//! | `BadRequest` | 400 | `INVALID_ARGUMENT` |
12//! | `NotFound` | 404 | `NOT_FOUND` |
13//! | `Conflict` | 200 + `dtm_result=FAILURE` | `FAILED_PRECONDITION` |
14//! | `Internal` | 500 | `INTERNAL` |
15//!
16//! `Conflict` 在 HTTP 上返回 200 是**刻意保留的历史行为**(已终结的事务再调
17//! abort),换成 4xx 会打破现有客户端。
18
19use crate::{msg_rows, saga_rows, tcc_rows};
20use dtmrs_core::{BranchOp, GlobalStatus, SagaStep, TransType};
21use dtmrs_store::{Store, SubmitOutcome};
22use serde::Serialize;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ApiError {
26    BadRequest(String),
27    NotFound(String),
28    /// 请求本身合法,但当前状态下做不了
29    Conflict(String),
30    Internal(String),
31}
32
33impl ApiError {
34    pub fn message(&self) -> &str {
35        match self {
36            Self::BadRequest(m) | Self::NotFound(m) | Self::Conflict(m) | Self::Internal(m) => m,
37        }
38    }
39}
40
41impl std::fmt::Display for ApiError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str(self.message())
44    }
45}
46
47pub type Result<T> = std::result::Result<T, ApiError>;
48
49fn internal(e: impl std::fmt::Display) -> ApiError {
50    ApiError::Internal(e.to_string())
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct BranchView {
55    pub branch_id: String,
56    pub op: String,
57    pub url: String,
58    pub status: String,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub struct TransView {
63    pub gid: String,
64    pub trans_type: String,
65    pub status: String,
66    pub rollback_reason: String,
67    pub create_time: i64,
68    pub finish_time: Option<i64>,
69    pub branches: Vec<BranchView>,
70}
71
72/// 分支登记请求。TCC 用 confirm/cancel,XA 用 commit/rollback。
73#[derive(Debug, Clone, Default)]
74pub struct RegisterBranch {
75    pub gid: String,
76    pub branch_id: String,
77    pub confirm: String,
78    pub cancel: String,
79    pub r#try: String,
80    pub commit: String,
81    pub rollback: String,
82}
83
84#[derive(Clone)]
85pub struct Api {
86    pub store: Store,
87    /// 提交后**直接开推**用的推进器。`None` 就是老行为:写完就返回,
88    /// 等推进器自己抢到再推。见 [`Api::with_inline_driver`]
89    inline: Option<crate::driver::Driver>,
90}
91
92impl Api {
93    pub fn new(store: Store) -> Self {
94        Self {
95            store,
96            inline: None,
97        }
98    }
99
100    /// 开启「提交后直接开推」。
101    ///
102    /// # 省掉的是那次抢占往返
103    ///
104    /// 老流程:提交方写完事务就返回,推进器再 `lock_one_due` 抢一次才能推。
105    /// 那次抢占**每笔事务都要付**,在 Redis 上是一次 Lua 往返 —— 实测它就是
106    /// saga 落后 DTM 的主要原因(saga 只有一次客户端请求,摊不薄)。
107    ///
108    /// 新流程:建事务的那条写入里**顺便把租约占在自己手上**
109    /// (`owner=自己`、`next_cron_time=现在+租约`),写成功就等于抢到了,
110    /// 直接推。零额外往返。
111    ///
112    /// # 跟 DTM 的差别:我们不阻塞提交
113    ///
114    /// DTM 是在 submit 请求里同步把事务推完,客户端要一直等。这里是
115    /// **spawn 出去推,提交立刻返回** —— 省掉往返的同时保住了提交延迟。
116    ///
117    /// # 代价
118    ///
119    /// 租约一占就是 `lease` 秒。如果进程在「写完」和「推完」之间挂了,
120    /// 这笔要等租约到期才会被别的实例接手,而不是下一个 tick。
121    /// 这跟「推进器抢到之后崩了」是同一种情形,不是新引入的风险。
122    pub fn with_inline_driver(mut self, d: crate::driver::Driver) -> Self {
123        self.inline = Some(d);
124        self
125    }
126
127    /// 建事务前把租约字段填上。返回是否真的占了 —— 没开内联就不占。
128    fn claim_for_inline(&self, g: &mut dtmrs_store::GlobalRow) -> bool {
129        let Some(d) = &self.inline else { return false };
130        g.owner = d.owner.clone();
131        g.next_cron_time = dtmrs_store::now() + d.lease;
132        true
133    }
134
135    /// 把 prepared 推成 submitted,开了内联就**顺便占下租约**。
136    ///
137    /// 返回 `Advanced` 时事务体一起带回来了,调用方可以直接 [`Self::drive_detached`],
138    /// 不用再读一次(Redis 是脚本尾巴上的 HGETALL,SQL 是本来就要发的那条 SELECT)
139    async fn claim_and_submit(&self, gid: &str) -> Result<SubmitOutcome> {
140        let (owner, nct) = match &self.inline {
141            Some(d) => (d.owner.clone(), dtmrs_store::now() + d.lease),
142            None => (String::new(), dtmrs_store::now()),
143        };
144        self.store
145            .submit_prepared(gid, &owner, nct)
146            .await
147            .map_err(internal)
148    }
149
150    /// 把已经拿到租约的事务扔出去推。**不等它跑完** —— 提交要立刻返回。
151    fn drive_detached(&self, g: dtmrs_store::GlobalRow) {
152        let Some(d) = self.inline.clone() else { return };
153        tokio::spawn(async move {
154            if let Err(e) = d.process(&g).await {
155                // 推失败不影响提交的结果,租约到期后会被重新捞起来
156                tracing::warn!(gid = %g.gid, error = %e, "提交后直接推进出错,等租约到期重试");
157            }
158        });
159    }
160
161    /// 时间戳 + 进程内计数。生产建议客户端直接用业务单号当 gid ——
162    /// 那样天然幂等,重试不会变成两笔
163    pub fn new_gid(&self) -> String {
164        use std::sync::atomic::{AtomicU64, Ordering};
165        static SEQ: AtomicU64 = AtomicU64::new(0);
166        let n = SEQ.fetch_add(1, Ordering::Relaxed);
167        format!("{}-{}", dtmrs_store::now(), n)
168    }
169
170    /// 提交。
171    ///
172    /// **重复提交同一个 gid 必须成功而不是报错** —— 客户端网络抖动重试时
173    /// 返回错误会让它以为没受理,然后换个 gid 再来一次,就成了两笔。
174    pub async fn submit(&self, gid: &str, trans_type: &str, steps: &[SagaStep]) -> Result<()> {
175        if gid.is_empty() {
176            return Err(ApiError::BadRequest("gid 不能为空".into()));
177        }
178        let Some(tt) = TransType::parse(trans_type) else {
179            return Err(ApiError::BadRequest("未知 trans_type".into()));
180        };
181
182        match tt {
183            TransType::Saga => {
184                if steps.is_empty() {
185                    // ⚠ 不带步骤的重复提交**必须幂等成功**,不能因为 steps 为空
186                    // 就报错 —— 客户端重试时经常只带 gid。只有事务压根不存在,
187                    // 才是真的参数错误。(`tc的grpc_api与http同源` 钉着这条)
188                    return match self.claim_and_submit(gid).await? {
189                        SubmitOutcome::Advanced(g) => {
190                            self.drive_detached(*g);
191                            Ok(())
192                        }
193                        SubmitOutcome::Already => Ok(()),
194                        SubmitOutcome::Missing => {
195                            Err(ApiError::BadRequest("saga 的 steps 不能为空".into()))
196                        }
197                    };
198                }
199                let (mut g, branches) = saga_rows(gid, steps);
200                // 开了内联推进的话,这条写入顺便把租约占下来,写成功就直接推,
201                // 不用再走一次抢占(见 `with_inline_driver`)
202                let claimed = self.claim_for_inline(&mut g);
203                // **先建,不先查。** `create_global` 本身就是幂等的(已存在返回
204                // false 且不覆盖),所以正常路径一次往返就够 —— 这是 saga 提交的
205                // 热路径,先查一次等于白付一次往返,而且那还是个 Lua 脚本调用,
206                // 比普通命令贵得多(实测多这一次让 saga 吞吐掉了 16%)
207                if self
208                    .store
209                    .create_global(&g, &branches)
210                    .await
211                    .map_err(internal)?
212                {
213                    if claimed {
214                        self.drive_detached(g);
215                    }
216                    return Ok(());
217                }
218                // 已存在。可能是重复提交(幂等返回成功就行),也可能是这个 gid
219                // 其实是 prepare 过的 tcc/msg/xa —— 客户端没传 trans_type 时
220                // 会被当成 saga。后一种要真的把它推成 submitted,交给下面决断
221                if let SubmitOutcome::Advanced(g) = self.claim_and_submit(gid).await? {
222                    self.drive_detached(*g);
223                }
224                Ok(())
225            }
226            TransType::Tcc | TransType::Msg | TransType::Xa => {
227                // prepare 已经建过事务,submit 只是把它推成 submitted。
228                //
229                // **一次存储调用做完**(原来是 get_global + set_global_status
230                // + schedule_now 三次)。Redis 上这三次是 11 条命令,现在 3 条
231                match self.claim_and_submit(gid).await? {
232                    // 推成 submitted 了。开了内联就直接推 —— 跟 saga 一样,
233                    // 省掉那次抢占往返。事务体是 submit_prepared 顺带返回的,
234                    // 没有多付一次读
235                    SubmitOutcome::Advanced(g) => {
236                        self.drive_detached(*g);
237                        Ok(())
238                    }
239                    // 已经提交过 —— 幂等返回成功
240                    SubmitOutcome::Already => Ok(()),
241                    SubmitOutcome::Missing => {
242                        Err(ApiError::BadRequest("tcc/xa/msg 要先调 prepare".into()))
243                    }
244                }
245            }
246            // workflow 的「步骤」是**代码**,没法表示成 URL 存进库里,
247            // 所以只能在嵌入式形态下提交(Embedded::workflow + submit_workflow)。
248            // 这不是暂未实现,是这个模式的本质决定的
249            TransType::Workflow => Err(ApiError::BadRequest(
250                "workflow 模式只能在嵌入式形态下提交(步骤是进程内的函数,不是 URL)".into(),
251            )),
252        }
253    }
254
255    /// 第一阶段。msg 建 prepared 事务 + 正向分支;tcc / xa 只建空事务。
256    pub async fn prepare(
257        &self,
258        gid: &str,
259        trans_type: &str,
260        actions: &[String],
261        query_prepared: &str,
262        grace_secs: Option<i64>,
263    ) -> Result<()> {
264        if gid.is_empty() {
265            return Err(ApiError::BadRequest("gid 不能为空".into()));
266        }
267        match TransType::parse(trans_type) {
268            Some(TransType::Msg) => {
269                if actions.is_empty() {
270                    return Err(ApiError::BadRequest("msg 的 actions 不能为空".into()));
271                }
272                if query_prepared.is_empty() {
273                    // 没有回查地址,客户端崩在 prepare 和 submit 之间就没人能
274                    // 决断这单了。猜「已提交」会重复扣款,猜「没提交」会丢单
275                    return Err(ApiError::BadRequest(
276                        "msg 必须提供 query_prepared,否则崩溃后无法决断".into(),
277                    ));
278                }
279                let (g, br) = msg_rows(gid, actions, query_prepared, grace_secs.unwrap_or(10));
280                self.store.create_global(&g, &br).await.map_err(internal)?;
281                Ok(())
282            }
283            Some(tt @ (TransType::Tcc | TransType::Xa)) => {
284                let mut g = tcc_rows(gid);
285                g.trans_type = tt;
286                self.store.create_global(&g, &[]).await.map_err(internal)?;
287                Ok(())
288            }
289            _ => Err(ApiError::BadRequest(
290                "prepare 支持 tcc / xa / msg;saga 直接 submit".into(),
291            )),
292        }
293    }
294
295    /// 分支登记。**必须先登记再做一阶段**:反过来的话一阶段成功但登记失败,
296    /// TC 就不知道有这个分支,回滚时不会处理它 —— TCC 是预留资源永久泄漏,
297    /// XA 更糟,会留下一个永久持锁的 prepared 事务。
298    pub async fn register_branch(&self, r: &RegisterBranch) -> Result<()> {
299        if r.gid.is_empty() || r.branch_id.is_empty() {
300            return Err(ApiError::BadRequest("gid / branch_id 不能为空".into()));
301        }
302        let tt = match self.store.get_global(&r.gid).await {
303            // ⚠ 必须挡住「事务已经不可能再推进新分支」的状态。
304            //
305            // TCC / XA 的正确顺序是**先登记分支再做一阶段**(见 CLAUDE.md
306            // 「绝对不能破坏的语义」第 5 条)。如果这里放行,客户端拿到 SUCCESS
307            // 之后就会去执行 try / XA PREPARE —— 而 TC 这边事务已经终结或正在
308            // 回滚,那份资源**永远不会有人 confirm 或 cancel**:
309            //   TCC 是资源永久泄漏,XA 更糟 —— 留下永久持锁的 prepared 事务。
310            //
311            // 真实触发路径不需要客户端有 bug:多分支 TCC 登记完分支 1、做完 try、
312            // 正要登记分支 2 时,这笔事务**超时了**,TC 已经回滚并落终态。
313            //
314            // 只放行 Prepared(正常流程)和 Submitted(容忍重试 ——
315            // register_branch 本身是幂等的,见 `分支登记是幂等的`)。
316            Ok(Some(g)) if matches!(g.status, GlobalStatus::Aborting) || g.status.is_final() => {
317                return Err(ApiError::Conflict(format!(
318                    "事务处于 {} 状态,不能再登记分支(登记后的一阶段将无人收尾)",
319                    g.status.as_str()
320                )));
321            }
322            Ok(Some(g)) => g.trans_type,
323            Ok(None) => return Err(ApiError::NotFound("gid 不存在,先 prepare".into())),
324            Err(e) => return Err(internal(e)),
325        };
326
327        let mut ops = Vec::new();
328        match tt {
329            TransType::Tcc => {
330                if r.confirm.is_empty() || r.cancel.is_empty() {
331                    return Err(ApiError::BadRequest(
332                        "tcc 分支必须提供 confirm 和 cancel".into(),
333                    ));
334                }
335                ops.push((BranchOp::Confirm, r.confirm.clone()));
336                ops.push((BranchOp::Cancel, r.cancel.clone()));
337                if !r.r#try.is_empty() {
338                    ops.push((BranchOp::Try, r.r#try.clone()));
339                }
340            }
341            TransType::Xa => {
342                if r.commit.is_empty() || r.rollback.is_empty() {
343                    // 缺任一个都可能留下永久持锁的 prepared 事务
344                    return Err(ApiError::BadRequest(
345                        "xa 分支必须提供 commit 和 rollback".into(),
346                    ));
347                }
348                ops.push((BranchOp::Commit, r.commit.clone()));
349                ops.push((BranchOp::Rollback, r.rollback.clone()));
350            }
351            _ => return Err(ApiError::BadRequest("只有 tcc 和 xa 需要登记分支".into())),
352        }
353
354        self.store
355            .register_branch(&r.gid, &r.branch_id, &ops)
356            .await
357            .map_err(internal)
358    }
359
360    /// 主动中止,触发逆序补偿
361    pub async fn abort(&self, gid: &str) -> Result<()> {
362        match self.store.get_global(gid).await {
363            Ok(Some(g)) if !g.status.is_final() => {
364                self.store
365                    .set_global_status(gid, GlobalStatus::Aborting, g.trans_type, "调用方主动中止")
366                    .await
367                    .map_err(internal)?;
368                let _ = self.store.schedule_now(gid).await;
369                Ok(())
370            }
371            Ok(Some(_)) => Err(ApiError::Conflict("事务已终结,无法中止".into())),
372            Ok(None) => Err(ApiError::NotFound("gid 不存在".into())),
373            Err(e) => Err(internal(e)),
374        }
375    }
376
377    /// 立刻重试:把下次调度时间提到现在,并清掉退避累积。
378    ///
379    /// 只是「排到队首」,不跳过任何安全检查 —— 分支该幂等还是要幂等。
380    /// 终态事务不能重试(没意义,而且会让它重新变成活跃事务)。
381    pub async fn retry(&self, gid: &str) -> Result<()> {
382        match self.store.get_global(gid).await {
383            Ok(Some(g)) if !g.status.is_final() => {
384                self.store.schedule_now(gid).await.map_err(internal)?;
385                Ok(())
386            }
387            Ok(Some(_)) => Err(ApiError::Conflict("事务已终结,无需重试".into())),
388            Ok(None) => Err(ApiError::NotFound("gid 不存在".into())),
389            Err(e) => Err(internal(e)),
390        }
391    }
392
393    pub async fn query(&self, gid: &str) -> Result<TransView> {
394        let g = self
395            .store
396            .get_global(gid)
397            .await
398            .map_err(internal)?
399            .ok_or_else(|| ApiError::NotFound("gid 不存在".into()))?;
400        let branches = self.store.list_branches(gid).await.map_err(internal)?;
401        Ok(TransView {
402            gid: g.gid,
403            trans_type: g.trans_type.to_string(),
404            status: g.status.as_str().into(),
405            rollback_reason: g.rollback_reason,
406            create_time: g.create_time,
407            finish_time: g.finish_time,
408            branches: branches
409                .into_iter()
410                .map(|b| BranchView {
411                    branch_id: b.branch_id,
412                    op: b.op.as_str().into(),
413                    url: b.url,
414                    status: b.status.as_str().into(),
415                })
416                .collect(),
417        })
418    }
419
420    pub async fn list_recent(&self, limit: i64) -> Vec<TransView> {
421        self.store
422            .list_recent(limit)
423            .await
424            .unwrap_or_default()
425            .into_iter()
426            .map(|g| TransView {
427                gid: g.gid,
428                trans_type: g.trans_type.to_string(),
429                status: g.status.as_str().into(),
430                rollback_reason: g.rollback_reason,
431                create_time: g.create_time,
432                finish_time: g.finish_time,
433                branches: Vec::new(),
434            })
435            .collect()
436    }
437}