use std::fmt;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use nasaga_core::ServiceIdentity;
use natelemetry::TraceContext;
use crate::transport_shared::{SagaCommandHandler, SagaResultHandler};
use crate::{HandleOutcome, ParticipantHandled, SagaCommandEnvelope, SagaResultEnvelope};
pub mod proto {
nagrpc::include_proto!("nasa.saga.transport.v1");
}
pub mod orchestrator_proto {
nagrpc::include_proto!("nasa.saga.orchestrator.v1");
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SagaGrpcBindingError {
InvalidPeerPrincipal,
}
impl fmt::Display for SagaGrpcBindingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("Saga gRPC peer principal is invalid")
}
}
impl std::error::Error for SagaGrpcBindingError {}
#[derive(Clone)]
pub struct SagaGrpcPeerBinding {
peer_principal: Arc<str>,
producer: ServiceIdentity,
}
impl SagaGrpcPeerBinding {
pub fn new(
peer_principal: impl Into<Arc<str>>,
producer: ServiceIdentity,
) -> Result<Self, SagaGrpcBindingError> {
let peer_principal = peer_principal.into();
let digest = peer_principal.strip_prefix("sha256:").unwrap_or_default();
if digest.len() != 64
|| !digest
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(SagaGrpcBindingError::InvalidPeerPrincipal);
}
Ok(Self {
peer_principal,
producer,
})
}
fn authenticate<T>(
&self,
request: &nagrpc::Request<T>,
) -> Result<SagaGrpcPeerIdentity, nagrpc::Status> {
let authenticated = request
.extensions()
.get::<nagrpc::PeerIdentity>()
.is_some_and(|peer| peer.principal() == self.peer_principal.as_ref());
if !authenticated {
return Err(nagrpc::Status::unauthenticated(
"verified Saga gRPC peer identity is required",
));
}
Ok(SagaGrpcPeerIdentity::MtlsPrincipal(self.producer.clone()))
}
}
fn receipt_trace<T>(request: &nagrpc::Request<T>) -> Option<TraceContext> {
let mut values = request.metadata().get_all("traceparent").iter();
match (values.next(), values.next()) {
(Some(value), None) => value
.to_str()
.ok()
.and_then(TraceContext::parse_traceparent),
_ => None,
}
}
fn delivery_receipt(receipt: SagaGrpcReceipt) -> proto::SagaDeliveryReceipt {
let (kind, reason) = match receipt {
SagaGrpcReceipt::Committed => (proto::SagaReceiptKind::Committed, String::new()),
SagaGrpcReceipt::Duplicate => (proto::SagaReceiptKind::Duplicate, String::new()),
SagaGrpcReceipt::DeterministicReject { reason } => (
proto::SagaReceiptKind::DeterministicReject,
reason.to_owned(),
),
SagaGrpcReceipt::Retryable => (proto::SagaReceiptKind::Retryable, String::new()),
};
proto::SagaDeliveryReceipt {
kind: kind as i32,
reason,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SagaGrpcReceipt {
Committed,
Duplicate,
DeterministicReject {
reason: &'static str,
},
Retryable,
}
#[derive(Debug, Clone)]
pub enum SagaGrpcPeerIdentity {
MtlsPrincipal(ServiceIdentity),
VerifiedSignature(ServiceIdentity),
}
impl SagaGrpcPeerIdentity {
pub fn producer(&self) -> &ServiceIdentity {
match self {
Self::MtlsPrincipal(identity) | Self::VerifiedSignature(identity) => identity,
}
}
}
pub struct SagaGrpcCommandServer<H> {
handler: Arc<H>,
trusted_producer: ServiceIdentity,
}
pub struct SagaGrpcCommandTransportService<H> {
adjudicator: SagaGrpcCommandServer<H>,
peer: SagaGrpcPeerBinding,
}
impl<H: SagaCommandHandler> SagaGrpcCommandTransportService<H> {
pub fn new(handler: Arc<H>, peer: SagaGrpcPeerBinding) -> Self {
let adjudicator = SagaGrpcCommandServer::new(handler, peer.producer.clone());
Self { adjudicator, peer }
}
}
#[nagrpc::async_trait]
impl<H: SagaCommandHandler> proto::saga_command_transport_server::SagaCommandTransport
for SagaGrpcCommandTransportService<H>
{
async fn deliver(
&self,
request: nagrpc::Request<proto::SagaDeliveryRequest>,
) -> Result<nagrpc::Response<proto::SagaDeliveryReceipt>, nagrpc::Status> {
let peer = self.peer.authenticate(&request)?;
let trace = receipt_trace(&request);
let receipt = self
.adjudicator
.adjudicate(&peer, &request.get_ref().envelope_json, trace.as_ref())
.await;
Ok(nagrpc::Response::new(delivery_receipt(receipt)))
}
}
impl<H: SagaCommandHandler> SagaGrpcCommandServer<H> {
pub fn new(handler: Arc<H>, trusted_producer: ServiceIdentity) -> Self {
Self {
handler,
trusted_producer,
}
}
pub async fn adjudicate(
&self,
peer: &SagaGrpcPeerIdentity,
payload: &[u8],
receipt_trace: Option<&TraceContext>,
) -> SagaGrpcReceipt {
if peer.producer() != &self.trusted_producer {
return SagaGrpcReceipt::DeterministicReject {
reason: "saga_command_producer_unauthorized",
};
}
let Ok(envelope) = serde_json::from_slice::<SagaCommandEnvelope>(payload) else {
return SagaGrpcReceipt::DeterministicReject {
reason: "saga_command_payload_undecodable",
};
};
let outcome = self
.handler
.handle_authenticated_command_traced(&envelope, peer.producer(), receipt_trace)
.await;
match outcome {
Ok(
ParticipantHandled::Executed(_)
| ParticipantHandled::Suppressed
| ParticipantHandled::Replayed(_)
| ParticipantHandled::ContractViolation,
) => SagaGrpcReceipt::Committed,
Ok(ParticipantHandled::Duplicate) => SagaGrpcReceipt::Duplicate,
Err(error) => match crate::command_dead_letter_reason(&error) {
Some(reason) => SagaGrpcReceipt::DeterministicReject { reason },
None => SagaGrpcReceipt::Retryable,
},
}
}
}
pub struct SagaGrpcResultServer<H> {
handler: Arc<H>,
trusted_producer: ServiceIdentity,
}
pub struct SagaGrpcResultTransportService<H> {
adjudicator: SagaGrpcResultServer<H>,
peer: SagaGrpcPeerBinding,
}
impl<H: SagaResultHandler> SagaGrpcResultTransportService<H> {
pub fn new(handler: Arc<H>, peer: SagaGrpcPeerBinding) -> Self {
let adjudicator = SagaGrpcResultServer::new(handler, peer.producer.clone());
Self { adjudicator, peer }
}
}
#[nagrpc::async_trait]
impl<H: SagaResultHandler> proto::saga_result_transport_server::SagaResultTransport
for SagaGrpcResultTransportService<H>
{
async fn deliver(
&self,
request: nagrpc::Request<proto::SagaDeliveryRequest>,
) -> Result<nagrpc::Response<proto::SagaDeliveryReceipt>, nagrpc::Status> {
let peer = self.peer.authenticate(&request)?;
let trace = receipt_trace(&request);
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| i64::try_from(duration.as_millis()).ok())
.ok_or_else(|| nagrpc::Status::internal("Saga gRPC clock is unavailable"))?;
let receipt = self
.adjudicator
.adjudicate(
&peer,
&request.get_ref().envelope_json,
trace.as_ref(),
now_ms,
)
.await;
Ok(nagrpc::Response::new(delivery_receipt(receipt)))
}
}
impl<H: SagaResultHandler> SagaGrpcResultServer<H> {
pub fn new(handler: Arc<H>, trusted_producer: ServiceIdentity) -> Self {
Self {
handler,
trusted_producer,
}
}
pub async fn adjudicate(
&self,
peer: &SagaGrpcPeerIdentity,
payload: &[u8],
receipt_trace: Option<&TraceContext>,
now_ms: i64,
) -> SagaGrpcReceipt {
if peer.producer() != &self.trusted_producer {
return SagaGrpcReceipt::DeterministicReject {
reason: "saga_result_producer_unauthorized",
};
}
let Ok(envelope) = serde_json::from_slice::<SagaResultEnvelope>(payload) else {
return SagaGrpcReceipt::DeterministicReject {
reason: "saga_result_payload_undecodable",
};
};
let outcome = self
.handler
.handle_authenticated_result_traced(&envelope, peer.producer(), receipt_trace, now_ms)
.await;
match outcome {
Ok(HandleOutcome::Applied { .. }) => SagaGrpcReceipt::Committed,
Ok(HandleOutcome::Duplicate) => SagaGrpcReceipt::Duplicate,
Err(error) => {
if matches!(
crate::classify_result_delivery_error(&error),
crate::ResultDeliveryDisposition::DeadLetter
) {
let reason = error
.chain()
.find_map(|cause| {
cause
.downcast_ref::<crate::SagaResultProcessingError>()
.copied()
})
.and_then(crate::SagaResultProcessingError::dead_letter_reason)
.unwrap_or("saga_result_contract_invalid");
SagaGrpcReceipt::DeterministicReject { reason }
} else {
SagaGrpcReceipt::Retryable
}
}
}
}
}
pub fn outbox_disposition_of(
receipt: Option<&SagaGrpcReceipt>,
) -> Result<(), naoutbox_core::OutboxPublishError> {
match receipt {
Some(SagaGrpcReceipt::Committed) | Some(SagaGrpcReceipt::Duplicate) => Ok(()),
Some(SagaGrpcReceipt::DeterministicReject { reason }) => {
Err(naoutbox_core::OutboxPublishError::new(*reason))
}
Some(SagaGrpcReceipt::Retryable) | None => Err(
naoutbox_core::OutboxPublishError::transient("saga_grpc_delivery_unresolved"),
),
}
}