Skip to main content

dtmrs_server/
embedded.rs

1//! 嵌入式 TC —— 把事务协调器当库链进你自己的进程,**不需要单独部署一个服务**。
2//!
3//! 这是 dtmrs 相对 DTM 的结构性差异。DTM 是 Go,`c-shared` 会把整个运行时拖进去,
4//! 实际没法当库用,所以必须独立部署:
5//!
6//! ```text
7//! DTM:    你的服务 ──HTTP──► 独立部署的 TC 进程 ──► DB
8//!                            (要运维、要高可用、要监控)
9//! dtmrs:  你的服务(TC 就在进程里)──► DB
10//! ```
11//!
12//! 少一个组件,而且分支调用退化成**一次函数调用** —— 没有网络、没有序列化。
13//!
14//! # 用法
15//!
16//! ```no_run
17//! use dtmrs_server::embedded::Embedded;
18//! use dtmrs_core::BranchResult;
19//!
20//! # async fn demo() -> anyhow::Result<()> {
21//! let tc = Embedded::builder("sqlite:app.db")
22//!     .handler("deduct",      |_ctx| async { BranchResult::Success })
23//!     .handler("deduct_undo", |_ctx| async { BranchResult::Success })
24//!     .start()
25//!     .await?;
26//!
27//! tc.saga("order-1001")
28//!     .step("local://deduct", "local://deduct_undo")
29//!     // 可以跟远端服务混用
30//!     .step("http://shipment/create", "http://shipment/cancel")
31//!     .submit()
32//!     .await?;
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! # 一个必须知道的约束
38//!
39//! `local://` 分支存的是**名字**,因为闭包没法持久化。重启后必须注册同名 handler,
40//! 否则事务推不动(会当成"结果未知"一直重试,不会误回滚)。
41//! `submit` 时会检查名字是否都注册了 —— 宁可提交就报错,也别等副作用落地了才发现。
42
43use crate::driver::Driver;
44use crate::registry::{BranchCtx, Registry};
45use crate::saga_rows;
46use crate::workflow::{WorkflowCtx, WorkflowRegistry, WorkflowResult};
47use dtmrs_core::{BranchResult, GlobalStatus, SagaStep, TransType};
48use dtmrs_store::Store;
49use std::future::Future;
50use std::sync::Arc;
51use std::time::Duration;
52
53pub struct EmbeddedBuilder {
54    db: String,
55    owner: String,
56    registry: Registry,
57    workflows: WorkflowRegistry,
58    tick: Duration,
59}
60
61impl EmbeddedBuilder {
62    /// 注册一个进程内分支。名字对应 `local://名字`。
63    pub fn handler<F, Fut>(mut self, name: &str, f: F) -> Self
64    where
65        F: Fn(BranchCtx) -> Fut + Send + Sync + 'static,
66        Fut: Future<Output = BranchResult> + Send + 'static,
67    {
68        self.registry.register(name, f);
69        self
70    }
71
72    pub fn owner(mut self, o: &str) -> Self {
73        self.owner = o.to_string();
74        self
75    }
76
77    /// 推进器轮询间隔。默认 200ms —— 进程内调用很快,不需要像跨网络那样保守
78    pub fn tick(mut self, d: Duration) -> Self {
79        self.tick = d;
80        self
81    }
82
83    /// 注册一个 workflow:把整个事务流程写成一个普通函数。
84    ///
85    /// 跟 saga 的区别是**步骤由函数自己决定** —— 可以有 if、有循环、
86    /// 可以依赖前一步的返回值。崩溃后靠重放 + 结果记忆化续跑。
87    ///
88    /// 详见 [`crate::workflow`],尤其是「你的函数必须是确定性的」那节。
89    pub fn workflow<F, Fut>(mut self, name: &str, f: F) -> Self
90    where
91        F: Fn(WorkflowCtx) -> Fut + Send + Sync + 'static,
92        Fut: std::future::Future<Output = WorkflowResult<()>> + Send + 'static,
93    {
94        self.workflows.register(name, f);
95        self
96    }
97
98    pub async fn start(self) -> anyhow::Result<Embedded> {
99        let store = Store::open(&self.db).await?;
100        let registry = Arc::new(self.registry);
101        let workflows = Arc::new(self.workflows);
102        let driver = Driver::new(store.clone(), self.owner)
103            .with_registry(registry.clone())
104            .with_workflows(workflows.clone());
105        // 常驻推进器。重启后未终结的事务会被它自动捞起继续推 —— 崩溃恢复就靠这个
106        let task = tokio::spawn(driver.clone().run_forever(self.tick));
107        Ok(Embedded {
108            store,
109            registry,
110            workflows,
111            task: Some(task),
112        })
113    }
114}
115
116pub struct Embedded {
117    store: Store,
118    registry: Arc<Registry>,
119    workflows: Arc<WorkflowRegistry>,
120    task: Option<tokio::task::JoinHandle<()>>,
121}
122
123impl Embedded {
124    pub fn builder(db: &str) -> EmbeddedBuilder {
125        EmbeddedBuilder {
126            db: db.to_string(),
127            owner: format!("embedded-{}", std::process::id()),
128            registry: Registry::new(),
129            workflows: WorkflowRegistry::new(),
130            tick: Duration::from_millis(200),
131        }
132    }
133
134    pub fn saga(&self, gid: &str) -> SagaBuilder<'_> {
135        SagaBuilder {
136            tc: self,
137            gid: gid.to_string(),
138            steps: Vec::new(),
139        }
140    }
141
142    /// 提交一个 workflow 事务。
143    ///
144    /// `name` 必须是 [`EmbeddedBuilder::workflow`] 注册过的名字 ——
145    /// 这里就检查,宁可提交报错,也别等推到一半才发现函数不存在。
146    ///
147    /// `input` 原样透传给函数(`WorkflowCtx::input`)。gid 本身通常就是业务单号,
148    /// 简单场景可以传空串。
149    pub async fn submit_workflow(&self, gid: &str, name: &str, input: &str) -> anyhow::Result<()> {
150        if !self.workflows.contains(name) {
151            anyhow::bail!(
152                "workflow「{name}」没注册。已注册的: {:?}",
153                self.workflows.names()
154            );
155        }
156        let mut g = crate::tcc_rows(gid);
157        g.trans_type = TransType::Workflow;
158        g.status = GlobalStatus::Submitted;
159        g.payload = crate::workflow::encode_payload(name, input);
160        // 重复提交同一个 gid 是幂等的(INSERT OR IGNORE),跟 saga 一致
161        self.store.create_global(&g, &[]).await?;
162        Ok(())
163    }
164
165    pub async fn status(&self, gid: &str) -> anyhow::Result<Option<GlobalStatus>> {
166        Ok(self.store.get_global(gid).await?.map(|g| g.status))
167    }
168
169    /// 等到事务落终态。**只是为了测试和"同步等结果"的场景方便** ——
170    /// 生产上事务是异步推进的,别在请求路径里等。
171    pub async fn wait_final(&self, gid: &str, timeout: Duration) -> anyhow::Result<GlobalStatus> {
172        let deadline = std::time::Instant::now() + timeout;
173        loop {
174            if let Some(s) = self.status(gid).await? {
175                if s.is_final() {
176                    return Ok(s);
177                }
178            }
179            if std::time::Instant::now() >= deadline {
180                anyhow::bail!("等 {gid} 落终态超时");
181            }
182            tokio::time::sleep(Duration::from_millis(20)).await;
183        }
184    }
185
186    pub fn store(&self) -> &Store {
187        &self.store
188    }
189}
190
191impl Drop for Embedded {
192    fn drop(&mut self) {
193        // 模拟进程退出:停掉推进器。未终结的事务留在库里,下次 start 会接着推
194        if let Some(t) = self.task.take() {
195            t.abort();
196        }
197    }
198}
199
200pub struct SagaBuilder<'a> {
201    tc: &'a Embedded,
202    gid: String,
203    steps: Vec<SagaStep>,
204}
205
206impl SagaBuilder<'_> {
207    /// 加一步。两个参数都可以是 `local://名字` 或 `http://...`,可混用。
208    pub fn step(mut self, action: &str, compensate: &str) -> Self {
209        self.steps.push(SagaStep::new(action, compensate));
210        self
211    }
212
213    /// 加一步,并带上**这一步自己的**业务数据(发给分支的请求体)。
214    ///
215    /// 扣款那步要金额、发货那步要地址 —— 它们本来就不该收到同一份数据。
216    pub fn step_with(mut self, action: &str, compensate: &str, payload: &str) -> Self {
217        self.steps
218            .push(SagaStep::with_payload(action, compensate, payload));
219        self
220    }
221
222    pub async fn submit(self) -> anyhow::Result<()> {
223        if self.steps.is_empty() {
224            anyhow::bail!("saga 至少要有一步");
225        }
226        // 提交前自查所有 local:// 名字。等推到一半才发现分支不存在就晚了 ——
227        // 那时前几步的副作用已经落地,只能靠补偿收拾。
228        let targets: Vec<String> = self
229            .steps
230            .iter()
231            .flat_map(|s| [s.action.clone(), s.compensate.clone()])
232            .collect();
233        if let Err(missing) = self.tc.registry.check_all(&targets) {
234            anyhow::bail!("这些本地分支没注册: {}", missing.join(", "));
235        }
236        let (g, branches) = saga_rows(&self.gid, &self.steps);
237        self.tc.store.create_global(&g, &branches).await?;
238        Ok(())
239    }
240}