Skip to main content

rx_rust/utils/
serialized_multicast.rs

1//! A multicast built on a [`SerializedDelivery`] that never terminates.
2//!
3//! [`SerializedMulticast`] is what a subject is made of: it owns the observers, the termination,
4//! the ids that order the observers, and whatever else its host needs (`R`). Everything the host
5//! must read before it decides what to emit is therefore guarded by **one lock** — the delivery's
6//! — so reading the termination, changing the host's own state and queueing the events it produces
7//! is a single atomic step. A host that keeps state of its own next to this one loses that: every
8//! check-then-act pair across two locks is a window another thread, or a re-entrant callback, can
9//! slip through.
10//!
11//! The multicast's termination travels as an `Action::Terminate`, an ordinary value of the
12//! delivery. **Nothing here may send an [`EventBatch::Termination`] to that delivery**: that would
13//! drop the subscribers and the resources, silently killing the multicast. Keeping the delivery
14//! alive is what lets an observer that arrives after the termination still be notified with it,
15//! from the resources.
16//!
17//! Recording the termination and queueing the action that delivers it is one step, and so is
18//! admitting a subscription — nothing can ever be queued behind the `Action::Terminate`, so the
19//! subscribers need no notion of termination of their own. The multicast is consequently
20//! terminated as soon as the termination is *queued*, not when it reaches the observers.
21//!
22//! The observers live inside `Subscribers`, which the delivery loop owns while it delivers, so
23//! subscribing, unsubscribing and terminating all travel as `Action`s and touch the observers
24//! only outside the lock. Unsubscribing is the one that must take effect at once: the disposal
25//! writes a flag the subscribers check before every notification, and the queued `Action::Prune`
26//! only releases the observer afterwards.
27//!
28//! # Replaying to a newcomer
29//!
30//! A host that replays something to a joining observer — the current value, a buffer, the last
31//! value of a completed subject — hands it to [`Admission::Join`] under the lock, and the values
32//! travel *inside* the `Action::Add`. They are delivered by the delivery loop, right before the
33//! entry joins, and therefore in the same serialized stream as everything else: the snapshot the
34//! host took cannot miss a value forwarded after it, nor repeat one forwarded before it. The
35//! observer has moved into the entry by then, so this is also the only place it can be notified
36//! without racing the loop that may already be feeding the other observers.
37//!
38//! The replay is consequently *not* guaranteed to happen before `subscribe` returns: it does when
39//! the delivery is idle, since the action is then applied on the subscribing thread, but a
40//! subscription made while a delivery is running is served by that delivery instead.
41
42use crate::disposable::Disposable;
43use crate::observer::{Flow, Observer, Termination, boxed_observer::BoxedObserver};
44use crate::utils::id_generator::{Id, IdGenerator};
45use crate::utils::mutable::{MutableBool, MutableBoolHelper};
46use crate::utils::pending_events::EventBatch;
47use crate::utils::serialized_delivery::{DeliveryStopped, SerializedDelivery, UpdateOutcome};
48use crate::utils::types::{MaybeSend, Shared};
49use educe::Educe;
50
51/// A shared, serialized delivery of events to many observers, guarding the host's state with it.
52///
53/// `R` is whatever the host owns besides the observers: the current value of a behavior subject,
54/// the buffer of a replay subject, `()` when it owns nothing.
55#[derive(Educe)]
56#[educe(Debug, Clone)]
57pub struct SerializedMulticast<'or, T, E, R = ()>(Delivery<'or, T, E, R>);
58
59/// Serializes every action against every value, and guards the whole state as its resources. Its
60/// termination is never sent: see the module documentation.
61type Delivery<'or, T, E, R> =
62    SerializedDelivery<Action<'or, T, E>, E, Subscribers<'or, T, E>, Resources<E, R>>;
63
64/// Everything the multicast owns besides its observers, guarded by the delivery's lock.
65#[derive(Educe)]
66#[educe(Debug)]
67struct Resources<E, R> {
68    /// Recorded when the `Action::Terminate` is queued: that is what terminates the multicast.
69    termination: Option<Termination<E>>,
70    /// Handed out in subscription order, so that the entries stay sorted by it.
71    ids: IdGenerator,
72    /// The host's own state, read and written under this very lock.
73    host: R,
74}
75
76/// What the host decided, under the lock, for an observer that wants to join.
77#[derive(Educe)]
78#[educe(Debug)]
79pub enum Admission<T, E> {
80    /// Deliver these values to the newcomer, then let it join the multicast. Use an empty [`Vec`],
81    /// which allocates nothing, when there is nothing to replay.
82    Join(Vec<T>),
83    /// Do not join: deliver these values and then this termination, to the newcomer alone.
84    Terminated(Vec<T>, Termination<E>),
85}
86
87/// What [`SerializedMulticast::subscribe_with`] did under the lock, for the observer waiting
88/// outside it.
89#[derive(Educe)]
90#[educe(Debug)]
91enum Admitted<T, E> {
92    /// The entry was queued with this id, carrying the observer and its replay with it.
93    Added(Id),
94    /// The observer stayed behind, to be notified with these events.
95    Terminated(Vec<T>, Termination<E>),
96}
97
98impl<'or, T, E, R> SerializedMulticast<'or, T, E, R> {
99    /// Starts with no observer, no termination, and the host's state parked in the resources.
100    pub fn idle(host: R) -> Self {
101        Self(SerializedDelivery::idle(
102            Subscribers {
103                entries: Vec::new(),
104            },
105            Resources {
106                termination: None,
107                ids: IdGenerator::default(),
108                host,
109            },
110        ))
111    }
112}
113
114impl<'or, T, E, R> SerializedMulticast<'or, T, E, R>
115where
116    T: Clone,
117    E: Clone,
118{
119    /// The termination, once one has been queued.
120    ///
121    /// The resources are gone once the delivery stopped, which only an observer's panic does: the
122    /// multicast is then dead, and reports no termination.
123    pub fn terminated(&self) -> Option<Termination<E>> {
124        self.0
125            .update(|resources| UpdateOutcome::new(resources.termination.clone()))
126            .unwrap_or(None)
127    }
128
129    /// Reads the host's state and the termination together, under the lock.
130    ///
131    /// `read` must not notify anyone and must not drop a value that can re-enter this multicast:
132    /// it runs under the lock. Returns [`DeliveryStopped`], without running `read`, once the
133    /// delivery has stopped.
134    pub fn read<Out>(
135        &self,
136        read: impl FnOnce(&R, Option<&Termination<E>>) -> Out,
137    ) -> Result<Out, DeliveryStopped> {
138        self.0.update(|resources| {
139            UpdateOutcome::new(read(&resources.host, resources.termination.as_ref()))
140        })
141    }
142
143    /// Updates the host's state and queues the events that update produced, under one lock.
144    ///
145    /// `update` sees the termination, so it can decide whether it may emit at all, and describes
146    /// its outcome with an [`UpdateOutcome`] over the *host's* events: a
147    /// [`Termination`](EventBatch::Termination) in that batch is what terminates the multicast,
148    /// recorded here as the action carrying it is queued. A host that emits after the termination
149    /// was queued is a bug — check the termination first, and hand the rejected event to
150    /// [`UpdateOutcome::with_drop_outside`].
151    ///
152    /// `update` must not notify anyone and must not drop a value that can re-enter this multicast:
153    /// it runs under the lock. Returns [`DeliveryStopped`], without running `update`, once the
154    /// delivery has stopped.
155    pub fn update<Out, DO, const EVENTS_DECIDED: bool>(
156        &self,
157        update: impl FnOnce(
158            &mut R,
159            Option<&Termination<E>>,
160        ) -> UpdateOutcome<T, E, Out, DO, EVENTS_DECIDED>,
161    ) -> Result<Out, DeliveryStopped> {
162        self.0.update(|resources| {
163            let (events, drop_outside, result) =
164                update(&mut resources.host, resources.termination.as_ref()).into_parts();
165            // The host's decision is turned into actions under the same lock that recorded the
166            // termination, so nothing can be queued between the two.
167            let outcome = UpdateOutcome::new(result).with_drop_outside(drop_outside);
168            match events {
169                Some(events) => {
170                    outcome.with_events(into_actions(events, &mut resources.termination))
171                }
172                None => outcome.without_events(),
173            }
174        })
175    }
176
177    /// Queues `events` for every observer, dropping them outside the lock once terminated.
178    ///
179    /// Returns whether the multicast still accepts events, which is [`Flow::Stop`] only once it
180    /// has terminated: a multicast with no subscriber left is still open, and a subscriber that
181    /// stops takes only itself away. This is [`Self::update`] for a host that reads nothing and
182    /// decides nothing.
183    pub fn send(&self, events: EventBatch<T, E>) -> Flow {
184        self.update(|_, terminated| {
185            if terminated.is_some() {
186                return UpdateOutcome::new(Flow::Stop)
187                    .with_drop_outside(events)
188                    .without_events();
189            }
190            UpdateOutcome::new(Flow::Continue)
191                .without_drop_outside()
192                .with_events(events)
193        })
194        .unwrap_or(Flow::Stop)
195    }
196
197    /// Subscribes `observer`, replaying nothing and terminating it at once when already terminated.
198    pub fn subscribe(
199        self,
200        observer: impl Observer<T, E> + MaybeSend + 'or,
201    ) -> Option<MulticastDisposal<'or, T, E, R>> {
202        self.subscribe_with(observer, |_, terminated| match terminated {
203            Some(termination) => Admission::Terminated(Vec::new(), termination.clone()),
204            None => Admission::Join(Vec::new()),
205        })
206    }
207
208    /// Subscribes `observer`, letting the host decide what it observes first, under the lock.
209    ///
210    /// Reading the host's state, handing out the id and queueing the entry are one step, so the
211    /// values `admit` snapshots are exactly the ones the newcomer missed: see the module
212    /// documentation. `admit` runs under the lock and must not notify anyone.
213    ///
214    /// Returns the disposal of the subscription, or [`None`] when the observer did not join: it
215    /// has then already been notified, outside the lock.
216    pub fn subscribe_with(
217        self,
218        observer: impl Observer<T, E> + MaybeSend + 'or,
219        admit: impl FnOnce(&mut R, Option<&Termination<E>>) -> Admission<T, E>,
220    ) -> Option<MulticastDisposal<'or, T, E, R>> {
221        let disposed = Shared::new(MutableBool::new(false));
222        // The observer travels with its entry, and stays here when there is no entry to join.
223        let mut observer = Some(observer);
224        let admitted = self.0.update(|resources| {
225            match admit(&mut resources.host, resources.termination.as_ref()) {
226                Admission::Terminated(values, termination) => {
227                    UpdateOutcome::new(Admitted::Terminated(values, termination)).without_events()
228                }
229                Admission::Join(replay) => {
230                    let id = resources.ids.next_id();
231                    let entry = Entry {
232                        id,
233                        is_disposed: disposed.clone(),
234                        observer: BoxedObserver::new(
235                            observer.take().expect("the update runs at most once"),
236                        ),
237                    };
238                    UpdateOutcome::new(Admitted::Added(id))
239                        .with_next_event(Action::Add { entry, replay })
240                }
241            }
242        });
243        match admitted {
244            Ok(Admitted::Added(id)) => Some(MulticastDisposal {
245                delivery: self.0,
246                disposed,
247                id,
248            }),
249            Ok(Admitted::Terminated(values, termination)) => {
250                let mut observer = observer.take().expect("the update left the observer here");
251                // A replayed value can stop the newcomer, which is then dropped where it is
252                // instead of being terminated.
253                let mut flow = Flow::Continue;
254                for value in values {
255                    flow = observer.on_next(value);
256                    if flow.is_stop() {
257                        break;
258                    }
259                }
260                if flow.is_continue() {
261                    observer.on_termination(termination);
262                }
263                None
264            }
265            Err(DeliveryStopped) => {
266                // The delivery only stops once an observer panicked, which kills the multicast:
267                // the termination went with the resources, so this observer is dropped here
268                // instead, outside the lock.
269                debug_assert!(false, "the multicast is dead because an observer panicked");
270                None
271            }
272        }
273    }
274}
275
276/// Translates the host's events into actions, recording the termination as it is queued.
277fn into_actions<'or, T, E>(
278    events: EventBatch<T, E>,
279    termination: &mut Option<Termination<E>>,
280) -> EventBatch<Action<'or, T, E>, E>
281where
282    E: Clone,
283{
284    let mut record = |queued: Termination<E>| {
285        debug_assert!(
286            termination.is_none(),
287            "a host must not emit once the termination was queued"
288        );
289        *termination = Some(queued.clone());
290        // Never an `EventBatch::Termination`: the delivery must stay alive, see the module
291        // documentation.
292        Action::Terminate(queued)
293    };
294    match events {
295        EventBatch::Next(value) => EventBatch::Next(Action::Forward(value)),
296        EventBatch::Termination(termination) => EventBatch::Next(record(termination)),
297        EventBatch::NextAndTermination(value, termination) => {
298            EventBatch::NextBatch(vec![Action::Forward(value), record(termination)])
299        }
300        EventBatch::NextBatch(values) => {
301            EventBatch::NextBatch(values.into_iter().map(Action::Forward).collect())
302        }
303        EventBatch::NextBatchAndTermination(values, termination) => {
304            let mut actions: Vec<_> = values.into_iter().map(Action::Forward).collect();
305            actions.push(record(termination));
306            EventBatch::NextBatch(actions)
307        }
308    }
309}
310
311/// One subscribed observer.
312#[derive(Educe)]
313#[educe(Debug)]
314struct Entry<'or, T, E> {
315    /// Identifies the entry before it has been added, so a subscription can be disposed while its
316    /// `Action::Add` is still queued.
317    id: Id,
318    /// Written by the disposal, read before every notification.
319    is_disposed: Shared<MutableBool>,
320    observer: BoxedObserver<'or, T, E>,
321}
322
323/// Everything that reaches the observers, serialized by the delivery and applied outside its lock.
324#[derive(Educe)]
325#[educe(Debug)]
326enum Action<'or, T, E> {
327    /// Sends a value to every entry that is still subscribed.
328    Forward(T),
329    /// Replays `replay` to the entry's observer, then adds the entry.
330    Add {
331        entry: Entry<'or, T, E>,
332        replay: Vec<T>,
333    },
334    /// Removes the entry with this id, releasing its observer.
335    Prune(Id),
336    /// Terminates every entry. An observer that subscribes afterwards is terminated by
337    /// [`SerializedMulticast::subscribe_with`] instead.
338    Terminate(Termination<E>),
339}
340
341/// Owns the observers, so that they are fed outside the lock that serializes the actions.
342#[derive(Educe)]
343#[educe(Debug)]
344struct Subscribers<'or, T, E> {
345    /// Sorted by id, which is handed out in subscription order: notifications follow that order,
346    /// and an id is found by binary search.
347    entries: Vec<Entry<'or, T, E>>,
348}
349
350impl<'or, T, E> Observer<Action<'or, T, E>, E> for Subscribers<'or, T, E>
351where
352    T: Clone,
353    E: Clone,
354{
355    fn on_next(&mut self, action: Action<'or, T, E>) -> Flow {
356        match action {
357            Action::Forward(value) => self.forward(value),
358            Action::Add { entry, replay } => self.add(entry, replay),
359            Action::Prune(id) => self.prune(id),
360            Action::Terminate(termination) => self.terminate(termination),
361        }
362        // A subscriber that stops takes only itself away, so the multicast itself never stops:
363        // it stays open for the subscribers it still has and for the ones still to come.
364        Flow::Continue
365    }
366
367    fn on_termination(self, _: Termination<E>) {
368        debug_assert!(
369            false,
370            "the multicast's delivery never terminates: see the module documentation"
371        );
372    }
373}
374
375impl<'or, T, E> Subscribers<'or, T, E>
376where
377    T: Clone,
378    E: Clone,
379{
380    fn forward(&mut self, value: T) {
381        // An entry whose observer stops is released right here, like a disposed one: it accepts
382        // nothing more, and must not be terminated either.
383        self.entries.retain_mut(|entry| {
384            if entry.is_disposed.read() {
385                // Unsubscribed, possibly during this very dispatch: its own `Action::Prune` is
386                // what removes it, so it is kept here.
387                return true;
388            }
389            entry.observer.on_next(value.clone()).is_continue()
390        });
391    }
392
393    fn add(&mut self, mut entry: Entry<'or, T, E>, replay: Vec<T>) {
394        // The replay is delivered here, where it is serialized with everything else: the values
395        // the host snapshotted are exactly the ones queued before this action.
396        for value in replay {
397            if entry.is_disposed.read() {
398                return; // Unsubscribed, possibly during this very replay.
399            }
400            if entry.observer.on_next(value).is_stop() {
401                return; // Stopped by the replay itself, so it never joins.
402            }
403        }
404        if entry.is_disposed.read() {
405            return; // Unsubscribed before it was ever added.
406        }
407        debug_assert!(
408            self.entries.last().is_none_or(|last| last.id < entry.id),
409            "the ids are handed out in subscription order"
410        );
411        self.entries.push(entry);
412    }
413
414    fn prune(&mut self, id: Id) {
415        if let Ok(index) = self.entries.binary_search_by_key(&id, |entry| entry.id) {
416            self.entries.remove(index); // Releases the observer here, outside the lock
417        }
418    }
419
420    fn terminate(&mut self, termination: Termination<E>) {
421        // Nothing is queued behind the termination, so emptying the entries here is final.
422        for entry in std::mem::take(&mut self.entries) {
423            if entry.is_disposed.read() {
424                continue; // Unsubscribed, possibly during this very dispatch.
425            }
426            entry.observer.on_termination(termination.clone());
427        }
428    }
429}
430
431/// Unsubscribes one observer from a [`SerializedMulticast`].
432pub struct MulticastDisposal<'or, T, E, R> {
433    delivery: Delivery<'or, T, E, R>,
434    disposed: Shared<MutableBool>,
435    id: Id,
436}
437
438impl<T, E, R> Disposable for MulticastDisposal<'_, T, E, R>
439where
440    T: Clone,
441    E: Clone,
442{
443    fn dispose(self) {
444        // The flag is what stops the events; the action only releases the observer afterwards.
445        self.disposed.write(true);
446        let _ = self.delivery.send(EventBatch::Next(Action::Prune(self.id)));
447    }
448}