Skip to main content

liminal_server/server/connection/
services_cluster.rs

1//! SRV-005: the shared (optionally clustered) channel supervisor used by the
2//! server's connection services.
3//!
4//! Extracted from `services.rs` to keep that file within the 500-line limit. A
5//! single [`ChannelCluster`] holds the one [`ChannelSupervisor`] every configured
6//! channel runs on, plus the [`ClusterResolver`] shared with that supervisor's
7//! scheduler when clustering is configured. Building it is the only place the
8//! choice between a clustered and a plain supervisor is made.
9
10use std::sync::{Arc, OnceLock};
11
12use liminal::channel::{ChannelRestartPolicy, ChannelSupervisor, ClusterObserver};
13use liminal::envelope::Envelope;
14
15use crate::ServerError;
16use crate::cluster::discovery::{ClusterResolver, as_resolver};
17use crate::config::types::ClusterConfig;
18
19/// The shared channel supervisor backing every configured channel (SRV-005).
20///
21/// Plus the cluster resolver when clustering is configured. All channels run on
22/// this ONE supervisor's scheduler so they share the clustered distribution
23/// transport; the resolver is the SAME instance handed to that scheduler, kept
24/// here so the cluster can dial seeds and learn peer names on it.
25#[derive(Clone, Debug)]
26pub struct ChannelCluster {
27    supervisor: ChannelSupervisor,
28    resolver: Option<Arc<ClusterResolver>>,
29    observer: Arc<ServerObserver>,
30}
31
32/// The one observer the channel supervisor reports to (SRV-012 R6): every
33/// subscribe and every departure, explicit or a dropped subscription's
34/// trapped exit, reaches the console hub once (attach and detach) and,
35/// once the cluster has started, `ClusterSync`. Departures arrive on the
36/// channel actor's thread, so nothing here waits on a channel.
37#[derive(Debug, Default)]
38pub struct ServerObserver {
39    cluster: OnceLock<Arc<dyn ClusterObserver>>,
40    console: OnceLock<Arc<crate::console::ConsoleHub>>,
41    /// Hears every subscribe and departure after the console has, console
42    /// subjects included. Test-only: the order a test waits on.
43    #[cfg(test)]
44    witness: OnceLock<Arc<dyn ClusterObserver>>,
45    /// The channel whose departures are held back from the console, and the
46    /// subscriber pids held so far. Test-only: a late detach, forced without
47    /// blocking the channel actor's thread.
48    #[cfg(test)]
49    held_departures: std::sync::Mutex<Option<HeldDepartures>>,
50}
51
52/// A held channel and the subscriber pids whose departures it holds back.
53/// Test-only.
54#[cfg(test)]
55type HeldDepartures = (String, Vec<u64>);
56
57impl ServerObserver {
58    /// Forwards to the cluster's observer from now on. A second cluster is
59    /// refused: the first keeps the channel.
60    pub fn set_cluster(&self, cluster: Arc<dyn ClusterObserver>) -> Result<(), ServerError> {
61        self.cluster
62            .set(cluster)
63            .map_err(|_| ServerError::ClusterJoin {
64                message: "the channel observer already forwards to a cluster".to_owned(),
65            })
66    }
67
68    /// Tells `witness` of every subscribe and departure from now on, after
69    /// the console has heard it. Test-only.
70    #[cfg(test)]
71    pub(crate) fn set_witness(&self, witness: Arc<dyn ClusterObserver>) -> Result<(), ServerError> {
72        self.witness
73            .set(witness)
74            .map_err(|_| ServerError::ClusterJoin {
75                message: "the channel observer already has a witness".to_owned(),
76            })
77    }
78
79    /// Holds back every departure from `channel` before the console hears
80    /// it, until [`Self::release_departures`] hands them on. Test-only.
81    #[cfg(test)]
82    pub(crate) fn hold_departures(&self, channel: &str) -> std::io::Result<()> {
83        *self.held()? = Some((channel.to_owned(), Vec::new()));
84        Ok(())
85    }
86
87    /// Hands every held departure on, as the channel reported it. Test-only.
88    #[cfg(test)]
89    pub(crate) fn release_departures(&self) -> std::io::Result<()> {
90        let held = self.held()?.take();
91        if let Some((channel, pids)) = held {
92            for pid in pids {
93                self.on_unsubscribe(&channel, pid);
94            }
95        }
96        Ok(())
97    }
98
99    /// The held departures. A poisoned lock is a test hook's own failure,
100    /// not a configuration or admission error, so it is reported as one.
101    #[cfg(test)]
102    fn held(&self) -> std::io::Result<std::sync::MutexGuard<'_, Option<HeldDepartures>>> {
103        self.held_departures.lock().map_err(|error| {
104            std::io::Error::other(format!("the held-departures lock was poisoned: {error}"))
105        })
106    }
107
108    /// Keeps a departure from a held channel back, answering whether it did.
109    /// A poisoned lock holds nothing: a callback cannot answer an error.
110    #[cfg(test)]
111    fn held_back(&self, channel: &str, subscriber_pid: u64) -> bool {
112        let Ok(mut held) = self.held_departures.lock() else {
113            return false;
114        };
115        match held.as_mut() {
116            Some((held_channel, pids)) if held_channel == channel => {
117                pids.push(subscriber_pid);
118                true
119            }
120            _ => false,
121        }
122    }
123
124    /// Attaches and detaches the console hub's taps from now on.
125    pub fn set_console(&self, console: Arc<crate::console::ConsoleHub>) -> Result<(), ServerError> {
126        self.console
127            .set(console)
128            .map_err(|_| ServerError::ConfigValidation {
129                message: "the channel observer already feeds a console hub".to_owned(),
130            })
131    }
132}
133
134impl ServerObserver {
135    /// A cluster peer joined or left: one topology change (SRV-012 R7).
136    pub fn peer(&self, name: &str, present: bool) {
137        if let Some(console) = self.console.get()
138            && let Err(error) = console.emit(crate::console::TopologyChange::Peer {
139                name: name.to_owned(),
140                present,
141            })
142        {
143            tracing::warn!(%error, peer = name, "console topology change was not applied");
144        }
145    }
146}
147
148impl ClusterObserver for ServerObserver {
149    fn on_subscribe(&self, channel: &str, subscriber_pid: u64) {
150        if let Some(console) = self.console.get()
151            && let Err(error) = console.attach(channel)
152        {
153            tracing::warn!(%error, channel, "console tap did not attach");
154        }
155        #[cfg(test)]
156        if let Some(witness) = self.witness.get() {
157            witness.on_subscribe(channel, subscriber_pid);
158        }
159        // SRV-012 R6: a console subject is this server's own; it is never
160        // advertised to a peer.
161        if crate::console::tappable(channel)
162            && let Some(cluster) = self.cluster.get()
163        {
164            cluster.on_subscribe(channel, subscriber_pid);
165        }
166    }
167
168    fn on_unsubscribe(&self, channel: &str, subscriber_pid: u64) {
169        #[cfg(test)]
170        if self.held_back(channel, subscriber_pid) {
171            return;
172        }
173        if let Some(console) = self.console.get() {
174            if let Err(error) = console.detach(channel) {
175                tracing::warn!(%error, channel, "console tap did not detach");
176            }
177            if let Err(error) = console.tap().forget_receiver(subscriber_pid) {
178                console.observe_failed();
179                tracing::warn!(%error, channel, "TAP_RECEIVER_FAILED");
180            }
181        }
182        #[cfg(test)]
183        if let Some(witness) = self.witness.get() {
184            witness.on_unsubscribe(channel, subscriber_pid);
185        }
186        // SRV-012 R6: a console subject is this server's own; it is never
187        // advertised to a peer.
188        if crate::console::tappable(channel)
189            && let Some(cluster) = self.cluster.get()
190        {
191            cluster.on_unsubscribe(channel, subscriber_pid);
192        }
193    }
194
195    fn on_publish(&self, channel: &str, envelope: &Envelope) {
196        // SRV-012 R4: console subjects are this server's own and never fan
197        // out to peers.
198        if !crate::console::tappable(channel) {
199            return;
200        }
201        if let Some(cluster) = self.cluster.get() {
202            cluster.on_publish(channel, envelope);
203        }
204    }
205}
206
207impl ChannelCluster {
208    /// The observer the supervisor reports to.
209    #[must_use]
210    pub const fn observer(&self) -> &Arc<ServerObserver> {
211        &self.observer
212    }
213
214    /// The shared channel supervisor.
215    #[must_use]
216    pub const fn supervisor(&self) -> &ChannelSupervisor {
217        &self.supervisor
218    }
219
220    /// The cluster resolver, present only when clustering is configured.
221    #[must_use]
222    pub const fn resolver(&self) -> Option<&Arc<ClusterResolver>> {
223        self.resolver.as_ref()
224    }
225}
226
227/// The channel restart policy used for server-hosted channels. Matches the
228/// library default (one-for-one with a bounded budget) and is passed explicitly
229/// because the distribution-enabled constructor takes a policy.
230fn server_channel_policy() -> ChannelRestartPolicy {
231    ChannelRestartPolicy::default()
232}
233
234/// Builds the shared channel supervisor (SRV-005).
235///
236/// When `cluster` is `Some`, the supervisor's scheduler is distribution-enabled
237/// with the configured node identity and cookie, and a [`ClusterResolver`] is
238/// created and shared between the scheduler and the returned [`ChannelCluster`].
239/// When `None`, an ordinary non-clustered supervisor is built.
240///
241/// # Errors
242/// Returns [`ServerError`] when the underlying scheduler cannot start.
243pub fn build_channel_cluster(
244    cluster: Option<&ClusterConfig>,
245) -> Result<ChannelCluster, ServerError> {
246    let Some(cluster) = cluster else {
247        let supervisor =
248            ChannelSupervisor::with_policy(server_channel_policy()).map_err(|error| {
249                ServerError::ConfigValidation {
250                    message: format!("failed to start channel supervisor: {error}"),
251                }
252            })?;
253        return Ok(with_observer(supervisor, None));
254    };
255    let resolver = Arc::new(ClusterResolver::new());
256    let supervisor = ChannelSupervisor::with_distribution(
257        cluster.node_name.clone(),
258        node_creation(),
259        cluster.cookie.clone(),
260        as_resolver(Arc::clone(&resolver)),
261        server_channel_policy(),
262    )
263    .map_err(|error| ServerError::ClusterJoin {
264        message: format!("failed to start clustered channel supervisor: {error}"),
265    })?;
266    Ok(with_observer(supervisor, Some(resolver)))
267}
268
269fn with_observer(
270    supervisor: ChannelSupervisor,
271    resolver: Option<Arc<ClusterResolver>>,
272) -> ChannelCluster {
273    let observer = Arc::new(ServerObserver::default());
274    supervisor.install_observer(Arc::clone(&observer) as Arc<dyn ClusterObserver>);
275    ChannelCluster {
276        supervisor,
277        resolver,
278        observer,
279    }
280}
281
282/// A non-zero node-incarnation value for beamr distribution. Derived from the
283/// process start time so two incarnations of the same node name on one host get
284/// distinct creations; never zero (beamr reserves 0 for "unknown creation").
285///
286/// The distinct-incarnations property holds only while the clock reads after the
287/// epoch. A clock fault — a host set before 1970 — folds every incarnation to the
288/// same constant value, and beamr then cannot tell a restarted node from its own
289/// predecessor. That case is not detected here and not reported; the non-zero
290/// guarantee still holds, which is what keeps distribution functioning at all.
291fn node_creation() -> u32 {
292    // A pre-epoch clock yields `Err` and is folded to 0 (hence to the constant 1
293    // below) rather than refused: distribution needs SOME non-zero creation to
294    // start, and there is no better value to invent. This is the exact case in
295    // which two incarnations become indistinguishable.
296    let since_epoch = std::time::SystemTime::now()
297        .duration_since(std::time::UNIX_EPOCH)
298        .map_or(0, |duration| duration.as_secs());
299    // Fold the 64-bit seconds into 31 bits (the low word, masked positive) so the
300    // creation is a stable-per-process, non-zero value; the exact bits do not
301    // matter, only that incarnations differ and none is zero.
302    let folded = u32::try_from(since_epoch & u64::from(u32::MAX)).unwrap_or(u32::MAX);
303    (folded & 0x7fff_ffff).max(1)
304}