Skip to main content

rx_rust/utils/
serialized_delivery.rs

1//! The state machine that serializes the events delivered to one observer, together with the lock
2//! that guards it and the only operations allowed to drive it.
3//!
4//! [`SerializedDelivery`] is a shared handle. Its lock never leaves this module, so a host cannot
5//! hold it across a notification, a drop, or a second lock: every transition, the delivery loop,
6//! and the rule that nothing is dropped or notified under the lock live here.
7//!
8//! A host that must change its own data, and emit the resulting events atomically, does so through
9//! [`SerializedDelivery::update`], which runs its callback under the same lock that then queues
10//! the events. The callback describes its outcome with an [`UpdateOutcome`], the one way to hand
11//! this module events to queue and a value to drop once they were delivered.
12
13use crate::{
14    observer::{Flow, Observer, Termination},
15    utils::{
16        mutable::{Mutable, MutableExt, MutableHelper},
17        on_panic::on_panic,
18        pending_events::{EventBatch, PendingEvents},
19        types::{Shared, WeakShared},
20    },
21};
22use educe::Educe;
23
24/// A shared, serialized delivery of events to one observer.
25///
26/// `R` is whatever the host owns alongside the observer. It is dropped, outside the lock, once the
27/// delivery stops — after the terminal notification when the delivery stops by terminating.
28#[derive(Educe)]
29#[educe(Debug, Clone)]
30pub struct SerializedDelivery<T, E, OR, R>(Shared<Mutable<State<T, E, OR, R>>>);
31
32/// A non-owning reference to a [`SerializedDelivery`].
33#[derive(Educe)]
34#[educe(Debug, Clone)]
35pub struct WeakSerializedDelivery<T, E, OR, R>(WeakShared<Mutable<State<T, E, OR, R>>>);
36
37#[derive(Educe)]
38#[educe(Debug)]
39enum State<T, E, OR, R> {
40    /// The observer is parked in the state while no delivery is running.
41    Idle { observer: OR, resources: R },
42    /// The observer is held by the delivery loop, while re-entrant events wait here.
43    Delivering {
44        pending: PendingEvents<T, E>,
45        resources: R,
46    },
47    /// The observer and all resources are gone. Every later event is rejected.
48    Stopped,
49}
50
51/// The action to perform after releasing the lock used to call `enqueue_batch`.
52enum EnqueueAction<T, E, OR> {
53    /// Start a delivery loop with the observer removed from the locked state.
54    Start { observer: OR, first_next: Option<T> },
55    /// The batch was queued for a running delivery, or was empty and needed no work.
56    Accepted,
57    /// The delivery was already stopped or a termination was already queued.
58    Rejected(EventBatch<T, E>),
59}
60
61/// One transition of the delivery loop, computed while the state is locked and acted on outside
62/// it. The observer is threaded through, so it is never used or dropped under the lock.
63enum Step<T, E, OR, R> {
64    /// Deliver one value, then ask for the next step.
65    Next(OR, T),
66    /// Deliver the termination, then drop `resources`. The state is already `Stopped`.
67    Terminate {
68        observer: OR,
69        termination: Termination<E>,
70        resources: R,
71    },
72    /// Nothing is queued; the observer was parked back into the state.
73    Parked,
74    /// The delivery stopped; drop the observer outside the lock.
75    Stopped(OR),
76}
77
78/// Returned when an update did not run because the delivery has stopped.
79///
80/// Stopping is terminal: once a delivery has stopped, every later update returns this instead of
81/// running, and the update and everything it captured are dropped outside the lock.
82#[derive(Educe)]
83#[educe(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct DeliveryStopped;
85
86/// The marker of an [`UpdateOutcome`] that has not decided what to drop outside the lock yet.
87pub struct DropUndecided;
88
89/// The marker of an [`UpdateOutcome`] that has decided, carrying the value to drop, if any.
90pub struct DropDecided<T>(Option<T>);
91
92/// What an update performed under the lock produced: the events to queue, a value to drop once
93/// they were delivered, and the result to give back to the caller.
94///
95/// The two type-state parameters make each effect settable at most once, and let the arms of a
96/// branching update share one type: an arm that sets no events next to one that does still has to
97/// say so, with [`Self::without_events`].
98#[derive(Educe)]
99#[educe(Debug)]
100pub struct UpdateOutcome<T, E, R = (), DO = DropUndecided, const EVENTS_DECIDED: bool = false> {
101    events: Option<EventBatch<T, E>>,
102    drop_outside: DO,
103    result: R,
104}
105
106impl<T, E, R> UpdateOutcome<T, E, R> {
107    pub fn new(result: R) -> Self {
108        Self {
109            events: None,
110            drop_outside: DropUndecided,
111            result,
112        }
113    }
114}
115
116impl<T, E> UpdateOutcome<T, E> {
117    pub fn empty() -> Self {
118        Self::new(())
119    }
120}
121
122impl<T, E, R, DO, const EVENTS_DECIDED: bool> UpdateOutcome<T, E, R, DO, EVENTS_DECIDED> {
123    /// Takes the outcome apart, for a host that queues the events somewhere else.
124    ///
125    /// This is how [`SerializedMulticast`](crate::utils::serialized_multicast::SerializedMulticast)
126    /// translates the outcome of its own host into the events of the delivery underneath it. The
127    /// events must still be queued, and the value still be dropped, under and outside the very
128    /// lock this outcome was produced under.
129    pub(crate) fn into_parts(self) -> (Option<EventBatch<T, E>>, DO, R) {
130        (self.events, self.drop_outside, self.result)
131    }
132}
133
134impl<T, E, R, const EVENTS_DECIDED: bool> UpdateOutcome<T, E, R, DropUndecided, EVENTS_DECIDED> {
135    pub fn with_drop_outside<DO>(
136        self,
137        drop_outside: DO,
138    ) -> UpdateOutcome<T, E, R, DropDecided<DO>, EVENTS_DECIDED> {
139        UpdateOutcome {
140            events: self.events,
141            drop_outside: DropDecided(Some(drop_outside)),
142            result: self.result,
143        }
144    }
145
146    pub fn without_drop_outside<DO>(
147        self,
148    ) -> UpdateOutcome<T, E, R, DropDecided<DO>, EVENTS_DECIDED> {
149        UpdateOutcome {
150            events: self.events,
151            drop_outside: DropDecided(None),
152            result: self.result,
153        }
154    }
155}
156
157impl<T, E, R, DO> UpdateOutcome<T, E, R, DO, false> {
158    pub fn with_next_event(self, next: T) -> UpdateOutcome<T, E, R, DO, true> {
159        self.with_events(EventBatch::Next(next))
160    }
161
162    pub fn with_termination_event(
163        self,
164        termination: Termination<E>,
165    ) -> UpdateOutcome<T, E, R, DO, true> {
166        self.with_events(EventBatch::Termination(termination))
167    }
168
169    pub fn with_next_and_termination_events(
170        self,
171        next: T,
172        termination: Termination<E>,
173    ) -> UpdateOutcome<T, E, R, DO, true> {
174        self.with_events(EventBatch::NextAndTermination(next, termination))
175    }
176
177    pub fn with_events(self, events: EventBatch<T, E>) -> UpdateOutcome<T, E, R, DO, true> {
178        UpdateOutcome {
179            events: Some(events),
180            drop_outside: self.drop_outside,
181            result: self.result,
182        }
183    }
184
185    pub fn without_events(self) -> UpdateOutcome<T, E, R, DO, true> {
186        UpdateOutcome {
187            events: None,
188            drop_outside: self.drop_outside,
189            result: self.result,
190        }
191    }
192}
193
194impl<T, E, OR, R> SerializedDelivery<T, E, OR, R> {
195    /// Starts with the observer attached and parked, waiting for the first event.
196    pub fn idle(observer: OR, resources: R) -> Self {
197        Self(Shared::new(Mutable::new(State::Idle {
198            observer,
199            resources,
200        })))
201    }
202
203    /// Stops the delivery, dropping the observer without notifying it. Stopping again is a no-op.
204    pub fn stop(&self) {
205        // `Stopped` is the only variant that owns nothing, so replacing the state with it takes
206        // the observer, the queued events and the resources out. Binding them here drops all of
207        // them outside the lock, avoiding a potential deadlock.
208        let _deferred_drop = self.0.replace_value(State::Stopped);
209    }
210
211    pub fn downgrade(&self) -> WeakSerializedDelivery<T, E, OR, R> {
212        WeakSerializedDelivery(Shared::downgrade(&self.0))
213    }
214}
215
216impl<T, E, OR, R> SerializedDelivery<T, E, OR, R>
217where
218    OR: Observer<T, E>,
219{
220    /// Queues `events` and delivers whatever that makes deliverable.
221    ///
222    /// Returns whether the observer still accepts events. It is [`Flow::Stop`] once the delivery
223    /// has stopped or a termination is already queued — the events are then rejected and dropped
224    /// outside the lock — and also whenever `events` carries a termination, since nothing can be
225    /// queued after it. Values queued behind a delivery running elsewhere are reported as
226    /// [`Flow::Continue`], as [`Flow`] describes.
227    pub fn send(&self, events: EventBatch<T, E>) -> Flow {
228        // A batch that carries a termination ends the stream whatever the delivery answers:
229        // nothing can be queued after it, and it is delivered for certain once queued.
230        let ends_stream = events.ends_stream();
231        let action = self.0.with_mut(|state| state.enqueue_batch(events));
232        let flow = self.perform(action);
233        if ends_stream { Flow::Stop } else { flow }
234    }
235
236    /// Updates the resources and queues the events that update produced, under one lock.
237    ///
238    /// This is how a host changes what it owns, whether or not that emits anything: an update that
239    /// emits nothing simply decides no events, and then no delivery can start here.
240    ///
241    /// `update` describes its outcome with an [`UpdateOutcome`]. It must not notify anyone or drop
242    /// a value that can re-enter this delivery: it runs under the lock, so hand such a value to
243    /// [`UpdateOutcome::with_drop_outside`] instead.
244    ///
245    /// Returns [`DeliveryStopped`], without running `update`, once the delivery has stopped.
246    /// `update` and everything it captured are then dropped outside the lock.
247    pub fn update<Out, DO, const EVENTS_DECIDED: bool>(
248        &self,
249        update: impl FnOnce(&mut R) -> UpdateOutcome<T, E, Out, DO, EVENTS_DECIDED>,
250    ) -> Result<Out, DeliveryStopped> {
251        self.update_with_flow(update).map(|(result, _)| result)
252    }
253
254    /// [`Self::update`], reporting as well whether the observer still accepts events.
255    ///
256    /// The flow is what delivering the queued events answered, or [`Flow::Continue`] when the
257    /// update queued none. A stopped delivery answers [`DeliveryStopped`] rather than a flow, so a
258    /// host that only needs the flow maps that error to [`Flow::Stop`].
259    pub fn update_with_flow<Out, DO, const EVENTS_DECIDED: bool>(
260        &self,
261        update: impl FnOnce(&mut R) -> UpdateOutcome<T, E, Out, DO, EVENTS_DECIDED>,
262    ) -> Result<(Out, Flow), DeliveryStopped> {
263        // Keep `update` out of the closure so that, when the delivery has stopped, its captures
264        // are dropped only after the lock is released.
265        let mut update = Some(update);
266        let (action, drop_outside, result) = self
267            .0
268            .with_mut(|state| {
269                let resources = state.resources_mut()?;
270                let update = update.take().expect("the update runs at most once");
271                let UpdateOutcome {
272                    events,
273                    drop_outside,
274                    result,
275                } = update(resources);
276                let action =
277                    events.map(|events| (events.ends_stream(), state.enqueue_batch(events)));
278                Some((action, drop_outside, result))
279            })
280            .ok_or(DeliveryStopped)?;
281        let flow = match action {
282            // As in `send`, a termination ends the stream whatever the delivery answers.
283            Some((true, action)) => {
284                let _ = self.perform(action);
285                Flow::Stop
286            }
287            Some((false, action)) => self.perform(action),
288            None => Flow::Continue,
289        };
290        drop(drop_outside); // Drop after the delivery, outside the lock
291        Ok((result, flow))
292    }
293
294    /// Performs, with the lock released, what `enqueue_batch` deferred to outside it.
295    fn perform(&self, action: EnqueueAction<T, E, OR>) -> Flow {
296        match action {
297            EnqueueAction::Start {
298                observer,
299                first_next,
300            } => self.deliver(observer, first_next),
301            EnqueueAction::Accepted => Flow::Continue,
302            EnqueueAction::Rejected(events) => {
303                drop(events); // Drop outside the lock to avoid potential deadlock
304                Flow::Stop
305            }
306        }
307    }
308
309    /// Delivers `first_next` and then the queued events, one at a time.
310    ///
311    /// The lock is reacquired between two events, so an event that arrives during a delivery is
312    /// delivered in arrival order, and a stop takes effect immediately — including between two
313    /// values of one `EventBatch::NextBatch`: the loop then drops the observer instead of
314    /// delivering to it.
315    ///
316    /// Every observer callback runs outside the lock. If one unwinds, the delivery is stopped, so
317    /// a caught panic cannot leave it stuck in its delivering state — and locking from the guard is
318    /// safe on the panicking thread for that same reason.
319    ///
320    /// Returns [`Flow::Stop`] once this delivery is over — because the observer stopped, because
321    /// it was terminated, or because the delivery had already been stopped — and
322    /// [`Flow::Continue`] when the observer was parked back, waiting for the next event.
323    fn deliver(&self, mut observer: OR, first_next: Option<T>) -> Flow {
324        if let Some(value) = first_next {
325            let guard = on_panic(|| self.stop());
326            let flow = observer.on_next(value);
327            drop(guard);
328            if flow.is_stop() {
329                return self.stop_with(observer);
330            }
331        }
332
333        loop {
334            match self.0.with_mut(|state| state.next_step(observer)) {
335                Step::Next(next_observer, value) => {
336                    observer = next_observer;
337                    let guard = on_panic(|| self.stop());
338                    let flow = observer.on_next(value);
339                    drop(guard);
340                    if flow.is_stop() {
341                        return self.stop_with(observer);
342                    }
343                }
344                Step::Terminate {
345                    observer,
346                    termination,
347                    resources,
348                } => {
349                    let guard = on_panic(|| self.stop());
350                    observer.on_termination(termination);
351                    drop(guard);
352                    // The resources outlive the terminal notification, so a host can dispose its
353                    // source only after its downstream was told the stream ended. Unwinding from
354                    // the callback drops them too.
355                    drop(resources);
356                    return Flow::Stop;
357                }
358                Step::Parked => return Flow::Continue,
359                Step::Stopped(observer) => {
360                    drop(observer); // Drop outside the lock to avoid potential deadlock
361                    return Flow::Stop;
362                }
363            }
364        }
365    }
366
367    /// Stops the delivery because `observer`, which the loop still holds, accepts nothing more.
368    ///
369    /// The observer is not terminated: [`Flow::Stop`] says it has already ended its own stream or
370    /// been disposed, so it is released like a disposed one. The state is stopped first, so that
371    /// anything its drop sends is rejected rather than queued for a delivery that is over.
372    fn stop_with(&self, observer: OR) -> Flow {
373        self.stop();
374        drop(observer); // Drop outside the lock to avoid potential deadlock
375        Flow::Stop
376    }
377}
378
379impl<T, E, OR, R> WeakSerializedDelivery<T, E, OR, R> {
380    pub fn upgrade(&self) -> Option<SerializedDelivery<T, E, OR, R>> {
381        self.0.upgrade().map(SerializedDelivery)
382    }
383}
384
385impl<T, E, OR, R> State<T, E, OR, R> {
386    fn resources_mut(&mut self) -> Option<&mut R> {
387        match self {
388            Self::Idle { resources, .. } | Self::Delivering { resources, .. } => Some(resources),
389            Self::Stopped => None,
390        }
391    }
392
393    fn enqueue_batch(&mut self, events: EventBatch<T, E>) -> EnqueueAction<T, E, OR> {
394        match self {
395            // The delivery loop holds the observer and picks these events up on its own.
396            Self::Delivering { pending, .. } => match pending.push_batch(events) {
397                Some(rejected) => EnqueueAction::Rejected(rejected),
398                None => EnqueueAction::Accepted,
399            },
400            Self::Stopped => EnqueueAction::Rejected(events),
401            Self::Idle { .. } => {
402                // The queue is built from the batch instead of being pushed to and popped from:
403                // a fresh queue rejects nothing, and the first value is delivered directly, so it
404                // never enters the queue and a single-value batch allocates no queue at all.
405                let (first_next, pending) = PendingEvents::from_batch(events);
406                if first_next.is_none() && pending.is_empty() {
407                    // An empty `NextBatch` is a no-op, consistent with the delivering state.
408                    return EnqueueAction::Accepted;
409                }
410
411                // The batch is known to need a delivery only now, so the observer is taken out of
412                // the state only now. It is put back into `Delivering` right below, so nothing the
413                // state owned is dropped under the lock.
414                let Self::Idle {
415                    observer,
416                    resources,
417                } = std::mem::replace(self, Self::Stopped)
418                else {
419                    unreachable!()
420                };
421                *self = Self::Delivering { pending, resources };
422                EnqueueAction::Start {
423                    observer,
424                    first_next,
425                }
426            }
427        }
428    }
429
430    /// Returns the next step of a running delivery loop, moving to `Stopped` and handing the
431    /// resources back to the caller before a terminal event.
432    fn next_step(&mut self, observer: OR) -> Step<T, E, OR, R> {
433        match self {
434            Self::Delivering { pending, .. } => {
435                if let Some(value) = pending.pop_next() {
436                    return Step::Next(observer, value);
437                }
438            }
439            Self::Stopped => return Step::Stopped(observer),
440            // The observer is out of the state only while a delivery is running.
441            Self::Idle { .. } => unreachable!("a delivery loop only runs in the delivering state"),
442        }
443
444        // Everything the state owns is handed to the caller or put back into `Idle` below, so
445        // nothing is dropped under the lock.
446        let Self::Delivering {
447            mut pending,
448            resources,
449        } = std::mem::replace(self, Self::Stopped)
450        else {
451            unreachable!()
452        };
453        match pending.take_termination() {
454            Some(termination) => {
455                // The terminal event is only popped after every value, so this queue is empty and
456                // owns no user value while it is dropped here.
457                drop(pending);
458                Step::Terminate {
459                    observer,
460                    termination,
461                    resources,
462                }
463            }
464            None => {
465                *self = Self::Idle {
466                    observer,
467                    resources,
468                };
469                Step::Parked
470            }
471        }
472    }
473}