1use nainbox_core::{InboxClaim, InboxStore};
13use naoutbox_core::DurableOutboxAppend;
14use natelemetry::TraceContext;
15use std::collections::{BTreeMap, BTreeSet};
16
17use nasaga_backend::{
18 CancelAdjudication, CompensationAdmission, ExecuteAdmission, ExternalCancelAdmission,
19 ParticipantGateKey, ResolutionAdmission, SagaBackend, SagaBackendFactory, SagaParticipantStore,
20};
21use nasaga_core::{
22 CancelOutcome, CompensationOutcome, DefinitionVersion, EffectId, SagaCancelStep, SagaContext,
23 SagaExecutionError, SagaOutcome, SagaResolveStep, SagaStep, ServiceIdentity, StepAttemptStatus,
24 StepCancelStatus, StepCompensationStatus, StepForwardStatus, StepPhase, StepResolutionStatus,
25 TerminalForwardOutcome, WorkflowName,
26};
27use serde::de::DeserializeOwned;
28
29use crate::envelope::{
30 derive_result_event_id, valid_reason_code, SagaCommandEnvelope, SagaResultEnvelope,
31};
32
33pub trait SagaCommandService<B: SagaBackend>: Send + Sync + 'static {
38 fn handle_saga_command<'a>(
47 &'a self,
48 runtime: &'a ParticipantRuntime<B>,
49 envelope: &'a SagaCommandEnvelope,
50 producer: &'a ServiceIdentity,
51 ) -> impl std::future::Future<Output = anyhow::Result<ParticipantHandled>> + Send + 'a;
52
53 fn handle_saga_command_traced<'a>(
68 &'a self,
69 runtime: &'a ParticipantRuntime<B>,
70 envelope: &'a SagaCommandEnvelope,
71 producer: &'a ServiceIdentity,
72 receipt_trace: Option<&'a TraceContext>,
73 ) -> impl std::future::Future<Output = anyhow::Result<ParticipantHandled>> + Send + 'a {
74 let _ = receipt_trace;
75 self.handle_saga_command(runtime, envelope, producer)
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ParticipantHandled {
85 Executed(StepAttemptStatus),
87 Duplicate,
89 Suppressed,
91 Replayed(StepAttemptStatus),
93 ContractViolation,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ParticipantCommandTrust {
103 producer: ServiceIdentity,
104 workflow: WorkflowName,
105 definition_version: DefinitionVersion,
106 definition_digest: Option<String>,
107}
108
109impl ParticipantCommandTrust {
110 pub fn new(
120 producer: ServiceIdentity,
121 workflow: WorkflowName,
122 definition_version: DefinitionVersion,
123 definition_digest: impl Into<String>,
124 ) -> anyhow::Result<Self> {
125 let definition_digest = definition_digest.into();
126 if !valid_definition_digest(&definition_digest) {
127 anyhow::bail!("participant command trust requires a canonical definition digest");
128 }
129 Ok(Self {
130 producer,
131 workflow,
132 definition_version,
133 definition_digest: Some(definition_digest),
134 })
135 }
136
137 pub fn for_definition_version(
146 producer: ServiceIdentity,
147 workflow: WorkflowName,
148 definition_version: DefinitionVersion,
149 ) -> Self {
150 Self {
151 producer,
152 workflow,
153 definition_version,
154 definition_digest: None,
155 }
156 }
157}
158
159pub struct ParticipantRuntime<B: SagaBackend> {
163 backend: B,
164 consumer: String,
165 trusted_command_producers: BTreeSet<ServiceIdentity>,
166 command_trust: BTreeMap<(WorkflowName, DefinitionVersion, Option<String>), ServiceIdentity>,
167}
168
169pub struct AuthenticatedParticipantRuntime<'a, B: SagaBackend> {
174 runtime: &'a ParticipantRuntime<B>,
175 producer: &'a ServiceIdentity,
176}
177
178impl<B> ParticipantRuntime<B>
179where
180 B: SagaBackend + SagaBackendFactory,
181{
182 pub fn new(
191 consumer: impl Into<String>,
192 command_trust: impl IntoIterator<Item = ParticipantCommandTrust>,
193 ) -> anyhow::Result<Self> {
194 Self::build(consumer, command_trust, B::default_backend()?)
195 }
196
197 pub fn with_datasource(
206 consumer: impl Into<String>,
207 command_trust: impl IntoIterator<Item = ParticipantCommandTrust>,
208 datasource: impl AsRef<str>,
209 ) -> anyhow::Result<Self> {
210 Self::build(
211 consumer,
212 command_trust,
213 B::backend_for(datasource.as_ref())?,
214 )
215 }
216
217 pub fn build(
226 consumer: impl Into<String>,
227 command_trust: impl IntoIterator<Item = ParticipantCommandTrust>,
228 backend: B,
229 ) -> anyhow::Result<Self> {
230 let consumer = consumer.into();
231 if consumer.is_empty()
232 || consumer.trim() != consumer
233 || consumer.len() > 128
234 || consumer.contains('\0')
235 || !consumer.bytes().all(|byte| {
236 byte.is_ascii_lowercase()
237 || byte.is_ascii_digit()
238 || matches!(byte, b'_' | b'-' | b'.')
239 })
240 {
241 anyhow::bail!("participant consumer must satisfy the Inbox key contract");
242 }
243 let mut trusted_command_producers = BTreeSet::new();
244 let mut projected_trust = BTreeMap::new();
245 for trust in command_trust {
246 trusted_command_producers.insert(trust.producer.clone());
247 let key = (
248 trust.workflow,
249 trust.definition_version,
250 trust.definition_digest,
251 );
252 if projected_trust.insert(key, trust.producer).is_some() {
253 anyhow::bail!(
254 "participant command trust contains a duplicate definition projection"
255 );
256 }
257 }
258 if projected_trust.is_empty() {
259 anyhow::bail!("participant requires at least one command trust projection");
260 }
261 Ok(Self {
262 backend,
263 consumer,
264 trusted_command_producers,
265 command_trust: projected_trust,
266 })
267 }
268
269 pub fn datasource_ref(&self) -> &natx_core::DatasourceRef {
275 self.backend.datasource_ref()
276 }
277
278 pub fn authenticate<'a>(
286 &'a self,
287 producer: &'a ServiceIdentity,
288 ) -> anyhow::Result<AuthenticatedParticipantRuntime<'a, B>> {
289 if !self.trusted_command_producers.contains(producer) {
290 return Err(crate::SagaCommandProcessingError::ProducerUnauthorized.into());
291 }
292 Ok(AuthenticatedParticipantRuntime {
293 runtime: self,
294 producer,
295 })
296 }
297
298 pub fn single_trusted_command_producer(&self) -> anyhow::Result<ServiceIdentity> {
307 if self.trusted_command_producers.len() != 1 {
308 anyhow::bail!(
309 "participant transport requires an explicit producer when trust contains multiple producers"
310 );
311 }
312 self.trusted_command_producers
313 .iter()
314 .next()
315 .cloned()
316 .ok_or_else(|| anyhow::anyhow!("participant command trust must not be empty"))
317 }
318
319 pub fn verify_authenticated_command(
330 &self,
331 envelope: &SagaCommandEnvelope,
332 producer: &ServiceIdentity,
333 ) -> anyhow::Result<()> {
334 self.verified_command_identity(envelope, producer).map(drop)
335 }
336
337 pub async fn handle_authenticated_execute<S>(
349 &self,
350 service: &S,
351 envelope: &SagaCommandEnvelope,
352 producer: &ServiceIdentity,
353 allow_unknown: bool,
354 ) -> anyhow::Result<ParticipantHandled>
355 where
356 S: SagaStep,
357 S::Command: DeserializeOwned,
358 {
359 self.handle_authenticated_execute_traced(service, envelope, producer, None, allow_unknown)
360 .await
361 }
362
363 pub async fn handle_authenticated_execute_traced<S>(
376 &self,
377 service: &S,
378 envelope: &SagaCommandEnvelope,
379 producer: &ServiceIdentity,
380 receipt_trace: Option<&TraceContext>,
381 allow_unknown: bool,
382 ) -> anyhow::Result<ParticipantHandled>
383 where
384 S: SagaStep,
385 S::Command: DeserializeOwned,
386 {
387 let identity = self.verified_command_identity(envelope, producer)?;
388 if identity.phase != StepPhase::Execute {
389 return Err(crate::SagaCommandProcessingError::ContractInvalid.into());
390 }
391 let _latency =
392 crate::latency::LatencyGuard::new(crate::latency::LatencyStage::ParticipantTransaction);
393 crate::transaction::run_for(&self.backend, async {
394 if matches!(
395 self.backend
396 .inbox()
397 .claim(&self.consumer, &envelope.command_id)
398 .await?,
399 InboxClaim::Duplicate
400 ) {
401 return Ok(ParticipantHandled::Duplicate);
402 }
403 let gate = ParticipantGateKey {
404 saga_id: &identity.saga_id,
405 step: &identity.step,
406 tenant: &identity.tenant,
407 workflow: &identity.workflow,
408 definition_version: identity.definition_version,
409 definition_digest: &envelope.definition_digest,
410 };
411 match self
412 .backend
413 .store()
414 .admit_execute(&gate, &identity.effect)
415 .await?
416 {
417 ExecuteAdmission::Suppressed => Ok(ParticipantHandled::Suppressed),
420 ExecuteAdmission::AlreadyTerminal(terminal) => {
423 let status = forward_to_attempt_status(terminal)?;
424 self.emit_result(receipt_trace, envelope, status, None, None)
425 .await?;
426 Ok(ParticipantHandled::Replayed(status))
427 }
428 ExecuteAdmission::Admitted => {
429 let context = self.context_of(&identity, envelope)?;
430 let command: S::Command = envelope.decode_payload()?;
431 match crate::latency::measure_handler(service.execute(&context, command)).await
432 {
433 Ok(SagaOutcome::Succeeded(_)) => {
434 self.backend
435 .store()
436 .settle_execute(
437 &identity.saga_id,
438 &identity.step,
439 StepForwardStatus::Succeeded,
440 )
441 .await?;
442 self.emit_result(
443 receipt_trace,
444 envelope,
445 StepAttemptStatus::Succeeded,
446 None,
447 None,
448 )
449 .await?;
450 Ok(ParticipantHandled::Executed(StepAttemptStatus::Succeeded))
451 }
452 Ok(SagaOutcome::Rejected {
453 code,
454 }) => {
455 self.backend
457 .store()
458 .settle_execute(
459 &identity.saga_id,
460 &identity.step,
461 StepForwardStatus::Rejected,
462 )
463 .await?;
464 self.emit_result(
465 receipt_trace,
466 envelope,
467 StepAttemptStatus::Rejected,
468 None,
469 Some(safe_reason_code(code)),
470 )
471 .await?;
472 Ok(ParticipantHandled::Executed(StepAttemptStatus::Rejected))
473 }
474 Ok(SagaOutcome::Unknown {
475 code,
476 }) => {
477 if allow_unknown {
478 self.backend
479 .store()
480 .settle_execute(
481 &identity.saga_id,
482 &identity.step,
483 StepForwardStatus::Unknown,
484 )
485 .await?;
486 self.emit_result(
487 receipt_trace,
488 envelope,
489 StepAttemptStatus::Unknown,
490 None,
491 Some(safe_reason_code(code)),
492 )
493 .await?;
494 Ok(ParticipantHandled::Executed(StepAttemptStatus::Unknown))
495 } else {
496 self.backend
499 .store()
500 .settle_execute(
501 &identity.saga_id,
502 &identity.step,
503 StepForwardStatus::Halted,
504 )
505 .await?;
506 self.emit_result(
507 receipt_trace,
508 envelope,
509 StepAttemptStatus::Halted,
510 None,
511 Some("unknown_not_allowed"),
512 )
513 .await?;
514 Ok(ParticipantHandled::Executed(StepAttemptStatus::Halted))
515 }
516 }
517 Ok(SagaOutcome::Halted {
518 code,
519 }) => {
520 self.backend
521 .store()
522 .settle_execute(
523 &identity.saga_id,
524 &identity.step,
525 StepForwardStatus::Halted,
526 )
527 .await?;
528 self.emit_result(
529 receipt_trace,
530 envelope,
531 StepAttemptStatus::Halted,
532 None,
533 Some(safe_reason_code(code)),
534 )
535 .await?;
536 Ok(ParticipantHandled::Executed(StepAttemptStatus::Halted))
537 }
538 Err(SagaExecutionError::Retryable {
540 code,
541 }) => {
542 anyhow::bail!("retryable execution error: {code}")
543 }
544 }
545 }
546 }
547 })
548 .await
549 }
550
551 pub async fn handle_authenticated_cancel_local(
560 &self,
561 envelope: &SagaCommandEnvelope,
562 producer: &ServiceIdentity,
563 ) -> anyhow::Result<ParticipantHandled> {
564 self.handle_authenticated_cancel_local_traced(envelope, producer, None)
565 .await
566 }
567
568 pub async fn handle_authenticated_cancel_local_traced(
578 &self,
579 envelope: &SagaCommandEnvelope,
580 producer: &ServiceIdentity,
581 receipt_trace: Option<&TraceContext>,
582 ) -> anyhow::Result<ParticipantHandled> {
583 let identity = self.verified_command_identity(envelope, producer)?;
584 if identity.phase != StepPhase::Cancel {
585 return Err(crate::SagaCommandProcessingError::ContractInvalid.into());
586 }
587 let _latency =
588 crate::latency::LatencyGuard::new(crate::latency::LatencyStage::ParticipantTransaction);
589 crate::transaction::run_for(&self.backend, async {
590 if matches!(
591 self.backend
592 .inbox()
593 .claim(&self.consumer, &envelope.command_id)
594 .await?,
595 InboxClaim::Duplicate
596 ) {
597 return Ok(ParticipantHandled::Duplicate);
598 }
599 let gate = ParticipantGateKey {
600 saga_id: &identity.saga_id,
601 step: &identity.step,
602 tenant: &identity.tenant,
603 workflow: &identity.workflow,
604 definition_version: identity.definition_version,
605 definition_digest: &envelope.definition_digest,
606 };
607 let execute_effect = EffectId::derive(
610 &identity.saga_id,
611 identity.definition_version,
612 &identity.step,
613 StepPhase::Execute,
614 );
615 let (status, terminal) = match self
616 .backend
617 .store()
618 .adjudicate_cancel(&gate, &execute_effect, &identity.effect)
619 .await?
620 {
621 CancelAdjudication::Confirmed => (StepAttemptStatus::CancelConfirmed, None),
622 CancelAdjudication::AlreadyTerminal(forward) => {
623 (StepAttemptStatus::AlreadyTerminal, Some(forward))
624 }
625 CancelAdjudication::ResolutionPending => {
626 (StepAttemptStatus::ResolutionPending, None)
627 }
628 };
629 self.emit_result(receipt_trace, envelope, status, terminal, None)
630 .await?;
631 Ok(ParticipantHandled::Executed(status))
632 })
633 .await
634 }
635
636 pub async fn handle_authenticated_cancel_external<C>(
650 &self,
651 service: &C,
652 envelope: &SagaCommandEnvelope,
653 producer: &ServiceIdentity,
654 ) -> anyhow::Result<ParticipantHandled>
655 where
656 C: SagaCancelStep,
657 C::Command: DeserializeOwned,
658 {
659 self.handle_authenticated_cancel_external_traced(service, envelope, producer, None)
660 .await
661 }
662
663 pub async fn handle_authenticated_cancel_external_traced<C>(
674 &self,
675 service: &C,
676 envelope: &SagaCommandEnvelope,
677 producer: &ServiceIdentity,
678 receipt_trace: Option<&TraceContext>,
679 ) -> anyhow::Result<ParticipantHandled>
680 where
681 C: SagaCancelStep,
682 C::Command: DeserializeOwned,
683 {
684 let identity = self.verified_command_identity(envelope, producer)?;
685 if identity.phase != StepPhase::Cancel {
686 return Err(crate::SagaCommandProcessingError::ContractInvalid.into());
687 }
688 let _latency =
689 crate::latency::LatencyGuard::new(crate::latency::LatencyStage::ParticipantTransaction);
690 crate::transaction::run_for(&self.backend, async {
691 if matches!(
692 self.backend
693 .inbox()
694 .claim(&self.consumer, &envelope.command_id)
695 .await?,
696 InboxClaim::Duplicate
697 ) {
698 return Ok(ParticipantHandled::Duplicate);
699 }
700 let gate = ParticipantGateKey {
701 saga_id: &identity.saga_id,
702 step: &identity.step,
703 tenant: &identity.tenant,
704 workflow: &identity.workflow,
705 definition_version: identity.definition_version,
706 definition_digest: &envelope.definition_digest,
707 };
708 let execute_effect = EffectId::derive(
709 &identity.saga_id,
710 identity.definition_version,
711 &identity.step,
712 StepPhase::Execute,
713 );
714 match self
715 .backend
716 .store()
717 .admit_external_cancel(&gate, &execute_effect, &identity.effect)
718 .await?
719 {
720 ExternalCancelAdmission::AlreadyAdjudicated(adjudication) => {
721 let (status, terminal) = cancel_adjudication_result(adjudication);
724 self.emit_result(receipt_trace, envelope, status, terminal, None)
725 .await?;
726 Ok(ParticipantHandled::Replayed(status))
727 }
728 ExternalCancelAdmission::Admitted => {
729 let context = self
732 .context_of(&identity, envelope)?
733 .with_target_phase(StepPhase::Execute);
734 let command: C::Command = envelope.decode_payload()?;
735 match crate::latency::measure_handler(service.cancel(&context, command)).await {
736 Ok(outcome) => {
737 let (forward, cancel, status, terminal, reason) =
738 external_cancel_result(outcome);
739 let reason = reason.map(safe_reason_code);
740 self.backend
743 .store()
744 .settle_external_cancel(
745 &identity.saga_id,
746 &identity.step,
747 forward,
748 cancel,
749 )
750 .await?;
751 self.emit_result(receipt_trace, envelope, status, terminal, reason)
752 .await?;
753 Ok(ParticipantHandled::Executed(status))
754 }
755 Err(SagaExecutionError::Retryable {
756 code,
757 }) => {
758 anyhow::bail!("retryable external cancel error: {code}")
761 }
762 }
763 }
764 }
765 })
766 .await
767 }
768
769 pub async fn handle_authenticated_compensate<S>(
780 &self,
781 service: &S,
782 envelope: &SagaCommandEnvelope,
783 producer: &ServiceIdentity,
784 ) -> anyhow::Result<ParticipantHandled>
785 where
786 S: SagaStep,
787 S::Compensation: DeserializeOwned,
788 {
789 self.handle_authenticated_compensate_traced(service, envelope, producer, None)
790 .await
791 }
792
793 pub async fn handle_authenticated_compensate_traced<S>(
804 &self,
805 service: &S,
806 envelope: &SagaCommandEnvelope,
807 producer: &ServiceIdentity,
808 receipt_trace: Option<&TraceContext>,
809 ) -> anyhow::Result<ParticipantHandled>
810 where
811 S: SagaStep,
812 S::Compensation: DeserializeOwned,
813 {
814 let identity = self.verified_command_identity(envelope, producer)?;
815 if identity.phase != StepPhase::Compensate {
816 return Err(crate::SagaCommandProcessingError::ContractInvalid.into());
817 }
818 let _latency =
819 crate::latency::LatencyGuard::new(crate::latency::LatencyStage::ParticipantTransaction);
820 crate::transaction::run_for(&self.backend, async {
821 if matches!(
822 self.backend
823 .inbox()
824 .claim(&self.consumer, &envelope.command_id)
825 .await?,
826 InboxClaim::Duplicate
827 ) {
828 return Ok(ParticipantHandled::Duplicate);
829 }
830 let gate = ParticipantGateKey {
831 saga_id: &identity.saga_id,
832 step: &identity.step,
833 tenant: &identity.tenant,
834 workflow: &identity.workflow,
835 definition_version: identity.definition_version,
836 definition_digest: &envelope.definition_digest,
837 };
838 match self
839 .backend
840 .store()
841 .admit_compensation(
842 &gate,
843 &identity.effect,
844 envelope.recovery_operation_id.as_deref(),
845 )
846 .await?
847 {
848 CompensationAdmission::AlreadySettled(terminal) => {
851 let status = compensation_to_attempt_status(terminal)?;
852 let reason = match terminal {
853 StepCompensationStatus::Unknown => Some("compensation_outcome_unknown"),
854 StepCompensationStatus::Halted => Some("compensation_already_halted"),
855 _ => None,
856 };
857 self.emit_result(receipt_trace, envelope, status, None, reason)
858 .await?;
859 Ok(ParticipantHandled::Replayed(status))
860 }
861 CompensationAdmission::MissingForwardEffect => {
864 self.emit_result(
865 receipt_trace,
866 envelope,
867 StepAttemptStatus::Halted,
868 None,
869 Some("compensation_contract_violation"),
870 )
871 .await?;
872 Ok(ParticipantHandled::ContractViolation)
873 }
874 CompensationAdmission::Admitted => {
875 let context = self.context_of(&identity, envelope)?;
876 let command: S::Compensation = envelope.decode_payload()?;
879 match crate::latency::measure_handler(service.compensate(&context, command))
880 .await
881 {
882 Ok(CompensationOutcome::Succeeded) => {
883 self.backend
884 .store()
885 .settle_compensation(
886 &identity.saga_id,
887 &identity.step,
888 StepCompensationStatus::Succeeded,
889 )
890 .await?;
891 self.emit_result(
892 receipt_trace,
893 envelope,
894 StepAttemptStatus::Succeeded,
895 None,
896 None,
897 )
898 .await?;
899 Ok(ParticipantHandled::Executed(StepAttemptStatus::Succeeded))
900 }
901 Ok(CompensationOutcome::Unknown {
902 code,
903 }) => {
904 self.backend
905 .store()
906 .settle_compensation(
907 &identity.saga_id,
908 &identity.step,
909 StepCompensationStatus::Unknown,
910 )
911 .await?;
912 self.emit_result(
913 receipt_trace,
914 envelope,
915 StepAttemptStatus::Unknown,
916 None,
917 Some(safe_reason_code(code)),
918 )
919 .await?;
920 Ok(ParticipantHandled::Executed(StepAttemptStatus::Unknown))
921 }
922 Ok(CompensationOutcome::Halted {
923 code,
924 }) => {
925 self.backend
926 .store()
927 .settle_compensation(
928 &identity.saga_id,
929 &identity.step,
930 StepCompensationStatus::Halted,
931 )
932 .await?;
933 self.emit_result(
934 receipt_trace,
935 envelope,
936 StepAttemptStatus::Halted,
937 None,
938 Some(safe_reason_code(code)),
939 )
940 .await?;
941 Ok(ParticipantHandled::Executed(StepAttemptStatus::Halted))
942 }
943 Err(SagaExecutionError::Retryable {
944 code,
945 }) => {
946 anyhow::bail!("retryable compensation error: {code}")
947 }
948 }
949 }
950 }
951 })
952 .await
953 }
954
955 pub async fn handle_authenticated_resolve<R>(
969 &self,
970 service: &R,
971 envelope: &SagaCommandEnvelope,
972 producer: &ServiceIdentity,
973 ) -> anyhow::Result<ParticipantHandled>
974 where
975 R: SagaResolveStep,
976 R::Command: DeserializeOwned,
977 {
978 self.handle_authenticated_resolve_traced(service, envelope, producer, None)
979 .await
980 }
981
982 pub async fn handle_authenticated_resolve_traced<R>(
993 &self,
994 service: &R,
995 envelope: &SagaCommandEnvelope,
996 producer: &ServiceIdentity,
997 receipt_trace: Option<&TraceContext>,
998 ) -> anyhow::Result<ParticipantHandled>
999 where
1000 R: SagaResolveStep,
1001 R::Command: DeserializeOwned,
1002 {
1003 let identity = self.verified_command_identity(envelope, producer)?;
1004 if identity.phase != StepPhase::Resolve {
1005 return Err(crate::SagaCommandProcessingError::ContractInvalid.into());
1006 }
1007 let _latency =
1008 crate::latency::LatencyGuard::new(crate::latency::LatencyStage::ParticipantTransaction);
1009 crate::transaction::run_for(&self.backend, async {
1010 if matches!(
1011 self.backend
1012 .inbox()
1013 .claim(&self.consumer, &envelope.command_id)
1014 .await?,
1015 InboxClaim::Duplicate
1016 ) {
1017 return Ok(ParticipantHandled::Duplicate);
1018 }
1019 let gate = ParticipantGateKey {
1020 saga_id: &identity.saga_id,
1021 step: &identity.step,
1022 tenant: &identity.tenant,
1023 workflow: &identity.workflow,
1024 definition_version: identity.definition_version,
1025 definition_digest: &envelope.definition_digest,
1026 };
1027 match self
1028 .backend
1029 .store()
1030 .admit_resolution(
1031 &gate,
1032 &identity.effect,
1033 envelope.recovery_operation_id.as_deref(),
1034 )
1035 .await?
1036 {
1037 ResolutionAdmission::AlreadySettled {
1038 status,
1039 ..
1040 } => {
1041 let attempt_status = resolution_to_attempt_status(status)?;
1042 self.emit_result(receipt_trace, envelope, attempt_status, None, None)
1043 .await?;
1044 Ok(ParticipantHandled::Replayed(attempt_status))
1045 }
1046 ResolutionAdmission::MissingUnknownEffect => {
1047 self.emit_result(
1050 receipt_trace,
1051 envelope,
1052 StepAttemptStatus::Halted,
1053 None,
1054 Some("resolution_contract_violation"),
1055 )
1056 .await?;
1057 Ok(ParticipantHandled::ContractViolation)
1058 }
1059 ResolutionAdmission::Admitted(target) => {
1060 let target_phase = match target {
1063 nasaga_backend::ResolutionTarget::Forward => StepPhase::Execute,
1064 nasaga_backend::ResolutionTarget::Compensation => StepPhase::Compensate,
1065 };
1066 let context = self
1067 .context_of(&identity, envelope)?
1068 .with_target_phase(target_phase);
1069 let command: R::Command = envelope.decode_payload()?;
1070 match crate::latency::measure_handler(service.resolve(&context, command)).await
1071 {
1072 Ok(SagaOutcome::Succeeded(_)) => {
1073 self.backend
1074 .store()
1075 .settle_resolution(
1076 &identity.saga_id,
1077 &identity.step,
1078 target,
1079 StepResolutionStatus::Succeeded,
1080 )
1081 .await?;
1082 self.emit_result(
1083 receipt_trace,
1084 envelope,
1085 StepAttemptStatus::Succeeded,
1086 None,
1087 None,
1088 )
1089 .await?;
1090 Ok(ParticipantHandled::Executed(StepAttemptStatus::Succeeded))
1091 }
1092 Ok(SagaOutcome::Rejected {
1093 code,
1094 }) => {
1095 self.backend
1098 .store()
1099 .settle_resolution(
1100 &identity.saga_id,
1101 &identity.step,
1102 target,
1103 StepResolutionStatus::Rejected,
1104 )
1105 .await?;
1106 self.emit_result(
1107 receipt_trace,
1108 envelope,
1109 StepAttemptStatus::Rejected,
1110 None,
1111 Some(safe_reason_code(code)),
1112 )
1113 .await?;
1114 Ok(ParticipantHandled::Executed(StepAttemptStatus::Rejected))
1115 }
1116 Ok(SagaOutcome::Unknown {
1117 code,
1118 }) => {
1119 self.backend
1120 .store()
1121 .settle_resolution(
1122 &identity.saga_id,
1123 &identity.step,
1124 target,
1125 StepResolutionStatus::Pending,
1126 )
1127 .await?;
1128 self.emit_result(
1129 receipt_trace,
1130 envelope,
1131 StepAttemptStatus::Unknown,
1132 None,
1133 Some(safe_reason_code(code)),
1134 )
1135 .await?;
1136 Ok(ParticipantHandled::Executed(StepAttemptStatus::Unknown))
1137 }
1138 Ok(SagaOutcome::Halted {
1139 code,
1140 }) => {
1141 self.backend
1142 .store()
1143 .settle_resolution(
1144 &identity.saga_id,
1145 &identity.step,
1146 target,
1147 StepResolutionStatus::Halted,
1148 )
1149 .await?;
1150 self.emit_result(
1151 receipt_trace,
1152 envelope,
1153 StepAttemptStatus::Halted,
1154 None,
1155 Some(safe_reason_code(code)),
1156 )
1157 .await?;
1158 Ok(ParticipantHandled::Executed(StepAttemptStatus::Halted))
1159 }
1160 Err(SagaExecutionError::Retryable {
1161 code,
1162 }) => {
1163 anyhow::bail!("retryable resolution error: {code}")
1165 }
1166 }
1167 }
1168 }
1169 })
1170 .await
1171 }
1172
1173 fn verified_command_identity(
1184 &self,
1185 envelope: &SagaCommandEnvelope,
1186 producer: &ServiceIdentity,
1187 ) -> anyhow::Result<crate::envelope::VerifiedIdentity> {
1188 let identity = envelope
1189 .verified()
1190 .map_err(|_| crate::SagaCommandProcessingError::IdentityInvalid)?;
1191 self.verify_command_producer(producer, &identity)?;
1192 let descriptor = crate::COLLECTED_SAGA_STEPS.iter().find(|descriptor| {
1194 descriptor.workflow == envelope.workflow
1195 && descriptor.definition_version == envelope.definition_version
1196 && descriptor.step == envelope.step
1197 });
1198 let (content_type, schema_id) = descriptor.map_or(("application/json", ""), |descriptor| {
1199 (
1200 descriptor.payload_content_type,
1201 descriptor.payload_schema_id,
1202 )
1203 });
1204 envelope.verify_payload_contract(content_type, schema_id)?;
1205 Ok(identity)
1206 }
1207
1208 fn verify_command_producer(
1216 &self,
1217 producer: &ServiceIdentity,
1218 identity: &crate::envelope::VerifiedIdentity,
1219 ) -> anyhow::Result<()> {
1220 let key = (
1221 identity.workflow.clone(),
1222 identity.definition_version,
1223 Some(identity.definition_digest.clone()),
1224 );
1225 let version_key = (identity.workflow.clone(), identity.definition_version, None);
1226 if self.command_trust.get(&key) != Some(producer)
1227 && self.command_trust.get(&version_key) != Some(producer)
1228 {
1229 return Err(crate::SagaCommandProcessingError::ProducerUnauthorized.into());
1230 }
1231 Ok(())
1232 }
1233
1234 fn context_of(
1242 &self,
1243 identity: &crate::envelope::VerifiedIdentity,
1244 envelope: &SagaCommandEnvelope,
1245 ) -> anyhow::Result<SagaContext> {
1246 Ok(SagaContext::new(
1247 identity.saga_id.clone(),
1248 identity.tenant.clone(),
1249 identity.workflow.clone(),
1250 identity.definition_version,
1251 envelope.definition_digest.clone(),
1252 identity.step.clone(),
1253 identity.phase,
1254 identity.attempt,
1255 )
1256 .with_payload(envelope.business_payload()?))
1257 }
1258
1259 async fn emit_result(
1274 &self,
1275 receipt_trace: Option<&TraceContext>,
1276 envelope: &SagaCommandEnvelope,
1277 status: StepAttemptStatus,
1278 terminal: Option<StepForwardStatus>,
1279 reason_code: Option<&str>,
1280 ) -> anyhow::Result<()> {
1281 let result = SagaResultEnvelope {
1282 event_id: derive_result_event_id(&envelope.command_id),
1283 saga_id: envelope.saga_id.clone(),
1284 tenant_id: envelope.tenant_id.clone(),
1285 workflow: envelope.workflow.clone(),
1286 definition_version: envelope.definition_version,
1287 definition_digest: envelope.definition_digest.clone(),
1288 step: envelope.step.clone(),
1289 phase: envelope.phase.clone(),
1290 attempt: envelope.attempt,
1291 effect_id: envelope.effect_id.clone(),
1292 command_id: envelope.command_id.clone(),
1293 status: status.as_str().to_string(),
1294 terminal_status: terminal.map(|value| value.as_str().to_string()),
1295 reason_code: reason_code.map(str::to_string),
1296 };
1297 let event = result
1301 .to_outbox_event()
1302 .map_err(|_| crate::SagaCommandProcessingError::ContractInvalid)?;
1303 let event = match receipt_trace {
1306 Some(trace) => {
1307 event.with_traceparent(trace.child(natelemetry::random_span_id()).to_traceparent())
1308 }
1309 None => event,
1310 };
1311 let write_context = naoutbox_core::OutboxWriteContext::new(envelope.tenant_id.as_str())
1314 .map_err(|_| crate::SagaCommandProcessingError::ContractInvalid)?;
1315 self.backend
1316 .outbox()
1317 .append_transactional_with_context(&write_context, &event)
1318 .await?;
1319 Ok(())
1320 }
1321}
1322
1323impl<B> AuthenticatedParticipantRuntime<'_, B>
1324where
1325 B: SagaBackend + SagaBackendFactory,
1326{
1327 pub async fn execute<S>(
1336 &self,
1337 service: &S,
1338 envelope: &SagaCommandEnvelope,
1339 allow_unknown: bool,
1340 ) -> anyhow::Result<ParticipantHandled>
1341 where
1342 S: SagaStep,
1343 S::Command: DeserializeOwned,
1344 {
1345 self.runtime
1346 .handle_authenticated_execute(service, envelope, self.producer, allow_unknown)
1347 .await
1348 }
1349
1350 pub async fn cancel_local(
1357 &self,
1358 envelope: &SagaCommandEnvelope,
1359 ) -> anyhow::Result<ParticipantHandled> {
1360 self.runtime
1361 .handle_authenticated_cancel_local(envelope, self.producer)
1362 .await
1363 }
1364
1365 pub async fn cancel_external<C>(
1373 &self,
1374 service: &C,
1375 envelope: &SagaCommandEnvelope,
1376 ) -> anyhow::Result<ParticipantHandled>
1377 where
1378 C: SagaCancelStep,
1379 C::Command: DeserializeOwned,
1380 {
1381 self.runtime
1382 .handle_authenticated_cancel_external(service, envelope, self.producer)
1383 .await
1384 }
1385
1386 pub async fn compensate<S>(
1394 &self,
1395 service: &S,
1396 envelope: &SagaCommandEnvelope,
1397 ) -> anyhow::Result<ParticipantHandled>
1398 where
1399 S: SagaStep,
1400 S::Compensation: DeserializeOwned,
1401 {
1402 self.runtime
1403 .handle_authenticated_compensate(service, envelope, self.producer)
1404 .await
1405 }
1406
1407 pub async fn resolve<R>(
1415 &self,
1416 service: &R,
1417 envelope: &SagaCommandEnvelope,
1418 ) -> anyhow::Result<ParticipantHandled>
1419 where
1420 R: SagaResolveStep,
1421 R::Command: DeserializeOwned,
1422 {
1423 self.runtime
1424 .handle_authenticated_resolve(service, envelope, self.producer)
1425 .await
1426 }
1427}
1428
1429fn valid_definition_digest(digest: &str) -> bool {
1436 digest.len() == 64
1437 && digest
1438 .bytes()
1439 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1440}
1441
1442fn safe_reason_code(code: &'static str) -> &'static str {
1450 if valid_reason_code(code) {
1451 code
1452 } else {
1453 tracing::error!(
1454 reason_code_valid = false,
1455 "Saga 参与方原因码不符合传输合同,已安全降级并保留业务裁决"
1456 );
1457 "participant_reason_code_invalid"
1458 }
1459}
1460
1461fn forward_to_attempt_status(terminal: StepForwardStatus) -> anyhow::Result<StepAttemptStatus> {
1468 match terminal {
1469 StepForwardStatus::Succeeded => Ok(StepAttemptStatus::Succeeded),
1470 StepForwardStatus::Rejected => Ok(StepAttemptStatus::Rejected),
1471 StepForwardStatus::Unknown => Ok(StepAttemptStatus::Unknown),
1472 StepForwardStatus::Halted => Ok(StepAttemptStatus::Halted),
1473 _ => anyhow::bail!("gate terminal status is not replayable"),
1474 }
1475}
1476
1477fn cancel_adjudication_result(
1484 adjudication: CancelAdjudication,
1485) -> (StepAttemptStatus, Option<StepForwardStatus>) {
1486 match adjudication {
1487 CancelAdjudication::Confirmed => (StepAttemptStatus::CancelConfirmed, None),
1488 CancelAdjudication::AlreadyTerminal(forward) => {
1489 (StepAttemptStatus::AlreadyTerminal, Some(forward))
1490 }
1491 CancelAdjudication::ResolutionPending => (StepAttemptStatus::ResolutionPending, None),
1492 }
1493}
1494
1495#[allow(clippy::type_complexity)]
1502fn external_cancel_result(
1503 outcome: CancelOutcome,
1504) -> (
1505 StepForwardStatus,
1506 StepCancelStatus,
1507 StepAttemptStatus,
1508 Option<StepForwardStatus>,
1509 Option<&'static str>,
1510) {
1511 match outcome {
1512 CancelOutcome::CancelConfirmed => (
1513 StepForwardStatus::Cancelled,
1514 StepCancelStatus::Confirmed,
1515 StepAttemptStatus::CancelConfirmed,
1516 None,
1517 None,
1518 ),
1519 CancelOutcome::AlreadyTerminal(terminal) => {
1520 let (forward, reason) = match terminal {
1521 TerminalForwardOutcome::Succeeded => (StepForwardStatus::Succeeded, None),
1522 TerminalForwardOutcome::Rejected {
1523 code,
1524 } => (StepForwardStatus::Rejected, Some(code)),
1525 TerminalForwardOutcome::Halted {
1526 code,
1527 } => (StepForwardStatus::Halted, Some(code)),
1528 };
1529 (
1530 forward,
1531 StepCancelStatus::AlreadyTerminal,
1532 StepAttemptStatus::AlreadyTerminal,
1533 Some(forward),
1534 reason,
1535 )
1536 }
1537 CancelOutcome::ResolutionPending {
1538 code,
1539 } => (
1540 StepForwardStatus::Unknown,
1541 StepCancelStatus::ResolutionPending,
1542 StepAttemptStatus::ResolutionPending,
1543 None,
1544 Some(code),
1545 ),
1546 }
1547}
1548
1549fn compensation_to_attempt_status(
1557 terminal: StepCompensationStatus,
1558) -> anyhow::Result<StepAttemptStatus> {
1559 match terminal {
1560 StepCompensationStatus::Succeeded => Ok(StepAttemptStatus::Succeeded),
1561 StepCompensationStatus::Unknown => Ok(StepAttemptStatus::Unknown),
1562 StepCompensationStatus::Halted => Ok(StepAttemptStatus::Halted),
1563 StepCompensationStatus::None | StepCompensationStatus::Pending => {
1564 anyhow::bail!("compensation gate status is not replayable")
1565 }
1566 }
1567}
1568
1569fn resolution_to_attempt_status(status: StepResolutionStatus) -> anyhow::Result<StepAttemptStatus> {
1577 match status {
1578 StepResolutionStatus::Succeeded => Ok(StepAttemptStatus::Succeeded),
1579 StepResolutionStatus::Rejected => Ok(StepAttemptStatus::Rejected),
1580 StepResolutionStatus::Halted => Ok(StepAttemptStatus::Halted),
1581 StepResolutionStatus::None | StepResolutionStatus::Pending => {
1582 anyhow::bail!("resolution gate status is not replayable")
1583 }
1584 }
1585}