Skip to main content

dtmrs_server/
registry.rs

1//! 进程内分支注册表 —— 嵌入式模式的核心。
2//!
3//! # 为什么分支要用「名字」而不是闭包
4//!
5//! 事务必须能跨进程重启恢复,而**闭包没法持久化**。所以数据库里存的是名字
6//! (`local://deduct`),重启后靠注册表把名字重新解析成函数。
7//!
8//! 这是持久化执行引擎的通用做法,也是唯一正确的做法:
9//!
10//! ```text
11//! 提交时   steps = ["local://deduct", "local://deduct_undo"]  → 落库
12//! 崩溃重启 从库里读出 "local://deduct" → registry 查表 → 拿到函数 → 继续推
13//! ```
14//!
15//! **代价**:注册表在重启后必须注册同样的名字,否则事务推不动。这不是缺陷,
16//! 是把"代码版本"这个隐式依赖显式化了 —— 漏注册会明确报错,而不是静默跑错。
17
18use dtmrs_core::{BranchOp, BranchResult};
19use std::collections::HashMap;
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::Arc;
23
24/// 分支被调用时拿到的上下文。业务侧用它做幂等(配合 dtmrs-barrier)。
25#[derive(Debug, Clone)]
26pub struct BranchCtx {
27    pub gid: String,
28    pub branch_id: String,
29    pub op: BranchOp,
30    pub trans_type: String,
31}
32
33type BoxFut = Pin<Box<dyn Future<Output = BranchResult> + Send>>;
34type Handler = Arc<dyn Fn(BranchCtx) -> BoxFut + Send + Sync>;
35
36/// 分支目标:进程内函数、远端 HTTP,还是远端 gRPC。
37///
38/// 用 URI 前缀区分而不是加新字段 —— 这样落库格式不变,也跟 DTM 的 http URL
39/// 完全兼容,同一个事务里可以三种混用。
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Target {
42    /// `local://名字`
43    Local(String),
44    /// `http://...` / `https://...`
45    Http(String),
46    /// `grpc://host:port/包.服务/方法`
47    Grpc(GrpcTarget),
48}
49
50/// 拆好的 gRPC 分支地址。
51///
52/// gRPC 的调用地址天然是两段:连哪个 server(endpoint)+ 调哪个方法(path),
53/// 而 HTTP 是一整个 URL。所以这里必须拆开存,不能像 http 那样原样透传。
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct GrpcTarget {
56    /// tonic 连接用,形如 `http://host:port`
57    pub endpoint: String,
58    /// gRPC 方法路径,形如 `/包.服务/方法`
59    pub path: String,
60}
61
62pub fn parse_target(s: &str) -> Target {
63    if let Some(name) = s.strip_prefix("local://") {
64        return Target::Local(name.to_string());
65    }
66    // grpcs:// 走 TLS,但当前 tonic 依赖没开 tls feature,先只认明文。
67    // 认错了不如认不出来 —— 落到 Http 分支会明确报错,而不是静默用错协议。
68    if let Some(rest) = s.strip_prefix("grpc://") {
69        if let Some(t) = parse_grpc(rest) {
70            return Target::Grpc(t);
71        }
72    }
73    Target::Http(s.to_string())
74}
75
76/// `host:port/包.服务/方法` → (endpoint, path)
77///
78/// 必须正好有两段路径(服务名 + 方法名)。少一段或多一段都说明地址写错了,
79/// 这时候**不能猜** —— 返回 None 让它落到 Http 分支去明确失败。
80fn parse_grpc(rest: &str) -> Option<GrpcTarget> {
81    let (authority, path) = rest.split_once('/')?;
82    if authority.is_empty() {
83        return None;
84    }
85    let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
86    if segs.len() != 2 {
87        return None;
88    }
89    Some(GrpcTarget {
90        endpoint: format!("http://{authority}"),
91        path: format!("/{}/{}", segs[0], segs[1]),
92    })
93}
94
95#[derive(Default)]
96pub struct Registry {
97    handlers: HashMap<String, Handler>,
98}
99
100impl Registry {
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// 注册一个进程内分支。名字要跟 `local://名字` 对应。
106    pub fn register<F, Fut>(&mut self, name: &str, f: F) -> &mut Self
107    where
108        F: Fn(BranchCtx) -> Fut + Send + Sync + 'static,
109        Fut: Future<Output = BranchResult> + Send + 'static,
110    {
111        let h: Handler = Arc::new(move |ctx| Box::pin(f(ctx)));
112        self.handlers.insert(name.to_string(), h);
113        self
114    }
115
116    pub fn get(&self, name: &str) -> Option<Handler> {
117        self.handlers.get(name).cloned()
118    }
119
120    pub fn names(&self) -> Vec<&str> {
121        self.handlers.keys().map(String::as_str).collect()
122    }
123
124    /// 提交前自查:所有 `local://` 分支都注册了吗?
125    ///
126    /// 宁可在提交时就报错,也不要等事务推到一半才发现分支不存在 ——
127    /// 那时候已经有副作用落地了,只能靠补偿收拾。
128    pub fn check_all(&self, targets: &[String]) -> Result<(), Vec<String>> {
129        let missing: Vec<String> = targets
130            .iter()
131            .filter_map(|t| match parse_target(t) {
132                Target::Local(n) if !self.handlers.contains_key(&n) => Some(n),
133                _ => None,
134            })
135            .collect();
136        if missing.is_empty() {
137            Ok(())
138        } else {
139            Err(missing)
140        }
141    }
142}
143
144impl std::fmt::Debug for Registry {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.debug_struct("Registry")
147            .field("handlers", &self.names())
148            .finish()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn 前缀区分本地与远端() {
158        assert_eq!(
159            parse_target("local://deduct"),
160            Target::Local("deduct".into())
161        );
162        assert_eq!(
163            parse_target("http://busi/deduct"),
164            Target::Http("http://busi/deduct".into())
165        );
166        // 没前缀就当 http,保持跟 DTM 的兼容
167        assert_eq!(
168            parse_target("https://a/b"),
169            Target::Http("https://a/b".into())
170        );
171    }
172
173    #[test]
174    fn grpc地址拆成端点与方法路径() {
175        let Target::Grpc(t) = parse_target("grpc://127.0.0.1:9000/busi.Busi/Deduct") else {
176            panic!("应该认成 grpc");
177        };
178        // endpoint 必须带 http:// —— tonic 的 Endpoint 要求是个完整 URI
179        assert_eq!(t.endpoint, "http://127.0.0.1:9000");
180        assert_eq!(t.path, "/busi.Busi/Deduct");
181    }
182
183    #[test]
184    fn 畸形grpc地址不猜而是落回http() {
185        // 少了方法名、少了服务名、路径多一段、没有 authority ——
186        // 全都不能猜。落到 Http 分支会明确失败,比连错服务安全。
187        for bad in [
188            "grpc://127.0.0.1:9000/onlyservice",
189            "grpc://127.0.0.1:9000/",
190            "grpc://127.0.0.1:9000/a/b/c",
191            "grpc:///a/b",
192            "grpc://noslash",
193        ] {
194            assert!(
195                matches!(parse_target(bad), Target::Http(_)),
196                "{bad} 不该被当成合法 grpc 地址"
197            );
198        }
199    }
200
201    #[tokio::test]
202    async fn 注册与调用() {
203        let mut r = Registry::new();
204        r.register("ok", |_ctx| async { BranchResult::Success });
205        let h = r.get("ok").expect("应该能查到");
206        let ctx = BranchCtx {
207            gid: "g".into(),
208            branch_id: "01".into(),
209            op: BranchOp::Action,
210            trans_type: "saga".into(),
211        };
212        assert_eq!(h(ctx).await, BranchResult::Success);
213        assert!(r.get("nope").is_none());
214    }
215
216    #[test]
217    fn 提交前能查出漏注册的分支() {
218        let mut r = Registry::new();
219        r.register("a", |_| async { BranchResult::Success });
220        let targets = vec![
221            "local://a".to_string(),
222            "local://missing".to_string(),
223            "http://x/y".to_string(),
224        ];
225        let err = r.check_all(&targets).unwrap_err();
226        assert_eq!(err, vec!["missing"], "只报本地漏的,http 不管");
227        assert!(r.check_all(&["local://a".to_string()]).is_ok());
228    }
229}