Skip to main content

dtmrs_server/grpc/
client.rs

1//! TC 去调业务方的 gRPC 分支。
2//!
3//! # 为什么不需要业务方的 proto
4//!
5//! 这是这一层唯一的技术难点。TC 要能调**任意**业务服务的**任意**方法,
6//! 但编译期根本不知道对方的 proto 长什么样。
7//!
8//! 解法是绕开 protobuf 的类型系统:用一个只会搬字节的 [`BytesCodec`] 替换
9//! tonic 默认的 prost codec,请求体发裸字节、响应体收裸字节。gRPC 的方法路径
10//! (`/包.服务/方法`)本来就是运行期的字符串,所以整条调用链都不需要类型信息。
11//!
12//! 换来的好处很实在:**业务方不用为 dtmrs 改接口**,已有的 gRPC 服务直接
13//! 就能当分支用。DTM 也是这个路子。
14//!
15//! # 请求体发什么
16//!
17//! 空字节。空的 protobuf 消息对**任何** message 类型都是合法的(所有字段取默认值),
18//! 所以不管对方方法的入参声明成什么都能解开。
19//!
20//! 分支的身份(gid / branch_id / op / trans_type)走 metadata,不走请求体 ——
21//! 这正是屏障需要的全部信息。跟 HTTP 那边把它们放 query 参数是一回事。
22//!
23//! (每步独立的业务 payload 是后续版本的事,HTTP 那边目前也统一发 `{}`。)
24//!
25//! # 结果判定
26//!
27//! 只看 gRPC 状态码,映射见 [`dtmrs_core::BranchResult::from_grpc`]。
28//! **连不上、超时、`UNAVAILABLE` 一律是「结果未知」而不是失败** ——
29//! 跟 HTTP 侧「超时不等于失败」是同一条命门。
30
31// 走 prost 的 re-export,不额外引一个 bytes 依赖
32use dtmrs_core::BranchResult;
33use prost::bytes::{Buf, BufMut};
34use std::collections::HashMap;
35use std::sync::{Arc, Mutex};
36use std::time::Duration;
37use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder};
38use tonic::codegen::http::uri::PathAndQuery;
39use tonic::transport::{Channel, Endpoint};
40use tonic::Status;
41use tracing::{info, warn};
42
43use super::{MD_BRANCH_ID, MD_GID, MD_OP, MD_TRANS_TYPE};
44use crate::registry::GrpcTarget;
45
46/// 只搬字节的 codec —— 让 tonic 在不知道消息类型的前提下完成一次 unary 调用。
47#[derive(Debug, Default, Clone, Copy)]
48pub struct BytesCodec;
49
50impl Codec for BytesCodec {
51    type Encode = Vec<u8>;
52    type Decode = Vec<u8>;
53    type Encoder = BytesCodec;
54    type Decoder = BytesCodec;
55
56    fn encoder(&mut self) -> Self::Encoder {
57        *self
58    }
59    fn decoder(&mut self) -> Self::Decoder {
60        *self
61    }
62}
63
64impl Encoder for BytesCodec {
65    type Item = Vec<u8>;
66    type Error = Status;
67
68    fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
69        dst.put_slice(&item);
70        Ok(())
71    }
72}
73
74impl Decoder for BytesCodec {
75    type Item = Vec<u8>;
76    type Error = Status;
77
78    /// tonic 保证 `src` 里正好是一条完整消息,不用自己拆帧
79    fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
80        let mut out = vec![0u8; src.remaining()];
81        src.copy_to_slice(&mut out);
82        Ok(Some(out))
83    }
84}
85
86/// gRPC 分支调用器,带 channel 缓存。
87///
88/// 用 `connect_lazy` 而不是 `connect`:连接在首次真正发请求时才建立,
89/// 断了之后 tonic 自己重连。所以缓存里的 channel **不会因为对方重启而变成死的**,
90/// 不需要额外的健康检查和淘汰逻辑。
91#[derive(Clone)]
92pub struct GrpcCaller {
93    channels: Arc<Mutex<HashMap<String, Channel>>>,
94    timeout: Duration,
95}
96
97impl GrpcCaller {
98    pub fn new(timeout: Duration) -> Self {
99        Self {
100            channels: Arc::new(Mutex::new(HashMap::new())),
101            timeout,
102        }
103    }
104
105    fn channel(&self, endpoint: &str) -> Result<Channel, String> {
106        // 先查缓存。锁里不做 await,所以用 std 的 Mutex 就够
107        if let Some(c) = self.channels.lock().unwrap().get(endpoint) {
108            return Ok(c.clone());
109        }
110        let ch = Endpoint::from_shared(endpoint.to_string())
111            .map_err(|e| format!("gRPC 地址不合法: {e}"))?
112            .timeout(self.timeout)
113            .connect_timeout(self.timeout)
114            .connect_lazy();
115        self.channels
116            .lock()
117            .unwrap()
118            .insert(endpoint.to_string(), ch.clone());
119        Ok(ch)
120    }
121
122    /// 调一个 gRPC 分支。任何失败都不会返回 [`BranchResult::Failure`] ——
123    /// 只有对方**明确**返回 `ABORTED` 才算业务要求回滚。
124    pub async fn call(
125        &self,
126        target: &GrpcTarget,
127        gid: &str,
128        trans_type: &str,
129        branch_id: &str,
130        op: &str,
131    ) -> BranchResult {
132        let channel = match self.channel(&target.endpoint) {
133            Ok(c) => c,
134            Err(e) => {
135                // 地址都拼不出来,是配置错误。但仍然按「未知」处理:
136                // 判失败会触发回滚,而这其实是部署问题,改对了重试才对
137                warn!(gid, branch = branch_id, endpoint = %target.endpoint, error = %e,
138                      "gRPC 分支地址不合法,按结果未知处理(会重试,不回滚)");
139                return BranchResult::Unknown;
140            }
141        };
142
143        let path = match PathAndQuery::try_from(target.path.clone()) {
144            Ok(p) => p,
145            Err(e) => {
146                warn!(gid, branch = branch_id, path = %target.path, error = %e,
147                      "gRPC 方法路径不合法,按结果未知处理");
148                return BranchResult::Unknown;
149            }
150        };
151
152        let mut grpc = tonic::client::Grpc::new(channel);
153        if let Err(e) = grpc.ready().await {
154            warn!(gid, branch = branch_id, error = %e, "gRPC 分支不可达,结果未知");
155            return BranchResult::Unknown;
156        }
157
158        // 空消息体:对任何 message 类型都合法。分支身份走 metadata
159        let mut req = tonic::Request::new(Vec::<u8>::new());
160        for (k, v) in [
161            (MD_GID, gid),
162            (MD_TRANS_TYPE, trans_type),
163            (MD_BRANCH_ID, branch_id),
164            (MD_OP, op),
165        ] {
166            match v.parse() {
167                Ok(val) => {
168                    req.metadata_mut().insert(k, val);
169                }
170                Err(_) => {
171                    // gid 里有非 ASCII 之类。这些值是我们自己生成/客户端给的,
172                    // 塞不进 header 就没法让业务方做幂等 —— 宁可不调
173                    warn!(
174                        gid,
175                        branch = branch_id,
176                        key = k,
177                        "metadata 值不合法(非 ASCII?),无法调用 gRPC 分支"
178                    );
179                    return BranchResult::Unknown;
180                }
181            }
182        }
183
184        match grpc
185            .unary::<Vec<u8>, Vec<u8>, BytesCodec>(req, path, BytesCodec)
186            .await
187        {
188            Ok(_) => {
189                info!(gid, branch = branch_id, op, "gRPC 分支返回 OK");
190                BranchResult::Success
191            }
192            Err(status) => {
193                let r = BranchResult::from_grpc(status.code() as i32);
194                info!(gid, branch = branch_id, op, code = ?status.code(), result = ?r,
195                      "gRPC 分支返回");
196                r
197            }
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn 地址不合法时是未知而不是失败() {
208        // 判失败会触发回滚,而地址写错是部署问题
209        let c = GrpcCaller::new(Duration::from_secs(1));
210        assert!(c.channel("这不是个地址").is_err());
211    }
212
213    #[tokio::test]
214    async fn 连不上的分支不能触发回滚() {
215        // 端口上没人听 —— 必须是 Unknown(重试),绝不能是 Failure(回滚)
216        let c = GrpcCaller::new(Duration::from_millis(300));
217        let t = GrpcTarget {
218            endpoint: "http://127.0.0.1:1".into(),
219            path: "/a.B/C".into(),
220        };
221        let r = c.call(&t, "g1", "saga", "01", "action").await;
222        assert_eq!(r, BranchResult::Unknown, "连不上必须是未知,不能是失败");
223    }
224
225    /// `connect_lazy` 内部要拿 tokio 的 executor,**必须在运行时里调** ——
226    /// 普通 `#[test]` 会直接 panic。生产路径上分支调用本来就在运行时里,
227    /// 不受影响
228    #[tokio::test]
229    async fn channel会被缓存复用() {
230        let c = GrpcCaller::new(Duration::from_secs(1));
231        let a = c.channel("http://127.0.0.1:9").unwrap();
232        let b = c.channel("http://127.0.0.1:9").unwrap();
233        // 缓存命中时表里只该有一条
234        assert_eq!(c.channels.lock().unwrap().len(), 1);
235        drop((a, b));
236    }
237}