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}