use super::*;
use obzenflow_core::event::observability::ObservationRecorder;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::Instant;
#[derive(Debug, Clone)]
pub struct EffectIdentity {
pub effect_type: &'static str,
pub safety: EffectSafety,
pub cursor: EffectCursor,
pub idempotency_key: Option<IdempotencyKey>,
}
type EffectCall = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send>,
>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysicalCallOutcome {
Succeeded,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysicalCallObservation {
Prepared,
Started {
dependency_elapsed: Duration,
},
Completed {
outcome: PhysicalCallOutcome,
dependency_elapsed: Duration,
},
}
#[derive(Debug)]
enum PhysicalCallState {
Prepared,
Started {
at: Instant,
},
Completed {
outcome: PhysicalCallOutcome,
dependency_elapsed: Duration,
},
}
#[derive(Debug, Clone)]
pub struct PhysicalCallReceipt(Arc<Mutex<PhysicalCallState>>);
impl PhysicalCallReceipt {
fn new() -> Self {
Self(Arc::new(Mutex::new(PhysicalCallState::Prepared)))
}
pub fn observation(&self) -> PhysicalCallObservation {
let state = self
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match *state {
PhysicalCallState::Prepared => PhysicalCallObservation::Prepared,
PhysicalCallState::Started { at } => PhysicalCallObservation::Started {
dependency_elapsed: at.elapsed(),
},
PhysicalCallState::Completed {
outcome,
dependency_elapsed,
} => PhysicalCallObservation::Completed {
outcome,
dependency_elapsed,
},
}
}
}
#[derive(Clone)]
pub(crate) struct PhysicalCallLifecycle {
receipt: PhysicalCallReceipt,
}
impl PhysicalCallLifecycle {
pub(crate) fn mark_started(&self) {
let mut state = self
.receipt
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if matches!(*state, PhysicalCallState::Prepared) {
*state = PhysicalCallState::Started { at: Instant::now() };
}
}
pub(crate) fn mark_completed(&self, outcome: PhysicalCallOutcome) {
let mut state = self
.receipt
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let PhysicalCallState::Started { at } = *state else {
return;
};
*state = PhysicalCallState::Completed {
outcome,
dependency_elapsed: at.elapsed(),
};
}
}
pub struct PreparedRepeatableEffectCall {
call: EffectCall,
receipt: PhysicalCallReceipt,
}
impl PreparedRepeatableEffectCall {
pub fn receipt(&self) -> PhysicalCallReceipt {
self.receipt.clone()
}
pub async fn execute(self) -> Result<Vec<ChainEvent>, EffectError> {
self.call.await
}
}
pub struct RepeatableEffectOperation {
call: Box<dyn FnMut(PhysicalCallLifecycle) -> EffectCall + Send>,
}
impl RepeatableEffectOperation {
pub fn new<F, Fut>(mut call: F) -> Self
where
F: FnMut() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send + 'static,
{
Self::new_with_lifecycle(move |lifecycle| {
let future = call();
async move {
lifecycle.mark_started();
let result = future.await;
lifecycle.mark_completed(if result.is_ok() {
PhysicalCallOutcome::Succeeded
} else {
PhysicalCallOutcome::Failed
});
result
}
})
}
pub(crate) fn new_with_lifecycle<F, Fut>(mut call: F) -> Self
where
F: FnMut(PhysicalCallLifecycle) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send + 'static,
{
Self {
call: Box::new(move |lifecycle| Box::pin(call(lifecycle))),
}
}
pub fn prepare(&mut self) -> PreparedRepeatableEffectCall {
let receipt = PhysicalCallReceipt::new();
let lifecycle = PhysicalCallLifecycle {
receipt: receipt.clone(),
};
PreparedRepeatableEffectCall {
call: (self.call)(lifecycle),
receipt,
}
}
pub async fn execute(&mut self) -> Result<Vec<ChainEvent>, EffectError> {
self.prepare().execute().await
}
}
pub struct SingleUseEffectOperation {
call: Box<dyn FnOnce(PhysicalCallLifecycle) -> EffectCall + Send>,
provenance: SingleUseEffectProvenance,
}
pub struct PreparedSingleUseEffectCall {
call: EffectCall,
provenance: SingleUseEffectProvenance,
receipt: PhysicalCallReceipt,
}
impl PreparedSingleUseEffectCall {
pub fn receipt(&self) -> PhysicalCallReceipt {
self.receipt.clone()
}
pub async fn execute(self) -> SingleUseEffectExecution {
let Self {
call, provenance, ..
} = self;
SingleUseEffectExecution {
result: call.await,
provenance,
}
}
}
impl SingleUseEffectOperation {
#[cfg(test)]
pub(super) fn new<F, Fut>(call: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send + 'static,
{
Self::new_with_lifecycle(move |lifecycle| async move {
lifecycle.mark_started();
let result = call().await;
lifecycle.mark_completed(if result.is_ok() {
PhysicalCallOutcome::Succeeded
} else {
PhysicalCallOutcome::Failed
});
result
})
}
pub(super) fn new_with_lifecycle<F, Fut>(call: F) -> Self
where
F: FnOnce(PhysicalCallLifecycle) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send + 'static,
{
Self {
call: Box::new(move |lifecycle| Box::pin(call(lifecycle))),
provenance: SingleUseEffectProvenance::new(),
}
}
pub(super) fn provenance(&self) -> SingleUseEffectProvenance {
self.provenance.clone()
}
pub async fn execute(self) -> SingleUseEffectExecution {
self.prepare().execute().await
}
pub fn prepare(self) -> PreparedSingleUseEffectCall {
let Self { call, provenance } = self;
let receipt = PhysicalCallReceipt::new();
let lifecycle = PhysicalCallLifecycle {
receipt: receipt.clone(),
};
PreparedSingleUseEffectCall {
call: call(lifecycle),
provenance,
receipt,
}
}
pub fn abort(
self,
reason: EffectAbortReason,
control_events: Vec<ChainEvent>,
) -> SingleUseEffectBoundaryReport {
let Self { call, provenance } = self;
drop(call);
SingleUseEffectBoundaryReport {
outcome: SingleUseEffectBoundaryOutcome::Aborted(reason),
control_events,
provenance,
}
}
}
#[derive(Clone)]
pub(super) struct SingleUseEffectProvenance(Arc<SingleUseEffectProvenanceMarker>);
struct SingleUseEffectProvenanceMarker;
impl SingleUseEffectProvenance {
fn new() -> Self {
Self(Arc::new(SingleUseEffectProvenanceMarker))
}
fn matches(&self, expected: &Self) -> bool {
Arc::ptr_eq(&self.0, &expected.0)
}
}
pub struct SingleUseEffectExecution {
result: Result<Vec<ChainEvent>, EffectError>,
provenance: SingleUseEffectProvenance,
}
impl SingleUseEffectExecution {
pub fn result(&self) -> &Result<Vec<ChainEvent>, EffectError> {
&self.result
}
pub fn into_report(self, control_events: Vec<ChainEvent>) -> SingleUseEffectBoundaryReport {
let provenance = self.provenance.clone();
SingleUseEffectBoundaryReport {
outcome: SingleUseEffectBoundaryOutcome::Executed(self),
control_events,
provenance,
}
}
}
#[derive(Debug, Clone)]
pub struct EffectAbortReason {
pub cause: EffectFailureCause,
pub message: String,
pub retry: RetryDisposition,
}
pub enum EffectBoundaryOutcome {
Executed(Result<Vec<ChainEvent>, EffectError>),
Aborted(EffectAbortReason),
}
pub struct EffectBoundaryReport {
pub outcome: EffectBoundaryOutcome,
pub control_events: Vec<ChainEvent>,
}
pub(crate) enum SingleUseEffectBoundaryOutcome {
Executed(SingleUseEffectExecution),
Aborted(EffectAbortReason),
}
pub struct SingleUseEffectBoundaryReport {
outcome: SingleUseEffectBoundaryOutcome,
control_events: Vec<ChainEvent>,
provenance: SingleUseEffectProvenance,
}
impl SingleUseEffectBoundaryReport {
pub fn execution_result(&self) -> Option<&Result<Vec<ChainEvent>, EffectError>> {
match &self.outcome {
SingleUseEffectBoundaryOutcome::Executed(execution) => Some(execution.result()),
SingleUseEffectBoundaryOutcome::Aborted(_) => None,
}
}
pub fn abort_reason(&self) -> Option<&EffectAbortReason> {
match &self.outcome {
SingleUseEffectBoundaryOutcome::Aborted(reason) => Some(reason),
SingleUseEffectBoundaryOutcome::Executed(_) => None,
}
}
pub fn extend_control_events(&mut self, events: impl IntoIterator<Item = ChainEvent>) {
self.control_events.extend(events);
}
pub(super) fn into_parts(
self,
expected: &SingleUseEffectProvenance,
) -> Result<(SingleUseEffectBoundaryOutcome, Vec<ChainEvent>), EffectError> {
if !self.provenance.matches(expected) {
return Err(EffectError::EffectProvenanceMismatch(
"single-use effect boundary returned a report for a different operation"
.to_string(),
));
}
Ok((self.outcome, self.control_events))
}
}
pub struct AffineEffectOperation {
inner: SingleUseEffectOperation,
highest_prior_attempt: u32,
}
pub struct PreparedAffineEffectCall {
inner: PreparedSingleUseEffectCall,
highest_prior_attempt: u32,
}
impl PreparedAffineEffectCall {
pub fn receipt(&self) -> PhysicalCallReceipt {
self.inner.receipt()
}
pub async fn execute(self) -> AffineEffectExecution {
AffineEffectExecution {
inner: self.inner.execute().await,
highest_prior_attempt: self.highest_prior_attempt,
}
}
}
impl AffineEffectOperation {
pub(super) fn new_with_lifecycle<F, Fut>(highest_prior_attempt: u32, call: F) -> Self
where
F: FnOnce(PhysicalCallLifecycle) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Vec<ChainEvent>, EffectError>> + Send + 'static,
{
Self {
inner: SingleUseEffectOperation::new_with_lifecycle(call),
highest_prior_attempt,
}
}
pub fn highest_prior_attempt(&self) -> u32 {
self.highest_prior_attempt
}
pub fn next_attempt(&self) -> u32 {
self.highest_prior_attempt.saturating_add(1)
}
pub(super) fn provenance(&self) -> SingleUseEffectProvenance {
self.inner.provenance()
}
pub fn prepare(self) -> PreparedAffineEffectCall {
PreparedAffineEffectCall {
inner: self.inner.prepare(),
highest_prior_attempt: self.highest_prior_attempt,
}
}
pub async fn execute(self) -> AffineEffectExecution {
self.prepare().execute().await
}
pub fn abort(
self,
reason: EffectAbortReason,
control_events: Vec<ChainEvent>,
) -> AffineEffectBoundaryReport {
AffineEffectBoundaryReport {
inner: self.inner.abort(reason, control_events),
highest_prior_attempt: self.highest_prior_attempt,
}
}
}
pub struct AffineEffectExecution {
inner: SingleUseEffectExecution,
highest_prior_attempt: u32,
}
impl AffineEffectExecution {
pub fn result(&self) -> &Result<Vec<ChainEvent>, EffectError> {
self.inner.result()
}
pub fn attempt(&self) -> u32 {
self.highest_prior_attempt.saturating_add(1)
}
pub fn into_report(self, control_events: Vec<ChainEvent>) -> AffineEffectBoundaryReport {
let highest_prior_attempt = self.highest_prior_attempt;
AffineEffectBoundaryReport {
inner: self.inner.into_report(control_events),
highest_prior_attempt,
}
}
}
pub struct AffineEffectBoundaryReport {
inner: SingleUseEffectBoundaryReport,
highest_prior_attempt: u32,
}
impl AffineEffectBoundaryReport {
pub fn execution_result(&self) -> Option<&Result<Vec<ChainEvent>, EffectError>> {
self.inner.execution_result()
}
pub fn abort_reason(&self) -> Option<&EffectAbortReason> {
self.inner.abort_reason()
}
pub fn highest_prior_attempt(&self) -> u32 {
self.highest_prior_attempt
}
pub fn extend_control_events(&mut self, events: impl IntoIterator<Item = ChainEvent>) {
self.inner.extend_control_events(events);
}
pub(super) fn into_parts(
self,
expected: &SingleUseEffectProvenance,
) -> Result<(SingleUseEffectBoundaryOutcome, Vec<ChainEvent>), EffectError> {
self.inner.into_parts(expected)
}
}
#[async_trait]
pub trait EffectBoundary: Send + Sync {
fn install_observation_recorder(&self, _recorder: Arc<dyn ObservationRecorder>) {}
async fn around_repeatable_effect(
&self,
identity: &EffectIdentity,
event: &ChainEvent,
operation: RepeatableEffectOperation,
) -> EffectBoundaryReport;
async fn around_single_use_effect(
&self,
identity: &EffectIdentity,
event: &ChainEvent,
operation: SingleUseEffectOperation,
) -> SingleUseEffectBoundaryReport;
async fn around_affine_effect(
&self,
_identity: &EffectIdentity,
_event: &ChainEvent,
operation: AffineEffectOperation,
) -> AffineEffectBoundaryReport {
operation.execute().await.into_report(Vec::new())
}
}
#[cfg(test)]
mod lifecycle_tests {
use super::*;
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn repeatable_receipt_excludes_post_dependency_materialisation() {
let mut operation = RepeatableEffectOperation::new_with_lifecycle(|lifecycle| async move {
lifecycle.mark_started();
tokio::time::sleep(Duration::from_millis(25)).await;
lifecycle.mark_completed(PhysicalCallOutcome::Succeeded);
tokio::time::sleep(Duration::from_millis(75)).await;
Err(EffectError::Serialization(
"outcome decomposition failed".to_string(),
))
});
let prepared = operation.prepare();
let receipt = prepared.receipt();
assert_eq!(receipt.observation(), PhysicalCallObservation::Prepared);
assert!(prepared.execute().await.is_err());
assert_eq!(
receipt.observation(),
PhysicalCallObservation::Completed {
outcome: PhysicalCallOutcome::Succeeded,
dependency_elapsed: Duration::from_millis(25),
}
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn transactional_receipt_times_the_whole_single_use_envelope() {
let operation = SingleUseEffectOperation::new_with_lifecycle(|lifecycle| async move {
lifecycle.mark_started();
tokio::time::sleep(Duration::from_millis(40)).await;
lifecycle.mark_completed(PhysicalCallOutcome::Failed);
Err(EffectError::Transport("commit failed".to_string()))
});
let prepared = operation.prepare();
let receipt = prepared.receipt();
let execution = prepared.execute().await;
assert!(execution.result().is_err());
assert_eq!(
receipt.observation(),
PhysicalCallObservation::Completed {
outcome: PhysicalCallOutcome::Failed,
dependency_elapsed: Duration::from_millis(40),
}
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn dropped_in_flight_call_leaves_an_observable_started_receipt() {
let mut operation = RepeatableEffectOperation::new_with_lifecycle(|lifecycle| async move {
lifecycle.mark_started();
std::future::pending::<Result<Vec<ChainEvent>, EffectError>>().await
});
let prepared = operation.prepare();
let receipt = prepared.receipt();
let task = tokio::spawn(prepared.execute());
tokio::task::yield_now().await;
assert!(matches!(
receipt.observation(),
PhysicalCallObservation::Started { .. }
));
task.abort();
let _ = task.await;
assert!(matches!(
receipt.observation(),
PhysicalCallObservation::Started { .. }
));
}
}