use std::any::Any;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::compensation::{
CompensationContext, CompensationOutcome, CompensationSpec, Compensator,
};
use crate::effect::{EffectContext, EffectFailure, EffectOutcome, EffectSpec, Precondition};
use crate::error::RuntimeError;
use crate::id::{EffectKey, EffectName, LogicalKey};
use crate::kind::EffectKind;
use crate::policy::{Capabilities, RiskLevel};
use crate::retry::RetryPolicy;
use crate::runtime::Runtime;
use crate::state::EffectStatus;
use crate::store::{EffectRecord, EffectStore, StoreError};
use crate::verification::{NoVerification, Verification, VerificationMode, VerifyWith};
pub trait EffectHandler: Send + Sync + 'static {
const NAME: &'static str;
type Input: Serialize + DeserializeOwned + Send + Sync + 'static;
type Output: Serialize + DeserializeOwned + Send + 'static;
type Error: Into<EffectFailure> + Send + 'static;
fn kind(&self) -> EffectKind {
EffectKind::IrreversibleWrite
}
fn remote_idempotency(&self) -> bool {
false
}
fn retry_policy(&self) -> Option<RetryPolicy> {
None
}
fn attempt_timeout(&self) -> Option<Duration> {
None
}
fn risk(&self) -> RiskLevel {
RiskLevel::Low
}
fn requires_approval(&self) -> bool {
false
}
fn precondition(
&self,
ctx: &EffectContext,
input: &Self::Input,
) -> impl Future<Output = Precondition> + Send {
let _ = (ctx, input);
async { Precondition::Satisfied }
}
fn execute(
&self,
ctx: &EffectContext,
input: &Self::Input,
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
}
pub trait VerifiableEffect: EffectHandler {
fn verification_mode(&self) -> VerificationMode {
VerificationMode::Authoritative
}
fn verify(
&self,
ctx: &EffectContext,
input: &Self::Input,
) -> impl Future<Output = Result<Verification<Self::Output>, Self::Error>> + Send;
}
pub trait CompensableEffect: EffectHandler {
fn compensate(
&self,
ctx: &CompensationContext,
input: &Self::Input,
output: Option<&Self::Output>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
type VerifyFn<H> = Arc<
dyn Fn(
Arc<H>,
EffectContext,
Arc<<H as EffectHandler>::Input>,
) -> BoxFuture<Result<Verification<<H as EffectHandler>::Output>, EffectFailure>>
+ Send
+ Sync,
>;
pub struct Handler<H: EffectHandler> {
effect: Arc<H>,
verify: Option<(VerificationMode, VerifyFn<H>)>,
compensate: Option<Compensator>,
}
impl<H: EffectHandler> Clone for Handler<H> {
fn clone(&self) -> Self {
Self {
effect: Arc::clone(&self.effect),
verify: self.verify.clone(),
compensate: self.compensate.clone(),
}
}
}
impl<H: EffectHandler> Handler<H> {
pub fn new(handler: H) -> Self {
Self {
effect: Arc::new(handler),
verify: None,
compensate: None,
}
}
}
impl<H: CompensableEffect> Handler<H> {
#[must_use]
pub fn compensable(mut self) -> Self {
let handler = Arc::clone(&self.effect);
self.compensate = Some(Arc::new(move |ctx, input, output| {
let handler = Arc::clone(&handler);
Box::pin(async move {
let input: H::Input = serde_json::from_value(input.unwrap_or(Value::Null))
.map_err(|e| {
EffectFailure::permanent(format!("stored input does not match: {e}"))
})?;
let output: Option<H::Output> = output
.map(serde_json::from_value)
.transpose()
.map_err(|e| {
EffectFailure::permanent(format!("stored output does not match: {e}"))
})?;
handler
.compensate(&ctx, &input, output.as_ref())
.await
.map_err(Into::into)
})
}));
self
}
}
impl<H: VerifiableEffect> Handler<H> {
#[must_use]
pub fn verifiable(mut self) -> Self {
let mode = self.effect.verification_mode();
let verify: VerifyFn<H> = Arc::new(|handler, ctx, input| {
Box::pin(async move { handler.verify(&ctx, &input).await.map_err(Into::into) })
});
self.verify = Some((mode, verify));
self
}
}
pub(crate) struct Registered<S> {
typed: Arc<dyn Any + Send + Sync>,
pub(crate) resume: Resume<S>,
}
pub(crate) type Resume<S> = Arc<
dyn Fn(Runtime<S>, EffectRecord) -> BoxFuture<Result<EffectStatus, RuntimeError>> + Send + Sync,
>;
pub(crate) type Registry<S> = HashMap<&'static str, Registered<S>>;
impl<S: EffectStore> Registered<S> {
pub(crate) fn new<H: EffectHandler>(handler: Handler<H>) -> Self {
let typed: Arc<dyn Any + Send + Sync> = Arc::new(handler.clone());
let resume: Resume<S> = Arc::new(move |runtime: Runtime<S>, record: EffectRecord| {
let handler = handler.clone();
Box::pin(async move { resume(&runtime, &handler, record).await })
});
Self { typed, resume }
}
pub(crate) fn typed<H: EffectHandler>(&self) -> Option<Handler<H>> {
self.typed.downcast_ref::<Handler<H>>().cloned()
}
}
#[must_use = "a submission does nothing until it is awaited"]
pub struct Submission<'a, S, H: EffectHandler> {
runtime: &'a Runtime<S>,
key: String,
input: H::Input,
actor: Option<String>,
}
impl<'a, S, H: EffectHandler> Submission<'a, S, H> {
pub(crate) fn new(runtime: &'a Runtime<S>, key: impl Display, input: H::Input) -> Self {
Self {
runtime,
key: key.to_string(),
input,
actor: None,
}
}
pub fn actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
}
impl<'a, S: EffectStore, H: EffectHandler> IntoFuture for Submission<'a, S, H> {
type Output = Result<EffectOutcome<H::Output>, RuntimeError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let handler = self
.runtime
.handler::<H>()
.ok_or(RuntimeError::NotRegistered { name: H::NAME })?;
let key = EffectKey::new(EffectName::new(H::NAME)?, LogicalKey::new(self.key)?);
let json = serde_json::to_value(&self.input).map_err(RuntimeError::Input)?;
let stored = Stored {
fingerprint: None,
json,
actor: self.actor,
from_record: false,
};
run(self.runtime, &handler, key, Arc::new(self.input), stored).await
})
}
}
async fn resume<S: EffectStore, H: EffectHandler>(
runtime: &Runtime<S>,
handler: &Handler<H>,
record: EffectRecord,
) -> Result<EffectStatus, RuntimeError> {
let id = record.id;
if record.status == EffectStatus::Compensating {
let compensate = handler
.compensate
.clone()
.ok_or(RuntimeError::NotCompensable { name: H::NAME })?;
let spec = CompensationSpec {
key: record.key.clone(),
reason: None,
actor: Some(format!("recovery:{}", runtime.worker_id())),
retry: handler.effect.retry_policy(),
attempt_timeout: handler.effect.attempt_timeout(),
};
runtime.compensate_effect(spec, compensate).await?;
let settled = runtime
.store()
.get(id)
.await?
.ok_or(StoreError::NotFound(id))?;
return Ok(settled.status);
}
let json = record.input.clone().unwrap_or(Value::Null);
let input: H::Input = serde_json::from_value(json.clone())
.map_err(|source| RuntimeError::StoredInput { id, source })?;
let stored = Stored {
json,
fingerprint: record.input_fingerprint.clone(),
actor: record.created_by.clone(),
from_record: true,
};
run(
runtime,
handler,
record.key.clone(),
Arc::new(input),
stored,
)
.await?;
let settled = runtime
.store()
.get(id)
.await?
.ok_or(StoreError::NotFound(id))?;
Ok(settled.status)
}
struct Stored {
json: Value,
fingerprint: Option<String>,
actor: Option<String>,
from_record: bool,
}
async fn run<S: EffectStore, H: EffectHandler>(
runtime: &Runtime<S>,
handler: &Handler<H>,
key: EffectKey,
input: Arc<H::Input>,
stored: Stored,
) -> Result<EffectOutcome<H::Output>, RuntimeError> {
let effect = Arc::clone(&handler.effect);
let precondition = {
let (effect, input) = (Arc::clone(&effect), Arc::clone(&input));
Arc::new(move |ctx: EffectContext| -> BoxFuture<Precondition> {
let (effect, input) = (Arc::clone(&effect), Arc::clone(&input));
Box::pin(async move { effect.precondition(&ctx, &input).await })
})
};
let spec = EffectSpec {
fingerprint: stored.fingerprint,
input: Some(stored.json),
key,
capabilities: Capabilities {
kind: effect.kind(),
remote_idempotency: effect.remote_idempotency(),
verification: handler
.verify
.as_ref()
.map_or(VerificationMode::None, |(mode, _)| *mode),
},
actor: stored.actor,
retry: effect
.retry_policy()
.unwrap_or_else(|| runtime.default_retry()),
attempt_timeout: effect.attempt_timeout(),
precondition: Some(precondition),
require_approval: effect.requires_approval(),
risk: effect.risk(),
automatic_retry: true,
input_stored: stored.from_record,
};
let action = {
let (effect, input) = (Arc::clone(&effect), Arc::clone(&input));
move |ctx: EffectContext| {
let (effect, input) = (Arc::clone(&effect), Arc::clone(&input));
async move { effect.execute(&ctx, &input).await.map_err(Into::into) }
}
};
match &handler.verify {
Some((_, verify)) => {
let verify = Arc::clone(verify);
let checker =
VerifyWith(move |ctx| verify(Arc::clone(&effect), ctx, Arc::clone(&input)));
runtime.execute(spec, action, checker).await
}
None => runtime.execute(spec, action, NoVerification).await,
}
}
#[must_use = "a compensation does nothing until it is awaited"]
pub struct CompensationSubmission<'a, S, H: EffectHandler> {
runtime: &'a Runtime<S>,
key: String,
reason: Option<String>,
actor: Option<String>,
_handler: std::marker::PhantomData<fn() -> H>,
}
impl<'a, S, H: EffectHandler> CompensationSubmission<'a, S, H> {
pub(crate) fn new(runtime: &'a Runtime<S>, key: impl Display) -> Self {
Self {
runtime,
key: key.to_string(),
reason: None,
actor: None,
_handler: std::marker::PhantomData,
}
}
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
}
impl<'a, S: EffectStore, H: EffectHandler> IntoFuture for CompensationSubmission<'a, S, H> {
type Output = Result<CompensationOutcome, RuntimeError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let handler = self
.runtime
.handler::<H>()
.ok_or(RuntimeError::NotRegistered { name: H::NAME })?;
let compensate = handler
.compensate
.clone()
.ok_or(RuntimeError::NotCompensable { name: H::NAME })?;
let spec = CompensationSpec {
key: EffectKey::new(EffectName::new(H::NAME)?, LogicalKey::new(self.key)?),
reason: self.reason,
actor: self.actor,
retry: handler.effect.retry_policy(),
attempt_timeout: handler.effect.attempt_timeout(),
};
self.runtime.compensate_effect(spec, compensate).await
})
}
}