Skip to main content

agent_effects/
verification.rs

1//! Verification: asking the remote system whether an effect applied.
2//!
3//! A verification runs after every successful attempt (a postcondition) and
4//! to reconcile an attempt whose outcome is unknown.
5
6use std::future::Future;
7use std::pin::Pin;
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12use crate::effect::{EffectContext, EffectFailure};
13
14/// What a verification found in the remote system.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum Verification<T> {
17    /// The effect applied; carries the remote state, which becomes the
18    /// effect's output.
19    Confirmed(T),
20    /// No trace of the effect. How far this is trusted depends on the
21    /// [`VerificationMode`].
22    NotApplied,
23    /// The remote system cannot tell right now. The runtime checks again
24    /// later and never treats this as "not applied".
25    Inconclusive,
26    /// The remote state contradicts the effect, e.g. a payment exists for
27    /// the order but with a different amount. Needs an operator.
28    Conflict {
29        /// What contradicts what.
30        details: String,
31    },
32}
33
34/// A boxed verification future.
35pub type VerificationFuture<T> =
36    Pin<Box<dyn Future<Output = Result<Verification<T>, EffectFailure>> + Send>>;
37
38/// The verification attached to an effect, if any.
39///
40/// Implemented by [`NoVerification`] and [`VerifyWith`]; set through
41/// `EffectBuilder::verify`. A failing check (`Err`) counts as
42/// [`Verification::Inconclusive`].
43pub trait Verifier<T>: Send + Sync + 'static {
44    /// Starts a check, or `None` if the effect has no verification.
45    fn check(&self, ctx: EffectContext) -> Option<VerificationFuture<T>>;
46}
47
48/// The effect has no verification.
49#[derive(Clone, Copy, Debug, Default)]
50pub struct NoVerification;
51
52impl<T> Verifier<T> for NoVerification {
53    fn check(&self, _ctx: EffectContext) -> Option<VerificationFuture<T>> {
54        None
55    }
56}
57
58/// Verification by a closure.
59#[derive(Clone, Copy, Debug)]
60pub struct VerifyWith<F>(pub(crate) F);
61
62impl<T, F, Fut> Verifier<T> for VerifyWith<F>
63where
64    F: Fn(EffectContext) -> Fut + Send + Sync + 'static,
65    Fut: Future<Output = Result<Verification<T>, EffectFailure>> + Send + 'static,
66{
67    fn check(&self, ctx: EffectContext) -> Option<VerificationFuture<T>> {
68        Some(Box::pin((self.0)(ctx)))
69    }
70}
71
72/// How far a verification result can be trusted.
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case", tag = "mode")]
75pub enum VerificationMode {
76    /// The effect has no verification.
77    #[default]
78    None,
79    /// The remote system reads its own writes: "not found" means the effect
80    /// did not apply.
81    Authoritative,
82    /// The remote system's lookup lags behind its writes, as many search and
83    /// list APIs do. "Not found" is only trusted once `settle` has passed
84    /// since the attempt ended (its action returned or was found
85    /// interrupted); earlier, it counts as inconclusive.
86    EventuallyConsistent {
87        /// How long a write may take to become visible to the lookup.
88        settle: Duration,
89    },
90}
91
92/// How to read a verification that found no trace of the effect.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum NotFoundReading {
95    /// The effect did not apply; re-executing it is safe.
96    NotApplied,
97    /// Too early to tell; verify again after `wait`.
98    TooEarly {
99        /// Remaining time until the lookup can be trusted.
100        wait: Duration,
101    },
102}
103
104impl VerificationMode {
105    /// How to read "not found" when `elapsed` has passed since the attempt
106    /// in question ended.
107    ///
108    /// [`VerificationMode::None`] has no verification to read; it is
109    /// reported as [`NotFoundReading::TooEarly`] with no wait so callers that
110    /// get here by mistake never re-execute.
111    pub fn read_not_found(self, elapsed: Duration) -> NotFoundReading {
112        match self {
113            Self::Authoritative => NotFoundReading::NotApplied,
114            Self::EventuallyConsistent { settle } if elapsed >= settle => {
115                NotFoundReading::NotApplied
116            }
117            Self::EventuallyConsistent { settle } => NotFoundReading::TooEarly {
118                wait: settle.saturating_sub(elapsed),
119            },
120            Self::None => NotFoundReading::TooEarly {
121                wait: Duration::ZERO,
122            },
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn eventually_consistent_lookups_wait_out_the_settle_delay() {
133        let mode = VerificationMode::EventuallyConsistent {
134            settle: Duration::from_secs(5),
135        };
136        assert_eq!(
137            mode.read_not_found(Duration::from_secs(2)),
138            NotFoundReading::TooEarly {
139                wait: Duration::from_secs(3)
140            }
141        );
142        assert_eq!(
143            mode.read_not_found(Duration::from_secs(5)),
144            NotFoundReading::NotApplied
145        );
146        assert_eq!(
147            VerificationMode::None.read_not_found(Duration::MAX),
148            NotFoundReading::TooEarly {
149                wait: Duration::ZERO
150            }
151        );
152    }
153}