rx_rust/utils/subscription_slot.rs
1//! The slot that holds the one inner subscription an operator keeps at a time.
2//!
3//! An operator such as [`Switch`](crate::operators::combining::switch::Switch) or
4//! [`ConcatAll`](crate::operators::combining::concat_all::ConcatAll) is subscribed to at most one
5//! inner observable at a time, and swaps that subscription as the source emits. Subscribing is an
6//! external API call, so it must happen with no lock held, which splits every swap into two
7//! locked steps around an unlocked one: reserve the slot, subscribe, fill the slot. Between the
8//! two steps the inner observable can terminate the operator synchronously and release the slot,
9//! so the fill has to be able to give the new subscription back.
10//!
11//! [`SubscriptionSlot`] is that three-step state machine and nothing else. It carries **no lock of
12//! its own**: it lives inside a model already guarded by the delivery lock — see
13//! [`SubscriptionContext::update`](crate::utils::subscribe_with_context::SubscriptionContext::update)
14//! — and every method takes `&mut self`. It also disposes nothing: each method hands the
15//! subscription it evicts back to the caller, which passes it to
16//! [`UpdateOutcome::with_drop_outside`](crate::utils::serialized_delivery::UpdateOutcome::with_drop_outside)
17//! so it is dropped outside the lock.
18//!
19//! # When a slot is the right type
20//!
21//! [`Reserved`](SubscriptionSlot::Reserved) is the whole of what this type adds. `Idle` and
22//! `Active` are what an `Option<D>` already says, so a host that needs only those two keeps its
23//! `Option`. Reach for a slot only when the host must tell "a value is on its way" apart from
24//! "nothing is held", which takes all three of:
25//!
26//! - the value is built by an external call that has to run with the lock released, so the state
27//! is observable by someone else while the build is in flight;
28//! - the slot can be released inside that window, and filling a released slot would install a
29//! value that is already dead — in a host that keeps one task alive while there is work to do,
30//! that means storing the handle of a task that already stopped, after which no new task is
31//! ever started and the queued events stall;
32//! - there is exactly one such value. Several of them are keyed, and an absent key already means
33//! `Idle`, so each entry collapses back to `Option` — see the maps in
34//! [`MergeAll`](crate::operators::combining::merge_all::MergeAll) and
35//! [`Amb`](crate::operators::conditional_boolean::amb::Amb), which run this same reserve/fill
36//! protocol without this type.
37//!
38//! When the third state is unreachable, a slot only widens the state space with a variant the
39//! host's invariants forbid, which is the opposite of what it is for.
40//!
41//! # Why this is not [`SharedDisposal`](crate::disposable::shared_disposal::SharedDisposal)
42//!
43//! The two are the same shape — idle, building, active — but not the same contract, so neither is
44//! built on the other:
45//!
46//! - `SharedDisposal` is itself a disposal, so it needs a terminal `Disposed` state that absorbs
47//! later replacements. A slot needs none: its host stops through the delivery, which stops
48//! running updates at all ([`DeliveryStopped`](crate::utils::serialized_delivery::DeliveryStopped)),
49//! and whatever is left in the slot is disposed when the model is dropped.
50//! - `SharedDisposal` releases its own lock while the builder runs, so a second `replace` can race
51//! the first and it needs a generation id to tell whether a finished build is still current. A
52//! slot cannot be raced: the reserve/fill pair is serialized by the delivery lock, and
53//! [`Reserved`](SubscriptionSlot::Reserved) makes a second reserve unreachable.
54//!
55//! Merging them would give every slot a state it never enters and an id it never reads.
56
57use educe::Educe;
58
59/// The one inner subscription an operator holds at a time.
60///
61/// `D` is the value being held — a [`Subscription`](crate::observable::Subscription) in every
62/// current use, which disposes when dropped.
63#[derive(Educe)]
64#[educe(Debug)]
65pub enum SubscriptionSlot<D> {
66 /// Nothing is subscribed and nothing is being subscribed.
67 Idle,
68 /// A subscription is being built with the lock released. Reserving the slot up front is what
69 /// tells a concurrent update that a subscription is on its way.
70 Reserved,
71 /// A subscription is held.
72 Active(D),
73}
74
75impl<D> SubscriptionSlot<D> {
76 /// Whether the slot is [`Idle`](Self::Idle).
77 pub fn is_idle(&self) -> bool {
78 matches!(self, Self::Idle)
79 }
80
81 /// Whether the slot is [`Reserved`](Self::Reserved).
82 pub fn is_reserved(&self) -> bool {
83 matches!(self, Self::Reserved)
84 }
85
86 /// Reserves the slot for a subscription that is about to be built, and gives back the
87 /// subscription it replaces, if any, to drop outside the lock.
88 ///
89 /// # Panics
90 ///
91 /// Panics if the slot is already reserved: only one build can be in flight, since the caller
92 /// reserves under the same lock that serializes its updates.
93 pub fn reserve(&mut self) -> Option<D> {
94 match std::mem::replace(self, Self::Reserved) {
95 Self::Idle => None,
96 Self::Active(value) => Some(value),
97 Self::Reserved => unreachable!("the slot is already reserved"),
98 }
99 }
100
101 /// Reserves the slot only if it is [`Idle`](Self::Idle), so nothing is ever evicted, and
102 /// returns whether it did.
103 ///
104 /// This is the form used by a host that keeps one task alive while there is work to do: it
105 /// starts a task only when none is running, instead of replacing a running one.
106 #[must_use = "a reservation that is not followed by a build leaves the slot reserved forever"]
107 pub fn reserve_if_idle(&mut self) -> bool {
108 match self {
109 Self::Idle => {
110 *self = Self::Reserved;
111 true
112 }
113 Self::Reserved | Self::Active(_) => false,
114 }
115 }
116
117 /// Fills a reserved slot with the subscription that was built.
118 ///
119 /// Returns `Some` when the slot was released while the build was running — the operator
120 /// terminated the inner subscription in the meantime — in which case the value was not stored
121 /// and is given back to drop outside the lock.
122 ///
123 /// # Panics
124 ///
125 /// Panics if the slot is already active, which would mean a build was never reserved.
126 pub fn fill(&mut self, value: D) -> Option<D> {
127 match self {
128 Self::Reserved => {
129 *self = Self::Active(value);
130 None
131 }
132 Self::Idle => Some(value),
133 Self::Active(_) => unreachable!("the slot was filled without being reserved"),
134 }
135 }
136
137 /// Releases the slot, giving back the held subscription, if any, to drop outside the lock.
138 ///
139 /// Releasing a [`Reserved`](Self::Reserved) slot returns `None` and makes the pending
140 /// [`fill`](Self::fill) give its subscription back instead of storing it.
141 pub fn release(&mut self) -> Option<D> {
142 match std::mem::replace(self, Self::Idle) {
143 Self::Active(value) => Some(value),
144 Self::Idle | Self::Reserved => None,
145 }
146 }
147}