1use std::error::Error;
4use std::fmt;
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::time::Duration;
9
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use serde_json::Value;
13
14use crate::error::RuntimeError;
15use crate::failure::FailureClass;
16use crate::id::{EffectId, EffectKey, IdempotencyKey};
17use crate::kind::EffectKind;
18use crate::policy::{Capabilities, RiskLevel};
19use crate::retry::RetryPolicy;
20use crate::runtime::Runtime;
21use crate::store::{EffectStore, ErrorRecord};
22use crate::verification::{NoVerification, Verification, VerificationMode, Verifier, VerifyWith};
23
24#[derive(Clone, Debug)]
26pub struct EffectContext {
27 pub(crate) id: EffectId,
28 pub(crate) key: EffectKey,
29 pub(crate) attempt: u32,
30}
31
32impl EffectContext {
33 pub fn effect_id(&self) -> EffectId {
35 self.id
36 }
37
38 pub fn key(&self) -> &EffectKey {
40 &self.key
41 }
42
43 pub fn idempotency_key(&self) -> IdempotencyKey {
46 self.key.idempotency_key()
47 }
48
49 pub fn attempt(&self) -> u32 {
51 self.attempt
52 }
53}
54
55#[derive(Debug)]
63pub struct EffectFailure {
64 class: FailureClass,
65 request_sent: Option<bool>,
66 retry_after: Option<Duration>,
67 source: Box<dyn Error + Send + Sync>,
68}
69
70impl EffectFailure {
71 pub fn new(class: FailureClass, source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
73 Self {
74 class,
75 request_sent: None,
76 retry_after: None,
77 source: source.into(),
78 }
79 }
80
81 pub fn transient(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
83 Self::new(FailureClass::Transient, source)
84 }
85
86 pub fn permanent(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
88 Self::new(FailureClass::Permanent, source)
89 }
90
91 pub fn ambiguous(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
93 Self::new(FailureClass::Ambiguous, source)
94 }
95
96 pub fn rate_limited(
98 retry_after: Option<Duration>,
99 source: impl Into<Box<dyn Error + Send + Sync>>,
100 ) -> Self {
101 Self::new(FailureClass::RateLimited { retry_after }, source)
102 }
103
104 pub fn authentication(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
106 Self::new(FailureClass::Authentication, source)
107 }
108
109 pub fn authorization(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
111 Self::new(FailureClass::Authorization, source)
112 }
113
114 pub fn validation(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
116 Self::new(FailureClass::Validation, source)
117 }
118
119 #[must_use]
122 pub fn request_sent(mut self, sent: bool) -> Self {
123 self.request_sent = Some(sent);
124 self
125 }
126
127 #[must_use]
131 pub fn with_retry_after(mut self, delay: Duration) -> Self {
132 self.retry_after = Some(delay);
133 self
134 }
135
136 pub fn retry_after(&self) -> Option<Duration> {
138 self.retry_after.max(self.class().retry_after())
139 }
140
141 pub fn class(&self) -> FailureClass {
143 self.class.with_request_sent(self.request_sent)
144 }
145
146 pub(crate) fn to_record(&self) -> ErrorRecord {
147 ErrorRecord {
148 class: Some(self.class()),
149 message: self.source.to_string(),
150 }
151 }
152}
153
154impl fmt::Display for EffectFailure {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "{:?} failure: {}", self.class(), self.source)
157 }
158}
159
160impl Error for EffectFailure {
161 fn source(&self) -> Option<&(dyn Error + 'static)> {
162 Some(self.source.as_ref())
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
172#[non_exhaustive]
173pub enum EffectOutcome<T> {
174 Committed(T),
177 Failed(ErrorRecord),
179 Rejected(ErrorRecord),
181 Unknown {
184 id: EffectId,
186 },
187 NeedsIntervention {
189 id: EffectId,
191 },
192 InProgress {
194 id: EffectId,
196 },
197 AwaitingApproval {
201 id: EffectId,
203 },
204 Compensated {
206 id: EffectId,
208 },
209}
210
211#[derive(Clone, Debug, PartialEq, Eq)]
213pub enum Precondition {
214 Satisfied,
216 Rejected {
219 reason: String,
221 },
222 RetryLater {
226 after: Duration,
228 reason: String,
230 },
231}
232
233impl Precondition {
234 pub fn reject(reason: impl Into<String>) -> Self {
236 Self::Rejected {
237 reason: reason.into(),
238 }
239 }
240
241 pub fn retry_later(after: Duration, reason: impl Into<String>) -> Self {
243 Self::RetryLater {
244 after,
245 reason: reason.into(),
246 }
247 }
248}
249
250pub(crate) type PreconditionFn =
251 Arc<dyn Fn(EffectContext) -> Pin<Box<dyn Future<Output = Precondition> + Send>> + Send + Sync>;
252
253#[must_use = "an effect does nothing until `run` is awaited"]
258pub struct EffectBuilder<S, V = NoVerification> {
259 runtime: Runtime<S>,
260 name: String,
261 key: String,
262 kind: EffectKind,
263 remote_idempotency: bool,
264 input: Option<Result<Value, serde_json::Error>>,
265 actor: Option<String>,
266 retry: RetryPolicy,
267 attempt_timeout: Option<Duration>,
268 precondition: Option<PreconditionFn>,
269 require_approval: bool,
270 risk: RiskLevel,
271 verification: VerificationMode,
272 verifier: V,
273 handler_version: String,
274 adapter: Option<String>,
275 input_schema_version: u32,
276 output_schema: crate::PayloadSchema,
277}
278
279impl<S: EffectStore> EffectBuilder<S> {
280 pub(crate) fn new(runtime: Runtime<S>, name: String, key: String) -> Self {
281 let retry = runtime.default_retry();
282 Self {
283 runtime,
284 name,
285 key,
286 kind: EffectKind::IrreversibleWrite,
287 remote_idempotency: false,
288 input: None,
289 actor: None,
290 retry,
291 attempt_timeout: None,
292 precondition: None,
293 require_approval: false,
294 risk: RiskLevel::Low,
295 verification: VerificationMode::None,
296 verifier: NoVerification,
297 handler_version: "1".into(),
298 adapter: None,
299 input_schema_version: 1,
300 output_schema: crate::PayloadSchema::default(),
301 }
302 }
303
304 pub fn verify<T, F, Fut>(self, check: F) -> EffectBuilder<S, VerifyWith<F>>
312 where
313 F: Fn(EffectContext) -> Fut + Send + Sync + 'static,
314 Fut: Future<Output = Result<Verification<T>, EffectFailure>> + Send + 'static,
315 {
316 self.with_verifier(VerificationMode::Authoritative, VerifyWith(check))
317 }
318
319 pub fn verify_eventually<T, F, Fut>(
324 self,
325 settle: Duration,
326 check: F,
327 ) -> EffectBuilder<S, VerifyWith<F>>
328 where
329 F: Fn(EffectContext) -> Fut + Send + Sync + 'static,
330 Fut: Future<Output = Result<Verification<T>, EffectFailure>> + Send + 'static,
331 {
332 self.with_verifier(
333 VerificationMode::EventuallyConsistent { settle },
334 VerifyWith(check),
335 )
336 }
337
338 fn with_verifier<V>(self, mode: VerificationMode, verifier: V) -> EffectBuilder<S, V> {
339 EffectBuilder {
340 runtime: self.runtime,
341 name: self.name,
342 key: self.key,
343 kind: self.kind,
344 remote_idempotency: self.remote_idempotency,
345 input: self.input,
346 actor: self.actor,
347 retry: self.retry,
348 attempt_timeout: self.attempt_timeout,
349 precondition: self.precondition,
350 require_approval: self.require_approval,
351 risk: self.risk,
352 verification: mode,
353 verifier,
354 handler_version: self.handler_version,
355 adapter: self.adapter,
356 input_schema_version: self.input_schema_version,
357 output_schema: self.output_schema,
358 }
359 }
360}
361
362impl<S: EffectStore, V> EffectBuilder<S, V> {
363 pub fn input_schema_version(mut self, version: u32) -> Self {
366 self.input_schema_version = version;
367 self
368 }
369
370 pub fn output_schema(mut self, schema: crate::PayloadSchema) -> Self {
373 self.output_schema = schema;
374 self
375 }
376
377 pub fn contract_version(mut self, version: impl Into<String>) -> Self {
381 self.handler_version = version.into();
382 self
383 }
384
385 pub fn adapter_identity(mut self, adapter: impl Into<String>) -> Self {
389 self.adapter = Some(adapter.into());
390 self
391 }
392
393 pub fn kind(mut self, kind: EffectKind) -> Self {
396 self.kind = kind;
397 self
398 }
399
400 pub fn remote_idempotency(mut self, supported: bool) -> Self {
404 self.remote_idempotency = supported;
405 self
406 }
407
408 pub fn input<I: Serialize + ?Sized>(mut self, input: &I) -> Self {
415 self.input = Some(serde_json::to_value(input));
416 self
417 }
418
419 pub fn actor(mut self, actor: impl Into<String>) -> Self {
422 self.actor = Some(actor.into());
423 self
424 }
425
426 pub fn retry(mut self, policy: RetryPolicy) -> Self {
433 self.retry = policy;
434 self
435 }
436
437 pub fn attempt_timeout(mut self, timeout: Duration) -> Self {
440 self.attempt_timeout = Some(timeout);
441 self
442 }
443
444 pub fn precondition<F, Fut>(mut self, check: F) -> Self
452 where
453 F: Fn(EffectContext) -> Fut + Send + Sync + 'static,
454 Fut: Future<Output = Precondition> + Send + 'static,
455 {
456 self.precondition = Some(Arc::new(move |ctx| Box::pin(check(ctx))));
457 self
458 }
459
460 pub fn risk(mut self, risk: RiskLevel) -> Self {
464 self.risk = risk;
465 self
466 }
467
468 pub fn require_approval(mut self) -> Self {
473 self.require_approval = true;
474 self
475 }
476
477 pub async fn run<T, F, Fut>(self, action: F) -> Result<EffectOutcome<T>, RuntimeError>
490 where
491 T: Serialize + DeserializeOwned + Send + 'static,
492 F: Fn(EffectContext) -> Fut + Send + Sync + 'static,
493 Fut: Future<Output = Result<T, EffectFailure>> + Send + 'static,
494 V: Verifier<T>,
495 {
496 let key = EffectKey::new(
497 crate::id::EffectName::new(self.name)?,
498 crate::id::LogicalKey::new(self.key)?,
499 );
500 let input = self.input.transpose().map_err(RuntimeError::Input)?;
501 let spec = EffectSpec {
502 fingerprint: None,
504 input,
505 key,
506 capabilities: Capabilities {
507 kind: self.kind,
508 remote_idempotency: self.remote_idempotency,
509 verification: self.verification,
510 },
511 actor: self.actor,
512 retry: self.retry,
513 attempt_timeout: self.attempt_timeout,
514 precondition: self.precondition,
515 require_approval: self.require_approval,
516 risk: self.risk,
517 automatic_retry: true,
518 input_stored: false,
519 handler_version: self.handler_version,
520 adapter: self.adapter,
521 input_schema_version: self.input_schema_version,
522 output_schema: self.output_schema,
523 };
524 self.runtime.execute(spec, action, self.verifier).await
525 }
526}
527
528pub(crate) struct EffectSpec {
530 pub(crate) input_schema_version: u32,
531 pub(crate) output_schema: crate::PayloadSchema,
532 pub(crate) handler_version: String,
533 pub(crate) adapter: Option<String>,
534 pub(crate) key: EffectKey,
535 pub(crate) capabilities: Capabilities,
536 pub(crate) input: Option<Value>,
537 pub(crate) fingerprint: Option<String>,
538 pub(crate) actor: Option<String>,
539 pub(crate) retry: RetryPolicy,
540 pub(crate) attempt_timeout: Option<Duration>,
541 pub(crate) precondition: Option<PreconditionFn>,
542 pub(crate) require_approval: bool,
543 pub(crate) risk: RiskLevel,
544 pub(crate) automatic_retry: bool,
547 pub(crate) input_stored: bool,
550}
551
552impl EffectSpec {
553 pub(crate) fn contract(&self) -> crate::store::ExecutionContract {
554 use crate::store::{ExecutionContract, VerificationContract};
555 ExecutionContract {
556 format_version: 1,
557 kind: self.capabilities.kind,
558 remote_idempotency: self.capabilities.remote_idempotency,
559 verification: match self.capabilities.verification {
560 VerificationMode::None => VerificationContract::None,
561 VerificationMode::Authoritative => VerificationContract::Authoritative,
562 VerificationMode::EventuallyConsistent { settle } => {
563 VerificationContract::EventuallyConsistent { settle }
564 }
565 },
566 handler_version: self.handler_version.clone(),
567 adapter: self.adapter.clone(),
568 require_approval: self.require_approval,
569 automatic_retry: self.automatic_retry,
570 max_attempts: self.retry.max_attempts,
571 }
572 }
573}