1use 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#[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 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#[derive(Clone)]
92pub struct GrpcCaller {
93 channels: Arc<Mutex<HashMap<String, Channel>>>,
94 timeout: Duration,
95 extra_ca: Option<Arc<Vec<u8>>>,
100}
101
102fn check_ca_pem(pem: Vec<u8>, from: &str) -> Option<Arc<Vec<u8>>> {
113 const MARK: &[u8] = b"-----BEGIN CERTIFICATE-----";
114 let has_cert = pem.windows(MARK.len()).any(|w| w == MARK);
115 if !has_cert {
116 warn!(
117 source = from,
118 bytes = pem.len(),
119 "额外 CA 里没有 BEGIN CERTIFICATE 块,已忽略 —— \
120 传了私钥或指错文件?注意 tonic 对这种输入不会报错,只会静默不生效"
121 );
122 return None;
123 }
124 Some(Arc::new(pem))
125}
126
127impl GrpcCaller {
128 pub fn new(timeout: Duration) -> Self {
133 let extra_ca = std::env::var("DTMRS_GRPC_CA").ok().and_then(|p| {
134 match std::fs::read(&p) {
135 Ok(pem) => check_ca_pem(pem, &p),
136 Err(e) => {
137 warn!(path = %p, error = %e, "DTMRS_GRPC_CA 读不到,忽略该配置");
138 None
139 }
140 }
141 });
142 Self {
143 channels: Arc::new(Mutex::new(HashMap::new())),
144 timeout,
145 extra_ca,
146 }
147 }
148
149 pub fn with_ca_pem(mut self, pem: impl Into<Vec<u8>>) -> Self {
151 self.extra_ca = check_ca_pem(pem.into(), "<直接传入>");
152 self
153 }
154
155 fn channel(&self, target: &GrpcTarget) -> Result<Channel, String> {
156 let endpoint = target.endpoint.as_str();
157 if let Some(c) = self.channels.lock().unwrap().get(endpoint) {
159 return Ok(c.clone());
160 }
161 let mut ep = Endpoint::from_shared(endpoint.to_string())
162 .map_err(|e| format!("gRPC 地址不合法: {e}"))?
163 .timeout(self.timeout)
164 .connect_timeout(self.timeout);
165
166 if target.tls {
167 let mut tls = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
171 if let Some(pem) = &self.extra_ca {
172 tls = tls.ca_certificate(tonic::transport::Certificate::from_pem(pem.as_slice()));
173 }
174 ep = ep
175 .tls_config(tls)
176 .map_err(|e| format!("gRPC TLS 配置不可用: {e}"))?;
177 }
178
179 let ch = ep.connect_lazy();
180 self.channels
181 .lock()
182 .unwrap()
183 .insert(endpoint.to_string(), ch.clone());
184 Ok(ch)
185 }
186
187 pub async fn call(
190 &self,
191 target: &GrpcTarget,
192 gid: &str,
193 trans_type: &str,
194 branch_id: &str,
195 op: &str,
196 ) -> BranchResult {
197 let channel = match self.channel(target) {
198 Ok(c) => c,
199 Err(e) => {
200 warn!(gid, branch = branch_id, endpoint = %target.endpoint, error = %e,
203 "gRPC 分支地址不合法,按结果未知处理(会重试,不回滚)");
204 return BranchResult::Unknown;
205 }
206 };
207
208 let path = match PathAndQuery::try_from(target.path.clone()) {
209 Ok(p) => p,
210 Err(e) => {
211 warn!(gid, branch = branch_id, path = %target.path, error = %e,
212 "gRPC 方法路径不合法,按结果未知处理");
213 return BranchResult::Unknown;
214 }
215 };
216
217 let mut grpc = tonic::client::Grpc::new(channel);
218 if let Err(e) = grpc.ready().await {
219 warn!(gid, branch = branch_id, error = %e, "gRPC 分支不可达,结果未知");
220 return BranchResult::Unknown;
221 }
222
223 let mut req = tonic::Request::new(Vec::<u8>::new());
225 for (k, v) in [
226 (MD_GID, gid),
227 (MD_TRANS_TYPE, trans_type),
228 (MD_BRANCH_ID, branch_id),
229 (MD_OP, op),
230 ] {
231 match v.parse() {
232 Ok(val) => {
233 req.metadata_mut().insert(k, val);
234 }
235 Err(_) => {
236 warn!(
239 gid,
240 branch = branch_id,
241 key = k,
242 "metadata 值不合法(非 ASCII?),无法调用 gRPC 分支"
243 );
244 return BranchResult::Unknown;
245 }
246 }
247 }
248
249 match grpc
250 .unary::<Vec<u8>, Vec<u8>, BytesCodec>(req, path, BytesCodec)
251 .await
252 {
253 Ok(_) => {
254 info!(gid, branch = branch_id, op, "gRPC 分支返回 OK");
255 BranchResult::Success
256 }
257 Err(status) => {
258 let r = BranchResult::from_grpc(status.code() as i32);
259 info!(gid, branch = branch_id, op, code = ?status.code(), result = ?r,
260 "gRPC 分支返回");
261 r
262 }
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn target(endpoint: &str, tls: bool) -> GrpcTarget {
272 GrpcTarget {
273 endpoint: endpoint.into(),
274 path: "/a.B/C".into(),
275 tls,
276 }
277 }
278
279 #[test]
280 fn 地址不合法时是未知而不是失败() {
281 let c = GrpcCaller::new(Duration::from_secs(1));
283 assert!(c.channel(&target("这不是个地址", false)).is_err());
284 }
285
286 #[tokio::test]
287 async fn 连不上的分支不能触发回滚() {
288 let c = GrpcCaller::new(Duration::from_millis(300));
290 let r = c
291 .call(&target("http://127.0.0.1:1", false), "g1", "saga", "01", "action")
292 .await;
293 assert_eq!(r, BranchResult::Unknown, "连不上必须是未知,不能是失败");
294 }
295
296 #[tokio::test]
301 async fn tls握手失败也只能是未知() {
302 let c = GrpcCaller::new(Duration::from_millis(300));
303 let r = c
304 .call(&target("https://127.0.0.1:1", true), "g1", "saga", "01", "action")
305 .await;
306 assert_eq!(r, BranchResult::Unknown, "TLS 失败是部署问题,不是业务拒绝");
307 }
308
309 #[tokio::test]
310 async fn tls端点能建出channel() {
311 let c = GrpcCaller::new(Duration::from_secs(1));
314 assert!(
315 c.channel(&target("https://busi.internal:9000", true)).is_ok(),
316 "TLS 配置装不上,多半是 tonic 的 tls feature 没开"
317 );
318 }
319
320 #[tokio::test]
327 async fn 垃圾ca要被挡掉而不是静默收下() {
328 for junk in [
329 &b"not a pem"[..],
330 &b"-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----"[..], &b""[..],
332 ] {
333 let c = GrpcCaller::new(Duration::from_secs(1)).with_ca_pem(junk.to_vec());
334 assert!(
335 c.extra_ca.is_none(),
336 "垃圾 PEM 被当成 CA 收下了,那它只会静默不生效"
337 );
338 assert!(c.channel(&target("https://a:1", true)).is_ok());
340 }
341 }
342
343 #[tokio::test]
344 async fn 像样的ca要能装上() {
345 let pem = b"-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n".to_vec();
346 let c = GrpcCaller::new(Duration::from_secs(1)).with_ca_pem(pem);
347 assert!(c.extra_ca.is_some(), "守卫不能误伤真的证书");
348 }
349
350 #[tokio::test]
354 async fn channel会被缓存复用() {
355 let c = GrpcCaller::new(Duration::from_secs(1));
356 let a = c.channel(&target("http://127.0.0.1:9", false)).unwrap();
357 let b = c.channel(&target("http://127.0.0.1:9", false)).unwrap();
358 assert_eq!(c.channels.lock().unwrap().len(), 1);
360 drop((a, b));
361 }
362}