use dtmrs_core::BranchResult;
use prost::bytes::{Buf, BufMut};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder};
use tonic::codegen::http::uri::PathAndQuery;
use tonic::transport::{Channel, Endpoint};
use tonic::Status;
use tracing::{info, warn};
use super::{MD_BRANCH_ID, MD_GID, MD_OP, MD_TRANS_TYPE};
use crate::registry::GrpcTarget;
#[derive(Debug, Default, Clone, Copy)]
pub struct BytesCodec;
impl Codec for BytesCodec {
type Encode = Vec<u8>;
type Decode = Vec<u8>;
type Encoder = BytesCodec;
type Decoder = BytesCodec;
fn encoder(&mut self) -> Self::Encoder {
*self
}
fn decoder(&mut self) -> Self::Decoder {
*self
}
}
impl Encoder for BytesCodec {
type Item = Vec<u8>;
type Error = Status;
fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
dst.put_slice(&item);
Ok(())
}
}
impl Decoder for BytesCodec {
type Item = Vec<u8>;
type Error = Status;
fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
let mut out = vec![0u8; src.remaining()];
src.copy_to_slice(&mut out);
Ok(Some(out))
}
}
#[derive(Clone)]
pub struct GrpcCaller {
channels: Arc<Mutex<HashMap<String, Channel>>>,
timeout: Duration,
extra_ca: Option<Arc<Vec<u8>>>,
}
fn check_ca_pem(pem: Vec<u8>, from: &str) -> Option<Arc<Vec<u8>>> {
const MARK: &[u8] = b"-----BEGIN CERTIFICATE-----";
let has_cert = pem.windows(MARK.len()).any(|w| w == MARK);
if !has_cert {
warn!(
source = from,
bytes = pem.len(),
"额外 CA 里没有 BEGIN CERTIFICATE 块,已忽略 —— \
传了私钥或指错文件?注意 tonic 对这种输入不会报错,只会静默不生效"
);
return None;
}
Some(Arc::new(pem))
}
impl GrpcCaller {
pub fn new(timeout: Duration) -> Self {
let extra_ca = std::env::var("DTMRS_GRPC_CA").ok().and_then(|p| {
match std::fs::read(&p) {
Ok(pem) => check_ca_pem(pem, &p),
Err(e) => {
warn!(path = %p, error = %e, "DTMRS_GRPC_CA 读不到,忽略该配置");
None
}
}
});
Self {
channels: Arc::new(Mutex::new(HashMap::new())),
timeout,
extra_ca,
}
}
pub fn with_ca_pem(mut self, pem: impl Into<Vec<u8>>) -> Self {
self.extra_ca = check_ca_pem(pem.into(), "<直接传入>");
self
}
fn channel(&self, target: &GrpcTarget) -> Result<Channel, String> {
let endpoint = target.endpoint.as_str();
if let Some(c) = self.channels.lock().unwrap().get(endpoint) {
return Ok(c.clone());
}
let mut ep = Endpoint::from_shared(endpoint.to_string())
.map_err(|e| format!("gRPC 地址不合法: {e}"))?
.timeout(self.timeout)
.connect_timeout(self.timeout);
if target.tls {
let mut tls = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
if let Some(pem) = &self.extra_ca {
tls = tls.ca_certificate(tonic::transport::Certificate::from_pem(pem.as_slice()));
}
ep = ep
.tls_config(tls)
.map_err(|e| format!("gRPC TLS 配置不可用: {e}"))?;
}
let ch = ep.connect_lazy();
self.channels
.lock()
.unwrap()
.insert(endpoint.to_string(), ch.clone());
Ok(ch)
}
pub async fn call(
&self,
target: &GrpcTarget,
gid: &str,
trans_type: &str,
branch_id: &str,
op: &str,
) -> BranchResult {
let channel = match self.channel(target) {
Ok(c) => c,
Err(e) => {
warn!(gid, branch = branch_id, endpoint = %target.endpoint, error = %e,
"gRPC 分支地址不合法,按结果未知处理(会重试,不回滚)");
return BranchResult::Unknown;
}
};
let path = match PathAndQuery::try_from(target.path.clone()) {
Ok(p) => p,
Err(e) => {
warn!(gid, branch = branch_id, path = %target.path, error = %e,
"gRPC 方法路径不合法,按结果未知处理");
return BranchResult::Unknown;
}
};
let mut grpc = tonic::client::Grpc::new(channel);
if let Err(e) = grpc.ready().await {
warn!(gid, branch = branch_id, error = %e, "gRPC 分支不可达,结果未知");
return BranchResult::Unknown;
}
let mut req = tonic::Request::new(Vec::<u8>::new());
for (k, v) in [
(MD_GID, gid),
(MD_TRANS_TYPE, trans_type),
(MD_BRANCH_ID, branch_id),
(MD_OP, op),
] {
match v.parse() {
Ok(val) => {
req.metadata_mut().insert(k, val);
}
Err(_) => {
warn!(
gid,
branch = branch_id,
key = k,
"metadata 值不合法(非 ASCII?),无法调用 gRPC 分支"
);
return BranchResult::Unknown;
}
}
}
match grpc
.unary::<Vec<u8>, Vec<u8>, BytesCodec>(req, path, BytesCodec)
.await
{
Ok(_) => {
info!(gid, branch = branch_id, op, "gRPC 分支返回 OK");
BranchResult::Success
}
Err(status) => {
let r = BranchResult::from_grpc(status.code() as i32);
info!(gid, branch = branch_id, op, code = ?status.code(), result = ?r,
"gRPC 分支返回");
r
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn target(endpoint: &str, tls: bool) -> GrpcTarget {
GrpcTarget {
endpoint: endpoint.into(),
path: "/a.B/C".into(),
tls,
}
}
#[test]
fn 地址不合法时是未知而不是失败() {
let c = GrpcCaller::new(Duration::from_secs(1));
assert!(c.channel(&target("这不是个地址", false)).is_err());
}
#[tokio::test]
async fn 连不上的分支不能触发回滚() {
let c = GrpcCaller::new(Duration::from_millis(300));
let r = c
.call(&target("http://127.0.0.1:1", false), "g1", "saga", "01", "action")
.await;
assert_eq!(r, BranchResult::Unknown, "连不上必须是未知,不能是失败");
}
#[tokio::test]
async fn tls握手失败也只能是未知() {
let c = GrpcCaller::new(Duration::from_millis(300));
let r = c
.call(&target("https://127.0.0.1:1", true), "g1", "saga", "01", "action")
.await;
assert_eq!(r, BranchResult::Unknown, "TLS 失败是部署问题,不是业务拒绝");
}
#[tokio::test]
async fn tls端点能建出channel() {
let c = GrpcCaller::new(Duration::from_secs(1));
assert!(
c.channel(&target("https://busi.internal:9000", true)).is_ok(),
"TLS 配置装不上,多半是 tonic 的 tls feature 没开"
);
}
#[tokio::test]
async fn 垃圾ca要被挡掉而不是静默收下() {
for junk in [
&b"not a pem"[..],
&b"-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----"[..], &b""[..],
] {
let c = GrpcCaller::new(Duration::from_secs(1)).with_ca_pem(junk.to_vec());
assert!(
c.extra_ca.is_none(),
"垃圾 PEM 被当成 CA 收下了,那它只会静默不生效"
);
assert!(c.channel(&target("https://a:1", true)).is_ok());
}
}
#[tokio::test]
async fn 像样的ca要能装上() {
let pem = b"-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n".to_vec();
let c = GrpcCaller::new(Duration::from_secs(1)).with_ca_pem(pem);
assert!(c.extra_ca.is_some(), "守卫不能误伤真的证书");
}
#[tokio::test]
async fn channel会被缓存复用() {
let c = GrpcCaller::new(Duration::from_secs(1));
let a = c.channel(&target("http://127.0.0.1:9", false)).unwrap();
let b = c.channel(&target("http://127.0.0.1:9", false)).unwrap();
assert_eq!(c.channels.lock().unwrap().len(), 1);
drop((a, b));
}
}