Skip to main content

liminal_server/server/connection/
supervisor.rs

1use std::collections::HashMap;
2use std::net::{SocketAddr, TcpStream};
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
5use std::sync::{Arc, Mutex, MutexGuard};
6use std::time::Duration;
7
8use beamr::atom::{Atom, AtomTable};
9use beamr::module::ModuleRegistry;
10use beamr::native::native_process::NativeHandlerFactory;
11use beamr::process::ExitReason;
12use beamr::scheduler::{Scheduler, SchedulerConfig};
13
14use liminal::protocol::WorkerRegistration;
15
16use super::notifier::ConnectionNotifier;
17use super::process::ConnectionProcess;
18use super::services::{ConnectionServices, LiminalConnectionServices};
19use crate::ServerError;
20use crate::config::types::ServerConfig;
21
22const CONNECTION_SCHEDULER_THREADS: usize = 4;
23const CONNECTION_SHUTDOWN_CONTROL_ATOM: &str = "liminal_server_connection_shutdown_control";
24
25#[cfg(test)]
26#[path = "supervisor_tests.rs"]
27mod tests;
28
29/// Supervisor that owns the beamr scheduler for per-connection processes.
30#[derive(Clone, Debug)]
31pub struct ConnectionSupervisor {
32    inner: Arc<SupervisorInner>,
33}
34
35impl ConnectionSupervisor {
36    /// Creates a connection supervisor backed by the configured liminal channels.
37    ///
38    /// # Errors
39    /// Returns [`ServerError`] when channel initialization or scheduler startup fails.
40    pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
41        let services = Arc::new(LiminalConnectionServices::from_config(config)?);
42        // The configured token (if any) is carried opaquely as bytes for a
43        // constant-time comparison against the handshake's `auth_token`. Absent
44        // `[auth]` leaves it `None`, so the connection stays open-access.
45        let auth_token = config
46            .auth
47            .as_ref()
48            .map(|auth| auth.token.clone().into_bytes());
49        SupervisorInner::new(services, None, auth_token).map(|inner| Self {
50            inner: Arc::new(inner),
51        })
52    }
53
54    /// Creates a connection supervisor with no configured channels.
55    ///
56    /// # Errors
57    /// Returns [`ServerError`] when scheduler startup fails.
58    pub fn new() -> Result<Self, ServerError> {
59        Self::with_services(Arc::new(LiminalConnectionServices::empty()?))
60    }
61
62    /// Creates a connection supervisor using an explicit service adapter.
63    ///
64    /// # Errors
65    /// Returns [`ServerError`] when scheduler startup fails.
66    pub fn with_services(services: Arc<dyn ConnectionServices>) -> Result<Self, ServerError> {
67        SupervisorInner::new(services, None, None).map(|inner| Self {
68            inner: Arc::new(inner),
69        })
70    }
71
72    /// Creates a connection supervisor with an explicit service adapter and the
73    /// configured connection auth token.
74    ///
75    /// This is the production constructor for callers that build services
76    /// themselves (the runtime needs the shared channel cluster before the
77    /// supervisor takes ownership) and therefore cannot use
78    /// [`Self::from_config`]: without it the configured `[auth]` token would be
79    /// silently dropped and the server would run open-access.
80    ///
81    /// # Errors
82    /// Returns [`ServerError`] when scheduler startup fails.
83    pub fn with_services_and_auth(
84        services: Arc<dyn ConnectionServices>,
85        auth_token: Option<Vec<u8>>,
86    ) -> Result<Self, ServerError> {
87        SupervisorInner::new(services, None, auth_token).map(|inner| Self {
88            inner: Arc::new(inner),
89        })
90    }
91
92    /// Creates a connection supervisor with an explicit service adapter and a
93    /// connection-keyed worker-registration notifier.
94    ///
95    /// The `notifier` is invoked when a worker registers on a connection and when
96    /// such a connection closes. Supervisors built via [`Self::with_services`],
97    /// [`Self::from_config`], or [`Self::new`] carry no notifier, so liminal still
98    /// runs standalone; a `WorkerRegister` frame is then accepted without any
99    /// application callback.
100    ///
101    /// # Errors
102    /// Returns [`ServerError`] when scheduler startup fails.
103    pub fn with_services_and_notifier(
104        services: Arc<dyn ConnectionServices>,
105        notifier: Arc<dyn ConnectionNotifier>,
106    ) -> Result<Self, ServerError> {
107        SupervisorInner::new(services, Some(notifier), None).map(|inner| Self {
108            inner: Arc::new(inner),
109        })
110    }
111
112    /// Spawns one supervised beamr process that owns `stream`.
113    ///
114    /// # Errors
115    /// Returns [`ServerError`] when stream configuration or beamr spawn fails.
116    pub fn spawn_connection(&self, stream: TcpStream) -> Result<ConnectionHandle, ServerError> {
117        self.inner.spawn_connection(stream)
118    }
119
120    /// Returns the underlying beamr scheduler.
121    #[must_use]
122    pub fn scheduler(&self) -> Arc<Scheduler> {
123        Arc::clone(&self.inner.scheduler)
124    }
125
126    /// Reaps connection processes that have exited outside the normal handler path.
127    #[must_use]
128    pub fn reap_crashed_connections(&self) -> usize {
129        self.inner.runtime.reap_crashed(&self.inner.scheduler)
130    }
131
132    /// Returns true when `pid` is still tracked by the supervisor.
133    #[must_use]
134    pub fn is_tracked(&self, pid: u64) -> bool {
135        self.inner.runtime.contains(pid)
136    }
137
138    /// Returns the number of tracked live connections.
139    #[must_use]
140    pub fn active_connection_count(&self) -> usize {
141        self.inner.runtime.active_count()
142    }
143
144    /// Returns the beamr process ids of the currently tracked live connections.
145    ///
146    /// Useful for addressing a specific connection — e.g. as the `pid` argument to
147    /// [`push_to_connection`](Self::push_to_connection) when the caller knows there
148    /// is a single connected client.
149    #[must_use]
150    pub fn active_connection_pids(&self) -> Vec<u64> {
151        self.inner
152            .runtime
153            .active_connections()
154            .into_iter()
155            .map(|connection| connection.pid)
156            .collect()
157    }
158
159    /// Broadcasts a best-effort shutdown notification to active connections.
160    ///
161    /// Connections with no active subscriptions ignore the notification. Failures
162    /// to enqueue the control message are logged and skipped; they are not retried.
163    pub fn notify_shutdown_subscribers(&self) {
164        self.inner
165            .broadcast_control(&ConnectionControl::NotifyShutdown);
166    }
167
168    /// Sends a force-close control message to every tracked connection process.
169    ///
170    /// Each live process attempts one shutdown notification before closing its
171    /// stream and exiting normally. Enqueue failures are logged and skipped.
172    pub fn force_close_active_connections(&self) {
173        for connection in self.inner.runtime.active_connections() {
174            tracing::warn!(
175                connection_pid = connection.pid,
176                peer_addr = ?connection.peer_addr,
177                "forcefully closing connection after drain timeout"
178            );
179            if !self
180                .inner
181                .enqueue_control(connection.pid, ConnectionControl::ForceClose)
182            {
183                tracing::warn!(
184                    connection_pid = connection.pid,
185                    peer_addr = ?connection.peer_addr,
186                    "failed to request forceful connection close; process is not live"
187                );
188            }
189        }
190    }
191
192    /// Pushes an opaque payload to a specific connected client over that client's
193    /// existing connection and returns an awaiter for the client's correlated reply.
194    ///
195    /// This is the server-initiated leg (server-to-client), the inverse of every
196    /// other request frame. It allocates a correlation id, registers a one-shot
197    /// reply slot keyed by that id, and enqueues a [`ConnectionControl::Push`] for
198    /// the connection process owning `pid`; that process writes a [`Frame::Push`]
199    /// out on its socket. When the client answers with a `PushReply` carrying the
200    /// same correlation id, the connection process resolves the awaiter's slot. The
201    /// returned [`PushReplyAwaiter`] blocks (bounded) for that reply.
202    ///
203    /// # Errors
204    /// Returns [`ServerError`] when the correlation id cannot be allocated, the
205    /// reply slot cannot be registered, or the control message cannot be enqueued
206    /// for the (possibly already-gone) connection process.
207    pub fn push_to_connection(
208        &self,
209        pid: u64,
210        payload: Vec<u8>,
211    ) -> Result<PushReplyAwaiter, ServerError> {
212        let correlation_id = self.inner.runtime.next_push_correlation_id();
213        let receiver = self.inner.runtime.register_push(pid, correlation_id)?;
214        let control = ConnectionControl::Push {
215            correlation_id,
216            payload,
217        };
218        if self.inner.enqueue_control(pid, control) {
219            Ok(PushReplyAwaiter {
220                correlation_id,
221                receiver,
222            })
223        } else {
224            // The process is gone; drop the now-unreachable reply slot so it cannot
225            // leak in the correlation registry.
226            self.inner.runtime.cancel_push(correlation_id);
227            Err(ServerError::ListenerAccept {
228                message: format!("cannot push to connection process {pid}: process is not live"),
229            })
230        }
231    }
232
233    /// Flushes durable channel state through the configured liminal services.
234    ///
235    /// # Errors
236    /// Returns [`ServerError::ShutdownFlush`] when the underlying service flush fails.
237    pub fn flush_durable_state(&self) -> Result<(), ServerError> {
238        self.inner.runtime.services().flush_durable_state()
239    }
240
241    /// Stops the beamr scheduler used by connection processes.
242    pub fn shutdown(&self) {
243        self.inner.scheduler.shutdown();
244    }
245}
246
247/// Handle for one supervised connection process.
248#[derive(Clone, Debug)]
249pub struct ConnectionHandle {
250    pid: u64,
251    peer_addr: Option<SocketAddr>,
252    supervisor: Arc<SupervisorInner>,
253}
254
255impl ConnectionHandle {
256    /// Returns the beamr process id for this connection.
257    #[must_use]
258    pub const fn pid(&self) -> u64 {
259        self.pid
260    }
261
262    /// Returns the peer address if it was available from the accepted stream.
263    #[must_use]
264    pub const fn peer_addr(&self) -> Option<SocketAddr> {
265        self.peer_addr
266    }
267
268    /// Returns whether the beamr process is still live.
269    #[must_use]
270    pub fn is_live(&self) -> bool {
271        self.supervisor
272            .scheduler
273            .process_table()
274            .get(self.pid)
275            .is_some()
276    }
277
278    /// Requests an error exit for tests and supervisor control paths.
279    ///
280    /// # Errors
281    /// Returns [`ServerError`] when the process is no longer live.
282    pub fn request_crash(&self) -> Result<(), ServerError> {
283        if self
284            .supervisor
285            .scheduler
286            .enqueue_atom_message(self.pid, Atom::ERROR)
287        {
288            Ok(())
289        } else {
290            Err(ServerError::ListenerAccept {
291                message: format!("connection process {} is not live", self.pid),
292            })
293        }
294    }
295}
296
297/// Awaits the correlated reply to a single server-initiated push.
298///
299/// Returned by [`ConnectionSupervisor::push_to_connection`]. The reply slot is
300/// resolved when the originating connection process receives a `PushReply` frame
301/// carrying the same correlation id, so [`PushReplyAwaiter::receive`] blocks
302/// (bounded) for that one correlated answer.
303#[derive(Debug)]
304pub struct PushReplyAwaiter {
305    correlation_id: u64,
306    receiver: Receiver<Vec<u8>>,
307}
308
309impl PushReplyAwaiter {
310    /// Returns the correlation id this awaiter is matched on.
311    #[must_use]
312    pub const fn correlation_id(&self) -> u64 {
313        self.correlation_id
314    }
315
316    /// Blocks up to `timeout` for the client's correlated reply payload.
317    ///
318    /// # Errors
319    /// Returns [`ServerError::PushReplyTimeout`] when no reply arrives within
320    /// `timeout` (the worker is connected but slow), or
321    /// [`ServerError::PushReplyDisconnected`] when the connection process dropped
322    /// the reply slot (the connection closed — the prompt worker-death signal).
323    /// The two are distinct variants so callers classify by type, not message.
324    pub fn receive(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
325        self.receiver
326            .recv_timeout(timeout)
327            .map_err(|error| match error {
328                RecvTimeoutError::Timeout => ServerError::PushReplyTimeout {
329                    correlation_id: self.correlation_id,
330                },
331                RecvTimeoutError::Disconnected => ServerError::PushReplyDisconnected {
332                    correlation_id: self.correlation_id,
333                },
334            })
335    }
336}
337
338pub(super) struct SupervisorInner {
339    scheduler: Arc<Scheduler>,
340    runtime: Arc<ConnectionRuntime>,
341}
342
343impl std::fmt::Debug for SupervisorInner {
344    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        formatter
346            .debug_struct("SupervisorInner")
347            .field("runtime", &self.runtime)
348            .finish_non_exhaustive()
349    }
350}
351
352impl SupervisorInner {
353    fn new(
354        services: Arc<dyn ConnectionServices>,
355        notifier: Option<Arc<dyn ConnectionNotifier>>,
356        auth_token: Option<Vec<u8>>,
357    ) -> Result<Self, ServerError> {
358        let atoms = AtomTable::with_common_atoms();
359        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
360        let registry = Arc::new(ModuleRegistry::new());
361
362        let scheduler = Scheduler::new(
363            SchedulerConfig {
364                thread_count: Some(CONNECTION_SCHEDULER_THREADS),
365                ..SchedulerConfig::default()
366            },
367            registry,
368        )
369        .map_err(|message| ServerError::ListenerAccept {
370            message: format!("failed to start connection scheduler: {message}"),
371        })?;
372        Ok(Self {
373            scheduler: Arc::new(scheduler),
374            runtime: Arc::new(ConnectionRuntime::new(
375                services,
376                control_atom,
377                notifier,
378                auth_token,
379            )),
380        })
381    }
382
383    fn spawn_connection(
384        self: &Arc<Self>,
385        stream: TcpStream,
386    ) -> Result<ConnectionHandle, ServerError> {
387        stream
388            .set_nonblocking(true)
389            .map_err(|error| ServerError::ListenerAccept {
390                message: format!("failed to configure connection stream: {error}"),
391            })?;
392        let peer_addr = stream.peer_addr().ok();
393        let holder = Arc::new(Mutex::new(Some(stream)));
394        let runtime = Arc::clone(&self.runtime);
395        let process_holder = Arc::clone(&holder);
396        let factory: NativeHandlerFactory = Box::new(move || {
397            Box::new(ConnectionProcess::from_holder(
398                Arc::clone(&runtime),
399                peer_addr,
400                &process_holder,
401            ))
402        });
403        let pid =
404            self.scheduler
405                .spawn_native(factory)
406                .map_err(|error| ServerError::ListenerAccept {
407                    message: format!("failed to spawn connection process: {error}"),
408                })?;
409        self.runtime.register(pid, peer_addr)?;
410        Ok(ConnectionHandle {
411            pid,
412            peer_addr,
413            supervisor: Arc::clone(self),
414        })
415    }
416
417    fn broadcast_control(&self, control: &ConnectionControl) {
418        for connection in self.runtime.active_connections() {
419            if !self.enqueue_control(connection.pid, control.clone()) {
420                tracing::debug!(
421                    connection_pid = connection.pid,
422                    peer_addr = ?connection.peer_addr,
423                    ?control,
424                    "connection control message skipped because process is not live"
425                );
426            }
427        }
428    }
429
430    fn enqueue_control(&self, pid: u64, control: ConnectionControl) -> bool {
431        // Keep a key for the failure-path removal before the control is moved into
432        // the queue, so a non-`Copy` (push) control can still be located and pulled
433        // back out if the scheduler wakeup fails.
434        let removal_key = control.clone();
435        if self.runtime.push_control(pid, control).is_err() {
436            return false;
437        }
438        if self
439            .scheduler
440            .enqueue_atom_message(pid, self.runtime.control_atom())
441        {
442            true
443        } else {
444            self.runtime.remove_control(pid, &removal_key);
445            false
446        }
447    }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub(super) enum ConnectionControl {
452    NotifyShutdown,
453    ForceClose,
454    /// Server-initiated push of an opaque payload, correlated by `correlation_id`,
455    /// to be written out as a [`Frame::Push`] by the receiving connection process.
456    Push {
457        correlation_id: u64,
458        payload: Vec<u8>,
459    },
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub struct ActiveConnection {
464    pid: u64,
465    peer_addr: Option<SocketAddr>,
466}
467
468#[derive(Debug)]
469pub(super) struct ConnectionRuntime {
470    services: Arc<dyn ConnectionServices>,
471    records: Mutex<HashMap<u64, ConnectionRecord>>,
472    controls: Mutex<Vec<QueuedConnectionControl>>,
473    control_atom: Atom,
474    /// One-shot reply slots for in-flight server pushes, keyed by correlation id.
475    /// The supervisor registers a slot in `push_to_connection`; the connection
476    /// process resolves it when the matching `PushReply` frame arrives. Each slot
477    /// records the owning connection pid so the close path can drop a connection's
478    /// outstanding slots and wake their awaiters with a prompt disconnected error.
479    push_replies: Mutex<HashMap<u64, PendingPush>>,
480    /// Monotonic source of push correlation ids. Server-allocated, so it never
481    /// collides with a client-chosen id on this connection.
482    next_push_id: AtomicU64,
483    /// Optional application hook invoked on worker registration and on the close
484    /// of a connection that had registered. `None` keeps liminal standalone: a
485    /// `WorkerRegister` is accepted with no callback.
486    notifier: Option<Arc<dyn ConnectionNotifier>>,
487    /// Configured connection auth token (the `[auth]` section's token as opaque
488    /// bytes). `Some` gates the `Connect` handshake — the frame's `auth_token` must
489    /// match under a constant-time comparison; `None` leaves the server open-access,
490    /// byte-identical to the pre-auth behaviour.
491    auth_token: Option<Vec<u8>>,
492}
493
494impl ConnectionRuntime {
495    fn new(
496        services: Arc<dyn ConnectionServices>,
497        control_atom: Atom,
498        notifier: Option<Arc<dyn ConnectionNotifier>>,
499        auth_token: Option<Vec<u8>>,
500    ) -> Self {
501        Self {
502            services,
503            records: Mutex::new(HashMap::new()),
504            controls: Mutex::new(Vec::new()),
505            control_atom,
506            push_replies: Mutex::new(HashMap::new()),
507            next_push_id: AtomicU64::new(1),
508            notifier,
509            auth_token,
510        }
511    }
512
513    /// Builds a runtime wrapping `services` for unit tests that exercise
514    /// `apply_frame` without a live scheduler. Uses a fresh interned control atom
515    /// and no notifier.
516    #[cfg(test)]
517    pub(super) fn for_tests(services: Arc<dyn ConnectionServices>) -> Self {
518        let atoms = AtomTable::with_common_atoms();
519        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
520        Self::new(services, control_atom, None, None)
521    }
522
523    /// Builds a runtime wrapping `services` with a configured auth `token` for unit
524    /// tests that exercise the `Connect` handshake enforcement without a live
525    /// scheduler. Uses a fresh interned control atom and no notifier.
526    #[cfg(test)]
527    pub(super) fn for_tests_with_auth_token(
528        services: Arc<dyn ConnectionServices>,
529        token: Vec<u8>,
530    ) -> Self {
531        let atoms = AtomTable::with_common_atoms();
532        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
533        Self::new(services, control_atom, None, Some(token))
534    }
535
536    /// Builds a runtime wrapping `services` with a `notifier` for unit tests that
537    /// exercise `apply_frame` and the close path without a live scheduler.
538    #[cfg(test)]
539    pub(super) fn for_tests_with_notifier(
540        services: Arc<dyn ConnectionServices>,
541        notifier: Arc<dyn ConnectionNotifier>,
542    ) -> Self {
543        let atoms = AtomTable::with_common_atoms();
544        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
545        Self::new(services, control_atom, Some(notifier), None)
546    }
547
548    pub(super) fn services(&self) -> &dyn ConnectionServices {
549        self.services.as_ref()
550    }
551
552    /// Returns the configured connection auth token as opaque bytes, or `None` when
553    /// no `[auth]` section was configured (open access).
554    pub(super) fn auth_token(&self) -> Option<&[u8]> {
555        self.auth_token.as_deref()
556    }
557
558    /// Returns the configured connection-keyed notifier, if any.
559    pub(super) fn notifier(&self) -> Option<&Arc<dyn ConnectionNotifier>> {
560        self.notifier.as_ref()
561    }
562
563    /// Offers a channel publish to the notifier's observability-drain tap, returning
564    /// `true` when the application consumed it (so the connection process skips the
565    /// normal fan-out). `false` when no notifier is installed (liminal standalone) or
566    /// the notifier did not recognise the channel, so the caller can invoke it
567    /// unconditionally and fall through to the normal publish path.
568    pub(super) fn notifier_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
569        self.notifier
570            .as_ref()
571            .is_some_and(|notifier| notifier.on_channel_publish(pid, channel, payload))
572    }
573
574    /// Stores `registration` on the connection record for `pid`, so the close
575    /// path can later fire `on_worker_unregistered` for exactly the connections
576    /// that registered. A missing record (the connection already closed) is a
577    /// no-op.
578    ///
579    /// # Errors
580    /// Returns [`ServerError`] when the connection registry mutex is poisoned.
581    pub(super) fn set_registration(
582        &self,
583        pid: u64,
584        registration: WorkerRegistration,
585    ) -> Result<(), ServerError> {
586        if let Some(record) = lock(&self.records, "connection registry")?.get_mut(&pid) {
587            record.registration = Some(registration);
588        }
589        Ok(())
590    }
591
592    /// Allocates the next monotonic push correlation id.
593    fn next_push_correlation_id(&self) -> u64 {
594        self.next_push_id.fetch_add(1, Ordering::Relaxed)
595    }
596
597    /// Registers a one-shot reply slot for `correlation_id`, owned by connection
598    /// `pid`, and returns its receiver. The connection process resolves the slot
599    /// via [`resolve_push`]; the close path drops the connection's outstanding
600    /// slots via [`cancel_pushes_for_connection`].
601    ///
602    /// # Errors
603    /// Returns [`ServerError`] when the correlation registry mutex is poisoned.
604    fn register_push(
605        &self,
606        pid: u64,
607        correlation_id: u64,
608    ) -> Result<Receiver<Vec<u8>>, ServerError> {
609        let (sender, receiver) = channel();
610        lock(&self.push_replies, "push correlation registry")?
611            .insert(correlation_id, PendingPush { pid, sender });
612        Ok(receiver)
613    }
614
615    /// Drops a registered reply slot without resolving it (the push could not be
616    /// delivered, so no reply can ever arrive). Dropping the slot's `Sender` wakes
617    /// the awaiter with a disconnected error.
618    pub(super) fn cancel_push(&self, correlation_id: u64) {
619        if let Ok(mut slots) = self.push_replies.lock() {
620            slots.remove(&correlation_id);
621        }
622    }
623
624    /// Drops every reply slot owned by connection `pid`, waking each awaiter with a
625    /// disconnected error (the dropped `Sender` disconnects the awaiter's
626    /// `Receiver`). Called from the close path so a connection that exits with
627    /// in-flight pushes signals worker death immediately instead of leaving each
628    /// awaiter to block the full push-reply timeout. A slot that [`resolve_push`]
629    /// already removed is gone, so it is untouched here; an unknown pid is a no-op.
630    fn cancel_pushes_for_connection(&self, pid: u64) {
631        if let Ok(mut slots) = self.push_replies.lock() {
632            slots.retain(|_correlation_id, pending| pending.pid != pid);
633        }
634    }
635
636    /// Resolves the reply slot for `correlation_id` with the client's reply
637    /// payload, waking the [`PushReplyAwaiter`]. Called by the connection process
638    /// when a correlated `PushReply` frame arrives. A missing slot (already
639    /// resolved, cancelled, or unknown id) is ignored.
640    pub(super) fn resolve_push(&self, correlation_id: u64, payload: Vec<u8>) {
641        let pending = self
642            .push_replies
643            .lock()
644            .ok()
645            .and_then(|mut slots| slots.remove(&correlation_id));
646        if let Some(pending) = pending {
647            // The receiver may already be gone if the awaiter timed out; a failed
648            // send is benign (the reply is simply discarded).
649            pending.sender.send(payload).ok();
650        }
651    }
652
653    pub(super) const fn control_atom(&self) -> Atom {
654        self.control_atom
655    }
656
657    /// Sole registration path for a connection: the spawn thread inserts the
658    /// record synchronously, before `spawn_connection` returns the handle, so
659    /// `is_tracked`/`active_connection_count` reflect the connection
660    /// immediately. The connection handler never writes the registry (it only
661    /// reads via `mark_crashed`/`finish`), so there is a single writer here and
662    /// no register/ensure-register race.
663    ///
664    /// Ordering note: `spawn_native` only enqueues the process, so its first
665    /// slice may run on another worker thread before this insert lands. If that
666    /// first slice exits immediately (e.g. a missing-stream crash) its
667    /// `mark_crashed`/`finish` removes nothing and this insert then leaves a
668    /// record for an already-dead pid. That orphan is self-healing:
669    /// `reap_crashed`, driven continuously by the listener loop, removes any
670    /// record whose pid is absent from the scheduler process table.
671    fn register(&self, pid: u64, peer_addr: Option<SocketAddr>) -> Result<(), ServerError> {
672        lock(&self.records, "connection registry")?.insert(
673            pid,
674            ConnectionRecord {
675                peer_addr,
676                registration: None,
677            },
678        );
679        // Single-writer insert (see doc above) pairs one gauge increment with the
680        // decrement in `remove`, keeping `liminal_connections_active` equal to the
681        // live record count on every teardown route.
682        crate::metrics::connection_spawned();
683        Ok(())
684    }
685
686    pub(super) fn mark_crashed(&self, pid: u64, reason: ExitReason, peer_addr: Option<SocketAddr>) {
687        let removed = self.remove(pid).unwrap_or(ConnectionRecord {
688            peer_addr,
689            registration: None,
690        });
691        self.fire_unregistered(pid, &removed);
692        tracing::warn!(
693            connection_pid = pid,
694            peer_addr = ?removed.peer_addr,
695            reason = ?reason,
696            "connection process crashed"
697        );
698    }
699
700    pub(super) fn finish(&self, pid: u64) {
701        if let Some(removed) = self.remove(pid) {
702            self.fire_unregistered(pid, &removed);
703        }
704    }
705
706    /// Invokes `on_worker_unregistered` for a removed connection record that
707    /// carried a worker registration. A record with no registration (a plain
708    /// connection, or a worker connection that never registered) is a no-op, so
709    /// only connections that actually registered deregister.
710    fn fire_unregistered(&self, pid: u64, record: &ConnectionRecord) {
711        if record.registration.is_some() {
712            if let Some(notifier) = self.notifier.as_ref() {
713                notifier.on_worker_unregistered(pid);
714            }
715        }
716    }
717
718    fn reap_crashed(&self, scheduler: &Scheduler) -> usize {
719        let pids = match self.records.lock() {
720            Ok(records) => records.keys().copied().collect::<Vec<_>>(),
721            Err(error) => {
722                tracing::warn!(%error, "connection registry unavailable during crash reap");
723                return 0;
724            }
725        };
726        let mut reaped = 0;
727        for pid in pids {
728            if scheduler.process_table().get(pid).is_none() {
729                let removed = self.remove(pid);
730                if let Some(record) = removed.as_ref() {
731                    self.fire_unregistered(pid, record);
732                }
733                let peer_addr = removed.and_then(|record| record.peer_addr);
734                // This process exited without ever reaching `mark_crashed`/`finish`
735                // (e.g. the beamr scheduler terminated it externally). beamr records
736                // the real `ExitReason` in its private `exit_tombstones` map, but its
737                // public `Scheduler` API exposes no non-blocking accessor for it
738                // (only `run_until_exit`, which blocks). So we cannot recover the true
739                // reason here; log a truthful, specific message rather than the
740                // misleading literal "unknown". If beamr later grows a public,
741                // non-blocking exit-reason query for a dead pid, read it here instead.
742                tracing::warn!(
743                    connection_pid = pid,
744                    ?peer_addr,
745                    reason = "terminated externally (no exit reason recorded by supervisor)",
746                    "connection process crashed"
747                );
748                reaped += 1;
749            }
750        }
751        reaped
752    }
753
754    fn contains(&self, pid: u64) -> bool {
755        self.records
756            .lock()
757            .is_ok_and(|records| records.contains_key(&pid))
758    }
759
760    fn active_connections(&self) -> Vec<ActiveConnection> {
761        self.records.lock().map_or_else(
762            |_| Vec::new(),
763            |records| {
764                records
765                    .iter()
766                    .map(|(&pid, record)| ActiveConnection {
767                        pid,
768                        peer_addr: record.peer_addr,
769                    })
770                    .collect()
771            },
772        )
773    }
774
775    fn push_control(&self, pid: u64, control: ConnectionControl) -> Result<(), ServerError> {
776        lock(&self.controls, "connection control queue")?
777            .push(QueuedConnectionControl { pid, control });
778        Ok(())
779    }
780
781    pub(super) fn pop_control(&self, pid: u64) -> Option<ConnectionControl> {
782        let mut controls = self.controls.lock().ok()?;
783        let index = controls.iter().position(|queued| queued.pid == pid)?;
784        Some(controls.remove(index).control)
785    }
786
787    fn remove_control(&self, pid: u64, control: &ConnectionControl) {
788        let Ok(mut controls) = self.controls.lock() else {
789            return;
790        };
791        let Some(index) = controls
792            .iter()
793            .position(|queued| queued.pid == pid && &queued.control == control)
794        else {
795            return;
796        };
797        controls.remove(index);
798    }
799
800    fn active_count(&self) -> usize {
801        self.records.lock().map_or(0, |records| records.len())
802    }
803
804    /// Removes the connection record for `pid` and, in the same close step, drops
805    /// every push reply slot that connection still owns so each waiting
806    /// [`PushReplyAwaiter`] wakes immediately with a disconnected error. This runs
807    /// on every close route — `finish`, `mark_crashed`, and `reap_crashed` all
808    /// remove through here — and fires regardless of whether the connection ever
809    /// registered a worker, so a plain push target is covered too.
810    fn remove(&self, pid: u64) -> Option<ConnectionRecord> {
811        self.cancel_pushes_for_connection(pid);
812        let removed = self
813            .records
814            .lock()
815            .ok()
816            .and_then(|mut records| records.remove(&pid));
817        // Decrement only when a record was actually present so a double-remove
818        // (e.g. `finish` after `reap_crashed`) cannot drive the gauge negative.
819        if removed.is_some() {
820            crate::metrics::connection_closed();
821        }
822        removed
823    }
824}
825
826/// One in-flight server-push reply slot, associating the awaiter's reply `sender`
827/// with the `pid` of the connection that owns the push. The pid lets the close
828/// path drop exactly that connection's slots; the correlation id (the map key)
829/// still drives [`ConnectionRuntime::resolve_push`] and
830/// [`ConnectionRuntime::cancel_push`].
831#[derive(Debug)]
832struct PendingPush {
833    pid: u64,
834    sender: Sender<Vec<u8>>,
835}
836
837#[derive(Debug, Clone)]
838struct ConnectionRecord {
839    peer_addr: Option<SocketAddr>,
840    /// Worker registration declared on this connection, set by `set_registration`
841    /// when a `WorkerRegister` frame is accepted. `Some` marks a connection whose
842    /// close must fire `on_worker_unregistered`.
843    registration: Option<WorkerRegistration>,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq)]
847struct QueuedConnectionControl {
848    pid: u64,
849    control: ConnectionControl,
850}
851
852fn lock<'a, T>(mutex: &'a Mutex<T>, context: &str) -> Result<MutexGuard<'a, T>, ServerError> {
853    mutex.lock().map_err(|error| ServerError::ListenerAccept {
854        message: format!("{context} unavailable: {error}"),
855    })
856}