Skip to main content

a3s_effect/
effect.rs

1//! Programs as values.
2//!
3//! An [`Effect`] describes a program: the value it produces, the expected
4//! error it can return, and the services it needs. Constructing one does not
5//! run it. [`Effect::run`] is the edge where a description becomes a result.
6//!
7//! The operators follow the Effect v4 onboarding split: typed expected
8//! failures, retries as a schedule, structured concurrency that cancels the
9//! sibling it no longer needs, resource release on every exit including
10//! cancellation, services passed in the type, and spans recorded by the
11//! runtime.
12
13use std::future::Future;
14use std::marker::PhantomData;
15use std::pin::Pin;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19use tokio_util::sync::CancellationToken;
20
21use crate::exit::Exit;
22
23pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct TraceEvent {
27    pub name: String,
28    pub outcome: &'static str,
29}
30
31pub struct RunCtx<S> {
32    pub services: Arc<S>,
33    pub cancel: CancellationToken,
34    trace: Arc<Mutex<Vec<TraceEvent>>>,
35}
36
37impl<S> Clone for RunCtx<S> {
38    fn clone(&self) -> Self {
39        Self {
40            services: Arc::clone(&self.services),
41            cancel: self.cancel.clone(),
42            trace: Arc::clone(&self.trace),
43        }
44    }
45}
46
47impl<S> RunCtx<S> {
48    pub fn child(&self) -> Self {
49        Self {
50            services: Arc::clone(&self.services),
51            cancel: self.cancel.child_token(),
52            trace: Arc::clone(&self.trace),
53        }
54    }
55
56    fn record(&self, name: impl Into<String>, outcome: &'static str) {
57        if let Ok(mut events) = self.trace.lock() {
58            events.push(TraceEvent {
59                name: name.into(),
60                outcome,
61            });
62        }
63    }
64}
65
66/// A description of a program. `A` is the success value, `E` is the expected
67/// error, and `S` is the service environment required to run it.
68type EffectProgram<A, E, S> = dyn Fn(RunCtx<S>) -> BoxFuture<Result<A, Exit<E>>> + Send + Sync;
69
70pub struct Effect<A, E, S> {
71    run: Arc<EffectProgram<A, E, S>>,
72    _marker: PhantomData<fn() -> (A, E)>,
73}
74
75impl<A, E, S> Clone for Effect<A, E, S> {
76    fn clone(&self) -> Self {
77        Self {
78            run: Arc::clone(&self.run),
79            _marker: PhantomData,
80        }
81    }
82}
83
84impl<A, E, S> Effect<A, E, S>
85where
86    A: Send + Sync + 'static,
87    E: Send + Sync + 'static,
88    S: Send + Sync + 'static,
89{
90    fn new<F>(run: F) -> Self
91    where
92        F: Fn(RunCtx<S>) -> BoxFuture<Result<A, Exit<E>>> + Send + Sync + 'static,
93    {
94        Self {
95            run: Arc::new(run),
96            _marker: PhantomData,
97        }
98    }
99
100    pub fn succeed(value: A) -> Self
101    where
102        A: Clone,
103    {
104        let value = Arc::new(value);
105        Self::new(move |_ctx| {
106            let value = Arc::clone(&value);
107            Box::pin(async move { Ok((*value).clone()) })
108        })
109    }
110
111    pub fn fail(error: E) -> Self
112    where
113        E: Clone,
114    {
115        let error = Arc::new(error);
116        Self::new(move |_ctx| {
117            let error = Arc::clone(&error);
118            Box::pin(async move { Err(Exit::Fail((*error).clone())) })
119        })
120    }
121
122    pub fn die(message: impl Into<String>) -> Self {
123        let message = Arc::new(message.into());
124        Self::new(move |_ctx| {
125            let message = Arc::clone(&message);
126            Box::pin(async move { Err(Exit::Die((*message).clone())) })
127        })
128    }
129
130    /// Build a program from services and a cancellation token. The function
131    /// runs only when [`Effect::run`] is called.
132    pub fn from_async<F, Fut>(f: F) -> Self
133    where
134        F: Fn(Arc<S>, CancellationToken) -> Fut + Send + Sync + 'static,
135        Fut: Future<Output = Result<A, Exit<E>>> + Send + 'static,
136    {
137        let f = Arc::new(f);
138        Self::new(move |ctx| {
139            if ctx.cancel.is_cancelled() {
140                return Box::pin(async { Err(Exit::Interrupt) });
141            }
142            let fut = f(Arc::clone(&ctx.services), ctx.cancel.clone());
143            Box::pin(fut)
144        })
145    }
146
147    /// Like [`Self::from_async`], but `f` still runs when the token is already
148    /// cancelled. `f` has to observe the token. A caller that must append a
149    /// fact on cancel uses this. [`Self::from_async`] returns
150    /// [`Exit::Interrupt`] before `f` starts, and dropping that future would
151    /// skip the fact.
152    pub fn from_async_observed<F, Fut>(f: F) -> Self
153    where
154        F: Fn(Arc<S>, CancellationToken) -> Fut + Send + Sync + 'static,
155        Fut: Future<Output = Result<A, Exit<E>>> + Send + 'static,
156    {
157        let f = Arc::new(f);
158        Self::new(move |ctx| {
159            let fut = f(Arc::clone(&ctx.services), ctx.cancel.clone());
160            Box::pin(fut)
161        })
162    }
163
164    pub fn map<B, F>(&self, f: F) -> Effect<B, E, S>
165    where
166        B: Send + Sync + 'static,
167        F: Fn(A) -> B + Send + Sync + 'static,
168    {
169        let run = Arc::clone(&self.run);
170        let f = Arc::new(f);
171        Effect::new(move |ctx| {
172            let run = Arc::clone(&run);
173            let f = Arc::clone(&f);
174            Box::pin(async move { run(ctx).await.map(|value| f(value)) })
175        })
176    }
177
178    pub fn and_then<B, F>(&self, f: F) -> Effect<B, E, S>
179    where
180        B: Send + Sync + 'static,
181        F: Fn(A) -> Effect<B, E, S> + Send + Sync + 'static,
182    {
183        let run = Arc::clone(&self.run);
184        let f = Arc::new(f);
185        Effect::new(move |ctx| {
186            let run = Arc::clone(&run);
187            let f = Arc::clone(&f);
188            Box::pin(async move {
189                match run(ctx.clone()).await {
190                    Ok(value) => f(value).run_in(ctx).await,
191                    Err(error) => Err(error),
192                }
193            })
194        })
195    }
196
197    /// Handle an expected failure. Defects and interruption pass through.
198    pub fn catch_fail<F>(&self, f: F) -> Self
199    where
200        F: Fn(E) -> Effect<A, E, S> + Send + Sync + 'static,
201    {
202        let run = Arc::clone(&self.run);
203        let f = Arc::new(f);
204        Self::new(move |ctx| {
205            let run = Arc::clone(&run);
206            let f = Arc::clone(&f);
207            Box::pin(async move {
208                match run(ctx.clone()).await {
209                    Err(Exit::Fail(error)) => f(error).run_in(ctx).await,
210                    other => other,
211                }
212            })
213        })
214    }
215
216    /// Retry expected failures. Interruption and defects are not retried.
217    ///
218    /// The wrapped effect owns cancellation: an observed effect still runs
219    /// when the token is already cancelled so it can append its interrupt
220    /// facts, and an effect built with [`Self::from_async`] refuses before
221    /// its body starts. Only the backoff sleep between attempts is cut
222    /// short by the token.
223    pub fn retry(&self, schedule: Schedule) -> Self
224    where
225        E: Clone,
226    {
227        let run = Arc::clone(&self.run);
228        Self::new(move |ctx| {
229            let run = Arc::clone(&run);
230            let mut schedule = schedule;
231            Box::pin(async move {
232                loop {
233                    match run(ctx.clone()).await {
234                        Ok(value) => return Ok(value),
235                        Err(Exit::Fail(_error)) if schedule.remaining > 0 => {
236                            schedule.remaining -= 1;
237                            ctx.record("retry", "fail");
238                            tokio::select! {
239                                _ = ctx.cancel.cancelled() => return Err(Exit::Interrupt),
240                                _ = tokio::time::sleep(schedule.delay) => {}
241                            }
242                        }
243                        Err(error) => return Err(error),
244                    }
245                }
246            })
247        })
248    }
249
250    pub fn timeout(&self, duration: Duration) -> Self {
251        let run = Arc::clone(&self.run);
252        Self::new(move |ctx| {
253            let run = Arc::clone(&run);
254            let child = ctx.child();
255            Box::pin(async move {
256                let child_cancel = child.cancel.clone();
257                let fut = run(child);
258                match tokio::time::timeout(duration, fut).await {
259                    Ok(result) => result,
260                    Err(_elapsed) => {
261                        child_cancel.cancel();
262                        Err(Exit::Interrupt)
263                    }
264                }
265            })
266        })
267    }
268
269    pub fn with_span(&self, name: impl Into<String>) -> Self {
270        let name = name.into();
271        let run = Arc::clone(&self.run);
272        Self::new(move |ctx| {
273            let run = Arc::clone(&run);
274            let name = name.clone();
275            let fut = run(ctx.clone());
276            Box::pin(async move {
277                let result = fut.await;
278                let outcome = match &result {
279                    Ok(_) => "ok",
280                    Err(Exit::Fail(_)) => "fail",
281                    Err(Exit::Die(_)) => "die",
282                    Err(Exit::Interrupt) => "interrupt",
283                };
284                ctx.record(name, outcome);
285                result
286            })
287        })
288    }
289
290    /// Run `left` and `right` together. The first expected failure or defect
291    /// cancels the sibling and waits for that sibling to finish.
292    pub fn zip_par<B>(&self, right: &Effect<B, E, S>) -> Effect<(A, B), E, S>
293    where
294        B: Send + Sync + 'static,
295    {
296        let left_run = Arc::clone(&self.run);
297        let right_run = Arc::clone(&right.run);
298        Effect::new(move |ctx| {
299            let left_run = Arc::clone(&left_run);
300            let right_run = Arc::clone(&right_run);
301            let left_ctx = ctx.child();
302            let right_ctx = ctx.child();
303            Box::pin(async move {
304                let left = left_run(left_ctx.clone());
305                let right = right_run(right_ctx.clone());
306                tokio::pin!(left, right);
307                tokio::select! {
308                    left_result = &mut left => match left_result {
309                        Ok(left_value) => match right.await {
310                            Ok(right_value) => Ok((left_value, right_value)),
311                            Err(error) => Err(error),
312                        },
313                        Err(error) => {
314                            right_ctx.cancel.cancel();
315                            let _ = right.await;
316                            Err(error)
317                        }
318                    },
319                    right_result = &mut right => match right_result {
320                        Ok(right_value) => match left.await {
321                            Ok(left_value) => Ok((left_value, right_value)),
322                            Err(error) => Err(error),
323                        },
324                        Err(error) => {
325                            left_ctx.cancel.cancel();
326                            let _ = left.await;
327                            Err(error)
328                        }
329                    },
330                }
331            })
332        })
333    }
334
335    /// First success or failure wins. The other side is cancelled and awaited.
336    pub fn race(&self, other: &Self) -> Self {
337        let left_run = Arc::clone(&self.run);
338        let right_run = Arc::clone(&other.run);
339        Self::new(move |ctx| {
340            let left_run = Arc::clone(&left_run);
341            let right_run = Arc::clone(&right_run);
342            let left_ctx = ctx.child();
343            let right_ctx = ctx.child();
344            Box::pin(async move {
345                let left = left_run(left_ctx.clone());
346                let right = right_run(right_ctx.clone());
347                tokio::pin!(left, right);
348                tokio::select! {
349                    left_result = &mut left => {
350                        right_ctx.cancel.cancel();
351                        let _ = right.await;
352                        left_result
353                    }
354                    right_result = &mut right => {
355                        left_ctx.cancel.cancel();
356                        let _ = left.await;
357                        right_result
358                    }
359                }
360            })
361        })
362    }
363
364    /// Acquire a resource, use it, and release it on success, expected
365    /// failure, and cancellation. Dropping the running future releases it too.
366    pub fn bracket<B, F, R>(&self, release: R, body: F) -> Effect<B, E, S>
367    where
368        A: Clone,
369        B: Send + Sync + 'static,
370        F: Fn(A) -> Effect<B, E, S> + Send + Sync + 'static,
371        R: Fn(A) + Send + Sync + 'static,
372    {
373        let acquire = Arc::clone(&self.run);
374        let release: Arc<dyn Fn(A) + Send + Sync> = Arc::new(release);
375        let body = Arc::new(body);
376        Effect::new(move |ctx| {
377            let acquire = Arc::clone(&acquire);
378            let release = Arc::clone(&release);
379            let body = Arc::clone(&body);
380            Box::pin(async move {
381                let resource = match acquire(ctx.clone()).await {
382                    Ok(resource) => resource,
383                    Err(error) => return Err(error),
384                };
385                let guard = ReleaseOnce::new(resource.clone(), Arc::clone(&release));
386                let result = body(resource).run_in(ctx).await;
387                guard.release_now();
388                result
389            })
390        })
391    }
392
393    /// Map the caller's services while preserving cancellation and the trace.
394    pub fn map_services<S2>(
395        self,
396        map: impl Fn(Arc<S2>) -> Arc<S> + Send + Sync + 'static,
397    ) -> Effect<A, E, S2>
398    where
399        S2: Send + Sync + 'static,
400    {
401        let run = self.run;
402        Effect::new(move |ctx| {
403            let mapped = RunCtx {
404                services: map(ctx.services),
405                cancel: ctx.cancel,
406                trace: ctx.trace,
407            };
408            run(mapped)
409        })
410    }
411
412    /// Replace the service environment. The resulting program no longer
413    /// requires `S`; the caller supplies a different environment at the edge.
414    pub fn provide<S2>(self, services: S) -> Effect<A, E, S2>
415    where
416        S2: Send + Sync + 'static,
417    {
418        let services = Arc::new(services);
419        let run = self.run;
420        Effect::new(move |ctx| {
421            let run = Arc::clone(&run);
422            let services = Arc::clone(&services);
423            let ctx = RunCtx {
424                services,
425                cancel: ctx.cancel,
426                trace: ctx.trace,
427            };
428            run(ctx)
429        })
430    }
431
432    pub async fn run(self, services: Arc<S>) -> (Result<A, Exit<E>>, Vec<TraceEvent>) {
433        self.run_with_cancel(services, CancellationToken::new())
434            .await
435    }
436
437    /// Run with the caller's token. [`Self::run`] passes a fresh token that
438    /// nothing else cancels.
439    pub async fn run_with_cancel(
440        self,
441        services: Arc<S>,
442        cancel: CancellationToken,
443    ) -> (Result<A, Exit<E>>, Vec<TraceEvent>) {
444        let trace = Arc::new(Mutex::new(Vec::new()));
445        let ctx = RunCtx {
446            services,
447            cancel,
448            trace: Arc::clone(&trace),
449        };
450        let result = self.run_in(ctx).await;
451        let events = trace
452            .lock()
453            .map(|events| events.clone())
454            .unwrap_or_default();
455        (result, events)
456    }
457
458    pub(crate) async fn run_in(&self, ctx: RunCtx<S>) -> Result<A, Exit<E>> {
459        (self.run)(ctx).await
460    }
461}
462
463#[derive(Debug, Clone, Copy)]
464pub struct Schedule {
465    pub remaining: u32,
466    pub delay: Duration,
467}
468
469struct ReleaseOnce<T> {
470    value: Mutex<Option<T>>,
471    release: Arc<dyn Fn(T) + Send + Sync>,
472}
473
474impl<T> ReleaseOnce<T> {
475    fn new(value: T, release: Arc<dyn Fn(T) + Send + Sync>) -> Self {
476        Self {
477            value: Mutex::new(Some(value)),
478            release,
479        }
480    }
481
482    fn release_now(&self) {
483        if let Ok(mut slot) = self.value.lock() {
484            if let Some(value) = slot.take() {
485                (self.release)(value);
486            }
487        }
488    }
489}
490
491impl<T> Drop for ReleaseOnce<T> {
492    fn drop(&mut self) {
493        self.release_now();
494    }
495}