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}