Skip to main content

liminal_server/server/connection/
supervisor.rs

1use std::collections::HashMap;
2use std::net::{SocketAddr, TcpStream};
3use std::os::fd::RawFd;
4#[cfg(test)]
5use std::sync::Barrier;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
8use std::sync::{Arc, Mutex, MutexGuard, Weak};
9use std::time::{Duration, Instant};
10
11use beamr::atom::{Atom, AtomTable};
12use beamr::module::ModuleRegistry;
13use beamr::native::native_process::NativeHandlerFactory;
14use beamr::process::ExitReason;
15use beamr::scheduler::{ReadinessToken, Scheduler, SchedulerConfig, SchedulerServices};
16use beamr::timer::TimerRef;
17
18use liminal::protocol::WorkerRegistration;
19
20use super::notifier::ConnectionNotifier;
21use super::process::ConnectionProcess;
22use super::services::{
23    ConnectionServices, LiminalConnectionServices, ProductionSubsystems, SubsystemFactory,
24    build_connection_services_via,
25};
26use crate::ServerError;
27use crate::config::types::{LimitsConfig, ServerConfig};
28
29const CONNECTION_SCHEDULER_THREADS: usize = 4;
30const CONNECTION_SHUTDOWN_CONTROL_ATOM: &str = "liminal_server_connection_shutdown_control";
31/// R6 (§1.2(4)): the single `READY` wake vocabulary for a connection. One atom;
32/// any marker (or N coalesced) triggers one full slice servicing all sources.
33const CONNECTION_READY_ATOM: &str = "liminal_server_connection_ready";
34
35#[cfg(test)]
36#[path = "supervisor_tests.rs"]
37mod tests;
38
39/// Supervisor that owns the beamr scheduler for per-connection processes.
40#[derive(Clone, Debug)]
41pub struct ConnectionSupervisor {
42    inner: Arc<SupervisorInner>,
43}
44
45impl ConnectionSupervisor {
46    /// Creates a connection supervisor backed by the services the config's
47    /// `[services]` profile selects: the full liminal channel/conversation stack
48    /// (the default) or the capability-scoped worker front door. Profile
49    /// enforcement is [`build_connection_services`](super::services::build_connection_services)'s,
50    /// so this constructor can never build full services for a worker-front-door
51    /// config.
52    ///
53    /// # Errors
54    /// Returns [`ServerError`] when service construction or scheduler startup fails.
55    pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
56        Self::from_config_via(config, &ProductionSubsystems)
57    }
58
59    /// [`Self::from_config`] with the §9 D2 subsystem factory injected.
60    ///
61    /// The factory is the only route to every scheduler-owning subsystem the
62    /// services construction builds, so a recording factory observes exactly what
63    /// was constructed; the connection scheduler itself (built below for BOTH
64    /// profiles) is the census baseline, not a census entry.
65    fn from_config_via(
66        config: &ServerConfig,
67        subsystems: &dyn SubsystemFactory,
68    ) -> Result<Self, ServerError> {
69        let services = build_connection_services_via(config, subsystems)?;
70        // The configured token (if any) is carried opaquely as bytes for a
71        // constant-time comparison against the handshake's `auth_token`. Absent
72        // `[auth]` leaves it `None`, so the connection stays open-access.
73        let auth_token = config
74            .auth
75            .as_ref()
76            .map(|auth| auth.token.clone().into_bytes());
77        SupervisorInner::new(services, None, auth_token, config.limits).map(|inner| Self {
78            inner: Arc::new(inner),
79        })
80    }
81
82    /// Creates a connection supervisor with no configured channels.
83    ///
84    /// # Errors
85    /// Returns [`ServerError`] when scheduler startup fails.
86    pub fn new() -> Result<Self, ServerError> {
87        Self::with_services(Arc::new(LiminalConnectionServices::empty()?))
88    }
89
90    /// Creates a connection supervisor using an explicit service adapter.
91    ///
92    /// # Errors
93    /// Returns [`ServerError`] when scheduler startup fails.
94    pub fn with_services(services: Arc<dyn ConnectionServices>) -> Result<Self, ServerError> {
95        SupervisorInner::new(services, None, None, LimitsConfig::default()).map(|inner| Self {
96            inner: Arc::new(inner),
97        })
98    }
99
100    /// Creates a connection supervisor with an explicit service adapter and the
101    /// configured connection auth token.
102    ///
103    /// This is the production constructor for callers that build services
104    /// themselves (the runtime needs the shared channel cluster before the
105    /// supervisor takes ownership) and therefore cannot use
106    /// [`Self::from_config`]: without it the configured `[auth]` token would be
107    /// silently dropped and the server would run open-access.
108    ///
109    /// # Errors
110    /// Returns [`ServerError`] when scheduler startup fails.
111    pub fn with_services_and_auth(
112        services: Arc<dyn ConnectionServices>,
113        auth_token: Option<Vec<u8>>,
114    ) -> Result<Self, ServerError> {
115        SupervisorInner::new(services, None, auth_token, LimitsConfig::default()).map(|inner| {
116            Self {
117                inner: Arc::new(inner),
118            }
119        })
120    }
121
122    /// Creates a connection supervisor with an explicit service adapter and a
123    /// connection-keyed worker-registration notifier.
124    ///
125    /// The `notifier` is invoked when a worker registers on a connection and when
126    /// such a connection closes. Supervisors built via [`Self::with_services`],
127    /// [`Self::from_config`], or [`Self::new`] carry no notifier, so liminal still
128    /// runs standalone; a `WorkerRegister` frame is then accepted without any
129    /// application callback.
130    ///
131    /// # Errors
132    /// Returns [`ServerError`] when scheduler startup fails.
133    pub fn with_services_and_notifier(
134        services: Arc<dyn ConnectionServices>,
135        notifier: Arc<dyn ConnectionNotifier>,
136    ) -> Result<Self, ServerError> {
137        SupervisorInner::new(services, Some(notifier), None, LimitsConfig::default()).map(|inner| {
138            Self {
139                inner: Arc::new(inner),
140            }
141        })
142    }
143
144    /// Spawns one supervised beamr process that owns `stream`.
145    ///
146    /// # Errors
147    /// Returns [`ServerError`] when stream configuration or beamr spawn fails.
148    pub fn spawn_connection(&self, stream: TcpStream) -> Result<ConnectionHandle, ServerError> {
149        self.inner.spawn_connection(stream)
150    }
151
152    /// Returns the underlying beamr scheduler.
153    #[must_use]
154    pub fn scheduler(&self) -> Arc<Scheduler> {
155        Arc::clone(&self.inner.scheduler)
156    }
157
158    /// Reaps connection processes that have exited outside the normal handler path.
159    #[must_use]
160    pub fn reap_crashed_connections(&self) -> usize {
161        self.inner.runtime.reap_crashed(&self.inner.scheduler)
162    }
163
164    /// Returns true when `pid` is still tracked by the supervisor.
165    #[must_use]
166    pub fn is_tracked(&self, pid: u64) -> bool {
167        self.inner.runtime.contains(pid)
168    }
169
170    /// Returns the number of tracked live connections.
171    #[must_use]
172    pub fn active_connection_count(&self) -> usize {
173        self.inner.runtime.active_count()
174    }
175
176    /// Returns the beamr process ids of the currently tracked live connections.
177    ///
178    /// Useful for addressing a specific connection — e.g. as the `pid` argument to
179    /// [`push_to_connection`](Self::push_to_connection) when the caller knows there
180    /// is a single connected client.
181    #[must_use]
182    pub fn active_connection_pids(&self) -> Vec<u64> {
183        self.inner
184            .runtime
185            .active_connections()
186            .into_iter()
187            .map(|connection| connection.pid)
188            .collect()
189    }
190
191    /// Broadcasts a best-effort shutdown notification to active connections.
192    ///
193    /// Connections with no active subscriptions ignore the notification. Failures
194    /// to enqueue the control message are logged and skipped; they are not retried.
195    pub fn notify_shutdown_subscribers(&self) {
196        self.inner
197            .broadcast_control(&ConnectionControl::NotifyShutdown);
198    }
199
200    /// Sends a force-close control message to every tracked connection process.
201    ///
202    /// Each live process attempts one shutdown notification before closing its
203    /// stream and exiting normally. Enqueue failures are logged and skipped.
204    pub fn force_close_active_connections(&self) {
205        for connection in self.inner.runtime.active_connections() {
206            tracing::warn!(
207                connection_pid = connection.pid,
208                peer_addr = ?connection.peer_addr,
209                "forcefully closing connection after drain timeout"
210            );
211            if !self
212                .inner
213                .enqueue_control(connection.pid, ConnectionControl::ForceClose)
214            {
215                tracing::warn!(
216                    connection_pid = connection.pid,
217                    peer_addr = ?connection.peer_addr,
218                    "failed to request forceful connection close; process is not live"
219                );
220            }
221        }
222    }
223
224    /// Pushes an opaque payload to a specific connected client over that client's
225    /// existing connection and returns an awaiter for the client's correlated reply.
226    ///
227    /// This is the server-initiated leg (server-to-client), the inverse of every
228    /// other request frame. It allocates a correlation id, registers a one-shot
229    /// reply slot keyed by that id, and enqueues a [`ConnectionControl::Push`] for
230    /// the connection process owning `pid`; that process writes a [`Frame::Push`]
231    /// out on its socket. When the client answers with a `PushReply` carrying the
232    /// same correlation id, the connection process resolves the awaiter's slot. The
233    /// returned [`PushReplyAwaiter`] blocks (bounded) for that reply.
234    ///
235    /// The reply's lifetime belongs to the push, not to any one
236    /// [`PushReplyAwaiter::receive`] call: this no-deadline push reserves a slot
237    /// that is reclaimed only by (a) the reply being consumed or (b) the
238    /// connection closing. An elapsed `receive` poll is a benign re-arm, never a
239    /// failure and never a cancellation. The §5
240    /// `max_pending_pushes_per_connection` cap bounds abandonment; use
241    /// [`push_to_connection_with_deadline`](Self::push_to_connection_with_deadline)
242    /// when the reply must have an explicit expiry.
243    ///
244    /// # Errors
245    /// Returns [`ServerError`] when the correlation id cannot be allocated, the
246    /// reply slot cannot be registered, or the control message cannot be enqueued
247    /// for the (possibly already-gone or concurrently-closing) connection
248    /// process. PUBLICATION INVARIANT: an `Err` guarantees no `Push` control was
249    /// published — the client never sees a `Push` frame for a failed call.
250    /// Conversely `Ok` promises ADMISSION, not delivery: the awaiter's outcome
251    /// is the delivery truth (a push admitted just as its connection closes
252    /// resolves to the truthful disconnected outcome, never to a lost reply).
253    pub fn push_to_connection(
254        &self,
255        pid: u64,
256        payload: Vec<u8>,
257    ) -> Result<PushReplyAwaiter, ServerError> {
258        self.push_with_deadline(pid, payload, None)
259    }
260
261    /// Like [`push_to_connection`](Self::push_to_connection) but attaches an
262    /// explicit reply deadline to the reserved slot: `deadline` is a DURATION
263    /// FROM NOW bounding the reply's lifetime — a property of THIS push rather
264    /// than of any [`PushReplyAwaiter::receive`] wait quantum.
265    ///
266    /// Deadline expiry is evaluated HOST-SIDE and LAZILY — at the next `receive`
267    /// touch, and at connection close at the latest. It never wakes the connection
268    /// process, adds no timer thread, and runs no periodic sweeper: a push that is
269    /// abandoned and never polled resolves at the next host-side touch (connection
270    /// close). At expiry the slot resolves to [`ServerError::PushReplyExpired`],
271    /// is removed, and its §5 `max_pending_pushes_per_connection` cap admission is
272    /// released. An elapsed `receive` poll BEFORE the deadline is still a benign
273    /// re-arm. A `receive` call in flight when the deadline falls due returns
274    /// the terminal expiry PROMPTLY — it waits the earlier of its quantum and
275    /// the deadline, so a large quantum can never extend the reply's lifetime
276    /// and the terminal outcome is quantum-independent.
277    ///
278    /// The deadline is evaluated at OBSERVATION POINTS, not enforced against the
279    /// wall clock: a reply that arrives before expiry is observed is delivered
280    /// normally, even if it arrives after the deadline instant. The deadline
281    /// bounds waiting and slot occupancy; it is not a delivery-freshness
282    /// guarantee. (This is deliberate — a reply is checked for at every
283    /// observation point before the deadline is, so an answer in hand always
284    /// beats an expiry.)
285    ///
286    /// # Errors
287    /// Returns [`ServerError`] when `deadline` is not representable on the
288    /// monotonic clock (an extreme duration is refused, never a panic), the
289    /// correlation id cannot be allocated, the reply slot cannot be registered,
290    /// or the control message cannot be enqueued for the (possibly already-gone
291    /// or concurrently-closing) connection process. PUBLICATION INVARIANT: an
292    /// `Err` guarantees no `Push` control was published — the client never sees
293    /// a `Push` frame for a failed call. Conversely `Ok` promises ADMISSION,
294    /// not delivery: the awaiter's outcome is the delivery truth.
295    pub fn push_to_connection_with_deadline(
296        &self,
297        pid: u64,
298        payload: Vec<u8>,
299        deadline: Duration,
300    ) -> Result<PushReplyAwaiter, ServerError> {
301        self.push_with_deadline(pid, payload, Some(deadline))
302    }
303
304    /// Shared body for the no-deadline and explicit-deadline push paths. With
305    /// `deadline == None` this is byte-for-byte the historical
306    /// `push_to_connection` behaviour (no per-slot deadline); with `Some`, the
307    /// slot carries an absolute expiry evaluated lazily at `receive`.
308    fn push_with_deadline(
309        &self,
310        pid: u64,
311        payload: Vec<u8>,
312        deadline: Option<Duration>,
313    ) -> Result<PushReplyAwaiter, ServerError> {
314        // S5: an extreme `Duration` must surface as this fallible API's typed
315        // error, not an `Instant` addition panic. Checked BEFORE any slot is
316        // registered so a refused deadline leaves nothing to roll back.
317        let deadline_at = match deadline {
318            None => None,
319            Some(window) => {
320                Some(
321                    Instant::now()
322                        .checked_add(window)
323                        .ok_or_else(|| ServerError::ListenerAccept {
324                            message: format!(
325                                "cannot push to connection process {pid}: reply deadline of {window:?} overflows the monotonic clock"
326                            ),
327                        })?,
328                )
329            }
330        };
331        let correlation_id = self.inner.runtime.next_push_correlation_id();
332        let receiver = self
333            .inner
334            .runtime
335            .register_push(pid, correlation_id, deadline_at)?;
336        // S3+S7 close-vs-register wall, ordered INSERT -> CONFIRM -> PUBLISH.
337        // The confirmation runs BEFORE the control is enqueued, which yields the
338        // PUBLICATION INVARIANT: an `Err` from this method guarantees no `Push`
339        // control was published — the client never sees a Push for a failed
340        // call. (Confirming after the enqueue was S7's non-linearizable race: a
341        // close could sweep, the published Push could already be answered and
342        // resolved, and the failed confirmation then returned `Err` for a push
343        // the client had received.) A close landing AFTER a successful confirm
344        // linearizes after push admission: the enqueue either fails (process
345        // gone — rollback below, `Err` truthful, nothing delivered) or succeeds
346        // with the slot already swept, and the awaiter then reads the truthful
347        // DISCONNECTED while a late client reply is the pinned harmless no-op.
348        // The exactly-one-side-observes argument lives at
349        // `confirm_push_registration`.
350        if !self
351            .inner
352            .runtime
353            .confirm_push_registration(pid, correlation_id)
354        {
355            return Err(ServerError::ListenerAccept {
356                message: format!(
357                    "cannot push to connection process {pid}: the connection closed during push registration"
358                ),
359            });
360        }
361        let control = ConnectionControl::Push {
362            correlation_id,
363            payload,
364        };
365        if self.inner.enqueue_control(pid, control) {
366            Ok(PushReplyAwaiter {
367                correlation_id,
368                receiver,
369                deadline: deadline_at,
370                runtime: Arc::downgrade(&self.inner.runtime),
371            })
372        } else {
373            // The process is gone AND the control provably never reached a
374            // consumer: `enqueue_control` returns false only when its failed-wake
375            // rollback REMOVED the queued control (S8 — an entry a drain already
376            // consumed counts as published and returns true, with the slot
377            // lifecycle carrying the delivery truth). Dropping the now-unreachable
378            // reply slot here therefore keeps the publication invariant exact on
379            // every `Err` path.
380            self.inner.runtime.cancel_push(correlation_id);
381            Err(ServerError::ListenerAccept {
382                message: format!("cannot push to connection process {pid}: process is not live"),
383            })
384        }
385    }
386
387    /// Flushes durable channel state through the configured liminal services.
388    ///
389    /// # Errors
390    /// Returns [`ServerError::ShutdownFlush`] when the underlying service flush fails.
391    pub fn flush_durable_state(&self) -> Result<(), ServerError> {
392        self.inner.runtime.services().flush_durable_state()
393    }
394
395    /// Stops the beamr scheduler used by connection processes.
396    pub fn shutdown(&self) {
397        // Remove every host record while the readiness owner is still live. The
398        // removal path ACKs deregistration and only then releases each fd guard;
399        // scheduler shutdown subsequently drops the process-owned handles.
400        for connection in self.inner.runtime.active_connections() {
401            self.inner.runtime.finish(connection.pid);
402        }
403        self.inner.scheduler.shutdown();
404    }
405
406    /// R7 test instrument: slices serviced by connection `pid` since spawn.
407    #[cfg(test)]
408    pub(super) fn slice_count(&self, pid: u64) -> u64 {
409        self.inner.runtime.slice_count(pid)
410    }
411
412    /// Reserved push reply slots outstanding (test observability for the public
413    /// push paths — lets e2e tests assert slot reclamation and cap accounting).
414    #[cfg(test)]
415    pub(super) fn pending_push_count(&self) -> usize {
416        self.inner.runtime.pending_push_count()
417    }
418
419    /// R6 test seam: a [`ReadyWaker`](super::wake::ReadyWaker) for `pid` — the same
420    /// handle a subscription-inbox or reply-availability notifier fires.
421    #[cfg(test)]
422    pub(super) fn ready_waker(&self, pid: u64) -> Option<super::wake::ReadyWaker> {
423        self.inner.runtime.ready_waker(pid)
424    }
425
426    /// Registered readiness tokens held in host records (test observability).
427    #[cfg(test)]
428    pub(super) fn readiness_registration_count(&self) -> usize {
429        self.inner.runtime.readiness_registration_count()
430    }
431
432    /// Kernel fd registered for `pid` (test observability for fd-reuse races).
433    #[cfg(test)]
434    pub(super) fn readiness_fd(&self, pid: u64) -> Option<RawFd> {
435        self.inner.runtime.readiness_fd(pid)
436    }
437
438    /// Installs a one-use arm-to-probe barrier and returns its test endpoints.
439    #[cfg(test)]
440    pub(super) fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
441        self.inner.runtime.install_pre_wait_barrier()
442    }
443
444    /// Barrier-staged final probes that observed newly arrived work.
445    #[cfg(test)]
446    pub(super) fn pre_wait_probe_hits(&self) -> u64 {
447        self.inner.runtime.pre_wait_probe_hits()
448    }
449}
450
451/// Handle for one supervised connection process.
452#[derive(Clone, Debug)]
453pub struct ConnectionHandle {
454    pid: u64,
455    peer_addr: Option<SocketAddr>,
456    supervisor: Arc<SupervisorInner>,
457}
458
459impl ConnectionHandle {
460    /// Returns the beamr process id for this connection.
461    #[must_use]
462    pub const fn pid(&self) -> u64 {
463        self.pid
464    }
465
466    /// Returns the peer address if it was available from the accepted stream.
467    #[must_use]
468    pub const fn peer_addr(&self) -> Option<SocketAddr> {
469        self.peer_addr
470    }
471
472    /// Returns whether the beamr process is still live.
473    #[must_use]
474    pub fn is_live(&self) -> bool {
475        self.supervisor
476            .scheduler
477            .process_table()
478            .get(self.pid)
479            .is_some()
480    }
481
482    /// Requests an error exit for tests and supervisor control paths.
483    ///
484    /// # Errors
485    /// Returns [`ServerError`] when the process is no longer live.
486    pub fn request_crash(&self) -> Result<(), ServerError> {
487        if self
488            .supervisor
489            .scheduler
490            .enqueue_atom_message(self.pid, Atom::ERROR)
491        {
492            Ok(())
493        } else {
494            Err(ServerError::ListenerAccept {
495                message: format!("connection process {} is not live", self.pid),
496            })
497        }
498    }
499}
500
501/// Awaits the correlated reply to a single server-initiated push.
502///
503/// Returned by [`ConnectionSupervisor::push_to_connection`]. The reply slot is
504/// resolved when the originating connection process receives a `PushReply` frame
505/// carrying the same correlation id, so [`PushReplyAwaiter::receive`] blocks
506/// (bounded) for that one correlated answer.
507#[derive(Debug)]
508pub struct PushReplyAwaiter {
509    correlation_id: u64,
510    receiver: Receiver<Vec<u8>>,
511    /// This push's absolute reply deadline, mirrored from its slot. `None` (the
512    /// default push) selects the no-deadline receive path, which NEVER touches
513    /// the runtime — byte-compatible with 0.2.3, no shared-lock exposure.
514    /// `Some` lets `receive` wait `min(caller quantum, time until deadline)` and
515    /// resolve expiry promptly, so the caller's quantum can never select a
516    /// deadlined push's terminal outcome.
517    deadline: Option<Instant>,
518    /// Weak handle to the owning runtime, used ONLY by the explicit-deadline
519    /// path to resolve expiry host-side at [`receive`](Self::receive). A
520    /// no-deadline push never upgrades it. `Weak` so the awaiter never keeps the
521    /// runtime alive; if it is already gone, the slot (and its sender) is gone
522    /// with it — the connection side is torn down.
523    runtime: Weak<ConnectionRuntime>,
524}
525
526impl PushReplyAwaiter {
527    /// Returns the correlation id this awaiter is matched on.
528    #[must_use]
529    pub const fn correlation_id(&self) -> u64 {
530        self.correlation_id
531    }
532
533    /// Blocks up to `timeout` for the client's correlated reply payload.
534    ///
535    /// `timeout` is a WAIT QUANTUM ONLY — a MAXIMUM wait, not a promise to
536    /// block: an elapsed poll is a benign re-arm, never a failure; the reply's
537    /// lifetime belongs to the push. A caller may re-invoke `receive`
538    /// indefinitely after a [`ServerError::PushReplyTimeout`]: the reserved slot
539    /// is untouched and a later reply is still delivered byte-exact. The poll
540    /// quantum never changes the protocol outcome — for a deadlined push the
541    /// call waits no longer than the EARLIER of the caller's quantum and the
542    /// push's deadline, so the terminal expiry is returned promptly once due,
543    /// never held until the quantum ends and never deferred past it.
544    ///
545    /// A push with no explicit deadline never touches shared supervisor state
546    /// here: the elapsed quantum returns straight from the channel wait
547    /// (behaviour-compatible with 0.2.3 — no registry lock, no contention, no
548    /// poison exposure on the unchanged API).
549    ///
550    /// # Errors
551    /// Returns [`ServerError::PushReplyTimeout`] when no reply arrived within this
552    /// `timeout` quantum and the push's deadline (if any) is not yet due (a
553    /// benign re-arm — call again to keep waiting);
554    /// [`ServerError::PushReplyExpired`] when the push carried an explicit reply
555    /// deadline (via
556    /// [`push_to_connection_with_deadline`](ConnectionSupervisor::push_to_connection_with_deadline))
557    /// and that deadline is due (terminal: the slot is removed and its §5 cap
558    /// admission released; returned as soon as the deadline passes, even
559    /// mid-quantum — but evaluated at observation points, not against the wall
560    /// clock: a reply already delivered when this call observes the slot wins
561    /// over expiry, even if it arrived after the deadline instant); or
562    /// [`ServerError::PushReplyDisconnected`] when the connection process
563    /// dropped the reply slot (the connection closed — the prompt worker-death
564    /// signal). The variants are distinct so callers classify by type, not
565    /// message.
566    pub fn receive(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
567        self.deadline.map_or_else(
568            || self.receive_no_deadline(timeout),
569            |deadline| self.receive_deadlined(timeout, deadline),
570        )
571    }
572
573    /// The default-push receive: exactly the 0.2.3 shape. One bounded channel
574    /// wait; an elapsed quantum is a benign timeout straight from the channel —
575    /// no runtime upgrade, no registry lock, EVER (unrelated registry work can
576    /// never stretch this call past its quantum, and registry poison cannot
577    /// reach it).
578    fn receive_no_deadline(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
579        match self.receiver.recv_timeout(timeout) {
580            Ok(payload) => Ok(payload),
581            Err(RecvTimeoutError::Timeout) => Err(ServerError::PushReplyTimeout {
582                correlation_id: self.correlation_id,
583            }),
584            Err(RecvTimeoutError::Disconnected) => Err(ServerError::PushReplyDisconnected {
585                correlation_id: self.correlation_id,
586            }),
587        }
588    }
589
590    /// The deadlined receive: waits `min(caller quantum, time until deadline)`
591    /// and re-evaluates reply-first-then-expiry on every wake, so the caller's
592    /// quantum can never select the terminal outcome (S1). Order per iteration:
593    ///
594    /// 1. Deliver a reply already in hand — an answer that is here must never be
595    ///    reported as a timeout OR an expiry (the observation-point rule).
596    /// 2. If the deadline is due, resolve expiry atomically against the registry
597    ///    (`expire_slot`) and return the terminal outcome promptly — even when
598    ///    the caller's quantum has time left (the quantum is a max wait).
599    /// 3. Otherwise wait for the earlier of quantum-remaining and deadline; a
600    ///    wake re-runs 1-2, and an exhausted quantum before the deadline is the
601    ///    benign `PushReplyTimeout` re-arm with the slot untouched.
602    fn receive_deadlined(
603        &self,
604        timeout: Duration,
605        deadline: Instant,
606    ) -> Result<Vec<u8>, ServerError> {
607        let started = Instant::now();
608        loop {
609            if let Some(result) = self.try_take_reply() {
610                return result;
611            }
612            let now = Instant::now();
613            if now >= deadline {
614                return self.expire_slot();
615            }
616            let quantum_left = timeout.saturating_sub(now.duration_since(started));
617            if quantum_left.is_zero() {
618                return Err(ServerError::PushReplyTimeout {
619                    correlation_id: self.correlation_id,
620                });
621            }
622            match self
623                .receiver
624                .recv_timeout(quantum_left.min(deadline.duration_since(now)))
625            {
626                Ok(payload) => return Ok(payload),
627                Err(RecvTimeoutError::Disconnected) => {
628                    return Err(ServerError::PushReplyDisconnected {
629                        correlation_id: self.correlation_id,
630                    });
631                }
632                // Re-loop: deliver a reply that raced the wake, expire a
633                // now-due deadline, or report the exhausted quantum benignly.
634                Err(RecvTimeoutError::Timeout) => {}
635            }
636        }
637    }
638
639    /// Resolves a due deadline against the registry's atomic removal transition.
640    fn expire_slot(&self) -> Result<Vec<u8>, ServerError> {
641        let timeout_error = || ServerError::PushReplyTimeout {
642            correlation_id: self.correlation_id,
643        };
644        let Some(runtime) = self.runtime.upgrade() else {
645            // The runtime is gone, and the slot map (with every sender) with it:
646            // the connection side is torn down. Re-check the channel so the
647            // dropped sender reads as the established DISCONNECTED outcome — a
648            // dead runtime must not be misreported as a benign healthy-but-slow
649            // timeout (S4).
650            return self
651                .try_take_reply()
652                .unwrap_or(Err(ServerError::PushReplyDisconnected {
653                    correlation_id: self.correlation_id,
654                }));
655        };
656        match runtime.expire_push_if_due(self.correlation_id) {
657            PushSlotDisposition::Expired => Err(ServerError::PushReplyExpired {
658                correlation_id: self.correlation_id,
659            }),
660            // Unreachable by construction (this is only called with the deadline
661            // due, and the registry re-reads a monotonic clock); honest benign
662            // fallback rather than a panic.
663            PushSlotDisposition::Live => Err(timeout_error()),
664            // Another path (a concurrent `resolve_push`, or connection close)
665            // removed the slot under the registry lock while we waited on it. Its
666            // send, if any, happens under that same lock, so re-check the channel:
667            // a delivered reply is present now; a dropped sender is disconnected.
668            PushSlotDisposition::Absent => self
669                .try_take_reply()
670                .unwrap_or_else(|| Err(timeout_error())),
671        }
672    }
673
674    /// Non-blocking check for a reply already sitting in the channel. `Some` with
675    /// the payload or a disconnected error; `None` when the channel is still empty
676    /// (no reply yet — the caller re-arms).
677    fn try_take_reply(&self) -> Option<Result<Vec<u8>, ServerError>> {
678        match self.receiver.try_recv() {
679            Ok(payload) => Some(Ok(payload)),
680            Err(TryRecvError::Disconnected) => Some(Err(ServerError::PushReplyDisconnected {
681                correlation_id: self.correlation_id,
682            })),
683            Err(TryRecvError::Empty) => None,
684        }
685    }
686}
687
688pub(super) struct SupervisorInner {
689    scheduler: Arc<Scheduler>,
690    runtime: Arc<ConnectionRuntime>,
691}
692
693impl std::fmt::Debug for SupervisorInner {
694    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695        formatter
696            .debug_struct("SupervisorInner")
697            .field("runtime", &self.runtime)
698            .finish_non_exhaustive()
699    }
700}
701
702impl SupervisorInner {
703    fn new(
704        services: Arc<dyn ConnectionServices>,
705        notifier: Option<Arc<dyn ConnectionNotifier>>,
706        auth_token: Option<Vec<u8>>,
707        limits: LimitsConfig,
708    ) -> Result<Self, ServerError> {
709        let atoms = AtomTable::with_common_atoms();
710        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
711        let registry = Arc::new(ModuleRegistry::new());
712
713        let scheduler = Scheduler::with_services(
714            SchedulerConfig {
715                thread_count: Some(CONNECTION_SCHEDULER_THREADS),
716                ..SchedulerConfig::default()
717            },
718            SchedulerServices::from_config().owned_readiness(),
719            registry,
720        )
721        .map_err(|message| ServerError::ListenerAccept {
722            message: format!("failed to start connection scheduler: {message}"),
723        })?;
724        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
725        let scheduler = Arc::new(scheduler);
726        // The runtime captures a WEAK handle to the connection scheduler so
727        // notifier wakes (R3/R1(vi)) can be fired from another actor's slice
728        // without a strong scheduler↔process↔runtime cycle that would leak the
729        // whole connection scheduler.
730        Ok(Self {
731            runtime: Arc::new(ConnectionRuntime::new(
732                services,
733                control_atom,
734                ready_atom,
735                Arc::downgrade(&scheduler),
736                notifier,
737                auth_token,
738                limits,
739            )),
740            scheduler,
741        })
742    }
743
744    fn spawn_connection(
745        self: &Arc<Self>,
746        stream: TcpStream,
747    ) -> Result<ConnectionHandle, ServerError> {
748        // §5 `max_connections`: ATOMIC admission reservation acquired BEFORE any
749        // process construction (review round 1 item 7 — a signed bound must not
750        // be exceedable by concurrent callers; check-then-spawn across an
751        // unlocked window was). The CAS reservation is released on every failure
752        // path below and converts into the connection record at `register`;
753        // thereafter the single record-removal path (`remove`) releases it. An
754        // over-cap accept therefore costs nothing and the bound holds under any
755        // concurrency.
756        self.runtime.try_reserve_admission()?;
757        let reservation = AdmissionReservation {
758            runtime: &self.runtime,
759            armed: true,
760        };
761        stream
762            .set_nonblocking(true)
763            .map_err(|error| ServerError::ListenerAccept {
764                message: format!("failed to configure connection stream: {error}"),
765            })?;
766        let peer_addr = stream.peer_addr().ok();
767        // The host-held duplicate keeps the fd alive until the single record-removal
768        // path has synchronously deregistered readiness. External process death can
769        // therefore never let fd reuse overtake host-side deregistration.
770        let fd_guard = stream
771            .try_clone()
772            .map_err(|error| ServerError::ListenerAccept {
773                message: format!("failed to retain connection fd for teardown: {error}"),
774            })?;
775        let holder = Arc::new(Mutex::new(Some(stream)));
776        let runtime = Arc::clone(&self.runtime);
777        let process_holder = Arc::clone(&holder);
778        let factory: NativeHandlerFactory = Box::new(move || {
779            Box::new(ConnectionProcess::from_holder(
780                Arc::clone(&runtime),
781                peer_addr,
782                &process_holder,
783            ))
784        });
785        let pid =
786            self.scheduler
787                .spawn_native(factory)
788                .map_err(|error| ServerError::ListenerAccept {
789                    message: format!("failed to spawn connection process: {error}"),
790                })?;
791        if let Err(error) = self.runtime.register_with_fd(pid, peer_addr, fd_guard) {
792            // Registration failure leaves no host record to reap. Terminate the
793            // just-spawned process explicitly so neither its stream nor admission
794            // reservation can escape this failed spawn.
795            self.scheduler.terminate_process(pid, ExitReason::Error);
796            return Err(error);
797        }
798        // The reservation is now owned by the registered record: `remove` (the
799        // single record-removal path — finish/mark_crashed/reap all funnel
800        // through it) releases the admission when the record goes away.
801        reservation.convert();
802        Ok(ConnectionHandle {
803            pid,
804            peer_addr,
805            supervisor: Arc::clone(self),
806        })
807    }
808
809    fn broadcast_control(&self, control: &ConnectionControl) {
810        for connection in self.runtime.active_connections() {
811            if !self.enqueue_control(connection.pid, control.clone()) {
812                tracing::debug!(
813                    connection_pid = connection.pid,
814                    peer_addr = ?connection.peer_addr,
815                    ?control,
816                    "connection control message skipped because process is not live"
817                );
818            }
819        }
820    }
821
822    /// Queues `control` for `pid` and wakes the process. Returns whether the
823    /// control was PUBLISHED (left in the queue with a successful wake, or
824    /// already consumed by a drain) — `false` guarantees no consumer ever saw
825    /// it.
826    ///
827    /// S8: a failed wake does NOT prove the queued control was never consumed.
828    /// The insert releases the queue lock before the wake attempt, and a
829    /// process already executing a control drain (each control atom drains ALL
830    /// queued controls for the pid) can pop the just-inserted entry in that
831    /// window, then exit before the wake check. Publication is therefore
832    /// disambiguated BY OBSERVATION on the failed-wake path: `remove_control`
833    /// finding and removing the entry proves no consumer saw it (truly
834    /// unpublished — `false`); finding nothing proves a drain consumed it
835    /// (`pop_control` is the only other remover of queue entries, and the
836    /// removal key embeds the push's runtime-unique correlation id, so it can
837    /// never match a different entry) — the control was published and the
838    /// caller's slot lifecycle carries the delivery truth (`true`).
839    fn enqueue_control(&self, pid: u64, control: ConnectionControl) -> bool {
840        // Keep a key for the failure-path removal before the control is moved into
841        // the queue, so a non-`Copy` (push) control can still be located and pulled
842        // back out if the scheduler wakeup fails.
843        let removal_key = control.clone();
844        if self.runtime.push_control(pid, control).is_err() {
845            return false;
846        }
847        // Deterministic test seam in the insert->wake window (S8 staging).
848        #[cfg(test)]
849        self.runtime.run_pre_wake_barrier();
850        if self
851            .scheduler
852            .enqueue_atom_message(pid, self.runtime.control_atom())
853        {
854            true
855        } else {
856            // Failed wake: the entry's fate is the publication verdict. Removed
857            // here => nobody consumed it => unpublished. Already gone => a
858            // drain consumed it before the wake check => published. (A poisoned
859            // queue lock reads as not-removed => published — the safe
860            // direction: the slot lifecycle then reports the truthful outcome,
861            // whereas claiming "unpublished" could be a lie.)
862            !self.runtime.remove_control(pid, &removal_key)
863        }
864    }
865}
866
867/// RAII guard for one §5 `max_connections` admission reservation.
868///
869/// Acquired (via [`ConnectionRuntime::try_reserve_admission`]) before any process
870/// construction in `spawn_connection`; every early-return failure path releases
871/// it through `Drop`, and a successful `register` converts it into the
872/// connection record (whose removal releases the admission instead). RAII means
873/// no failure path — present or future — can leak a reservation.
874struct AdmissionReservation<'a> {
875    runtime: &'a ConnectionRuntime,
876    armed: bool,
877}
878
879impl AdmissionReservation<'_> {
880    /// Converts the reservation into record ownership: `Drop` no longer releases
881    /// it, because the registered record's removal will.
882    fn convert(mut self) {
883        self.armed = false;
884    }
885}
886
887impl Drop for AdmissionReservation<'_> {
888    fn drop(&mut self) {
889        if self.armed {
890            self.runtime.release_admission();
891        }
892    }
893}
894
895#[derive(Debug, Clone, PartialEq, Eq)]
896pub(super) enum ConnectionControl {
897    NotifyShutdown,
898    ForceClose,
899    /// Server-initiated push of an opaque payload, correlated by `correlation_id`,
900    /// to be written out as a [`Frame::Push`] by the receiving connection process.
901    Push {
902        correlation_id: u64,
903        payload: Vec<u8>,
904    },
905}
906
907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
908pub struct ActiveConnection {
909    pid: u64,
910    peer_addr: Option<SocketAddr>,
911}
912
913#[cfg(test)]
914#[derive(Debug, Clone)]
915struct PreWaitBarrier {
916    armed: Arc<Barrier>,
917    release: Arc<Barrier>,
918}
919
920#[derive(Debug)]
921pub(super) struct ConnectionRuntime {
922    services: Arc<dyn ConnectionServices>,
923    records: Mutex<HashMap<u64, ConnectionRecord>>,
924    controls: Mutex<Vec<QueuedConnectionControl>>,
925    control_atom: Atom,
926    /// R6 single `READY` wake atom for this connection scheduler. Fired by every
927    /// wake source's notifier (R3/R1(vi)); coalescing and duplicates are harmless.
928    ready_atom: Atom,
929    /// Weak handle to the connection scheduler, used to build [`ReadyWaker`]s a
930    /// notifier fires from another actor's slice. Weak so it never keeps the
931    /// scheduler alive (the scheduler owns the processes that own this runtime).
932    scheduler: Weak<Scheduler>,
933    /// R7 (§1.2(6)) test-only per-connection slice counter, keyed by pid. Bumped
934    /// once at the head of every serviced slice. The park-flip's permanent rule-1
935    /// assertion (a parked connection's counter must not advance without an event)
936    /// reads this; the instrument lands now with a test proving it counts slices.
937    #[cfg(test)]
938    slice_counts: Mutex<HashMap<u64, u64>>,
939    /// Deterministic test gate placed after arm and before the final probe.
940    #[cfg(test)]
941    pre_wait_barrier: Mutex<Option<PreWaitBarrier>>,
942    /// Deterministic test gate in `enqueue_control`'s insert->wake window (S8).
943    #[cfg(test)]
944    pre_wake_barrier: Mutex<Option<PreWaitBarrier>>,
945    /// Barrier-staged slices where the final probe found newly arrived work.
946    #[cfg(test)]
947    pre_wait_probe_hits: AtomicU64,
948    /// One-shot reply slots for in-flight server pushes, keyed by correlation id.
949    /// The supervisor registers a slot in `push_to_connection`; the connection
950    /// process resolves it when the matching `PushReply` frame arrives. Each slot
951    /// records the owning connection pid so the close path can drop a connection's
952    /// outstanding slots and wake their awaiters with a prompt disconnected error.
953    push_replies: Mutex<HashMap<u64, PendingPush>>,
954    /// Monotonic source of push correlation ids. Server-allocated, so it never
955    /// collides with a client-chosen id on this connection.
956    next_push_id: AtomicU64,
957    /// §5 `max_connections` admission counter. Incremented atomically (CAS
958    /// against the limit) BEFORE a connection process is constructed and
959    /// decremented on every spawn-failure path and on final record removal, so
960    /// the signed bound holds under concurrent spawns — admission is never
961    /// derived from the records-map length across an unlocked window.
962    admissions: AtomicU64,
963    /// Optional application hook invoked on worker registration and on the close
964    /// of a connection that had registered. `None` keeps liminal standalone: a
965    /// `WorkerRegister` is accepted with no callback.
966    notifier: Option<Arc<dyn ConnectionNotifier>>,
967    /// Configured connection auth token (the `[auth]` section's token as opaque
968    /// bytes). `Some` gates the `Connect` handshake — the frame's `auth_token` must
969    /// match under a constant-time comparison; `None` leaves the server open-access,
970    /// byte-identical to the pre-auth behaviour.
971    auth_token: Option<Vec<u8>>,
972    /// Operational caps (§5). Enforced with typed refusals at admission:
973    /// per-connection subscription, conversation, push, and pending-reply counts,
974    /// plus the shared inbox byte budget. Non-config constructors carry the signed
975    /// defaults ([`LimitsConfig::default`]).
976    limits: LimitsConfig,
977}
978
979impl ConnectionRuntime {
980    fn new(
981        services: Arc<dyn ConnectionServices>,
982        control_atom: Atom,
983        ready_atom: Atom,
984        scheduler: Weak<Scheduler>,
985        notifier: Option<Arc<dyn ConnectionNotifier>>,
986        auth_token: Option<Vec<u8>>,
987        limits: LimitsConfig,
988    ) -> Self {
989        Self {
990            services,
991            records: Mutex::new(HashMap::new()),
992            controls: Mutex::new(Vec::new()),
993            control_atom,
994            ready_atom,
995            scheduler,
996            #[cfg(test)]
997            slice_counts: Mutex::new(HashMap::new()),
998            #[cfg(test)]
999            pre_wait_barrier: Mutex::new(None),
1000            #[cfg(test)]
1001            pre_wake_barrier: Mutex::new(None),
1002            #[cfg(test)]
1003            pre_wait_probe_hits: AtomicU64::new(0),
1004            push_replies: Mutex::new(HashMap::new()),
1005            next_push_id: AtomicU64::new(1),
1006            admissions: AtomicU64::new(0),
1007            notifier,
1008            auth_token,
1009            limits,
1010        }
1011    }
1012
1013    /// Atomically reserves one §5 `max_connections` admission slot: a CAS loop
1014    /// against the configured limit, so N concurrent callers racing for the last
1015    /// slot admit EXACTLY one — the bound cannot be transiently exceeded.
1016    ///
1017    /// # Errors
1018    /// Returns [`ServerError::ConnectionLimitReached`] when every slot is taken.
1019    fn try_reserve_admission(&self) -> Result<(), ServerError> {
1020        let limit = self.limits.max_connections as u64;
1021        let mut current = self.admissions.load(Ordering::Acquire);
1022        loop {
1023            if current >= limit {
1024                return Err(ServerError::ConnectionLimitReached {
1025                    limit: self.limits.max_connections,
1026                });
1027            }
1028            match self.admissions.compare_exchange_weak(
1029                current,
1030                current + 1,
1031                Ordering::AcqRel,
1032                Ordering::Acquire,
1033            ) {
1034                Ok(_) => return Ok(()),
1035                Err(observed) => current = observed,
1036            }
1037        }
1038    }
1039
1040    /// Releases one admission slot. Called by the spawn failure paths (via the
1041    /// [`AdmissionReservation`] guard) and by [`Self::remove`] when a registered
1042    /// record is removed — exactly one release per reservation. Saturating so a
1043    /// spurious release can never wrap the counter.
1044    fn release_admission(&self) {
1045        let mut current = self.admissions.load(Ordering::Acquire);
1046        loop {
1047            let next = current.saturating_sub(1);
1048            match self.admissions.compare_exchange_weak(
1049                current,
1050                next,
1051                Ordering::AcqRel,
1052                Ordering::Acquire,
1053            ) {
1054                Ok(_) => return,
1055                Err(observed) => current = observed,
1056            }
1057        }
1058    }
1059
1060    /// The operational caps (§5) this runtime enforces.
1061    pub(super) const fn limits(&self) -> &LimitsConfig {
1062        &self.limits
1063    }
1064
1065    /// The connection's single R6 `READY` wake atom.
1066    pub(super) const fn ready_atom(&self) -> Atom {
1067        self.ready_atom
1068    }
1069
1070    /// Builds a [`ReadyWaker`] targeting `pid` on the connection scheduler, if the
1071    /// scheduler is still live. `None` when the scheduler is gone (teardown) or in
1072    /// scheduler-free unit tests — a notifier with no waker simply never wakes,
1073    /// which under the busy loop is redundant anyway (the every-slice pump still
1074    /// services the source). This is the seam every wake source installs its
1075    /// notifier through (R3/R1(vi)).
1076    pub(super) fn ready_waker(&self, pid: u64) -> Option<super::wake::ReadyWaker> {
1077        let scheduler = self.scheduler.upgrade()?;
1078        Some(super::wake::ReadyWaker::new(
1079            &scheduler,
1080            pid,
1081            self.ready_atom,
1082        ))
1083    }
1084
1085    /// R7: records one serviced slice for `pid`. Bumped at the head of every
1086    /// slice; the park-flip's quiescence assertion reads [`Self::slice_count`].
1087    #[cfg(test)]
1088    pub(super) fn record_slice(&self, pid: u64) {
1089        if let Ok(mut counts) = self.slice_counts.lock() {
1090            *counts.entry(pid).or_insert(0) += 1;
1091        }
1092    }
1093
1094    /// R7: slices serviced by connection `pid` since spawn (test instrument).
1095    #[cfg(test)]
1096    pub(super) fn slice_count(&self, pid: u64) -> u64 {
1097        self.slice_counts
1098            .lock()
1099            .map_or(0, |counts| counts.get(&pid).copied().unwrap_or(0))
1100    }
1101
1102    #[cfg(test)]
1103    fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
1104        let armed = Arc::new(Barrier::new(2));
1105        let release = Arc::new(Barrier::new(2));
1106        if let Ok(mut slot) = self.pre_wait_barrier.lock() {
1107            *slot = Some(PreWaitBarrier {
1108                armed: Arc::clone(&armed),
1109                release: Arc::clone(&release),
1110            });
1111        }
1112        (armed, release)
1113    }
1114
1115    /// Runs a one-use deterministic test gate after arm. Returns whether the gate
1116    /// was installed so only that staged probe contributes to observability.
1117    #[cfg(test)]
1118    pub(super) fn run_pre_wait_barrier(&self) -> bool {
1119        let barrier = self
1120            .pre_wait_barrier
1121            .lock()
1122            .ok()
1123            .and_then(|mut slot| slot.take());
1124        let Some(barrier) = barrier else {
1125            return false;
1126        };
1127        barrier.armed.wait();
1128        barrier.release.wait();
1129        true
1130    }
1131
1132    /// Installs a one-use barrier in `enqueue_control`'s insert->wake window
1133    /// (S8 staging: lets a test act as the control-drain consumer between the
1134    /// queue insertion and the wake attempt) and returns its test endpoints.
1135    #[cfg(test)]
1136    pub(super) fn install_pre_wake_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
1137        let armed = Arc::new(Barrier::new(2));
1138        let release = Arc::new(Barrier::new(2));
1139        if let Ok(mut slot) = self.pre_wake_barrier.lock() {
1140            *slot = Some(PreWaitBarrier {
1141                armed: Arc::clone(&armed),
1142                release: Arc::clone(&release),
1143            });
1144        }
1145        (armed, release)
1146    }
1147
1148    /// Runs the one-use insert->wake test gate, if installed.
1149    #[cfg(test)]
1150    pub(super) fn run_pre_wake_barrier(&self) {
1151        let barrier = self
1152            .pre_wake_barrier
1153            .lock()
1154            .ok()
1155            .and_then(|mut slot| slot.take());
1156        if let Some(barrier) = barrier {
1157            barrier.armed.wait();
1158            barrier.release.wait();
1159        }
1160    }
1161
1162    #[cfg(test)]
1163    pub(super) fn record_pre_wait_probe_hit(&self) {
1164        self.pre_wait_probe_hits.fetch_add(1, Ordering::AcqRel);
1165    }
1166
1167    #[cfg(test)]
1168    fn pre_wait_probe_hits(&self) -> u64 {
1169        self.pre_wait_probe_hits.load(Ordering::Acquire)
1170    }
1171
1172    /// Builds a runtime wrapping `services` for unit tests that exercise
1173    /// `apply_frame` without a live scheduler. Uses a fresh interned control atom
1174    /// and no notifier.
1175    #[cfg(test)]
1176    pub(super) fn for_tests(services: Arc<dyn ConnectionServices>) -> Self {
1177        let atoms = AtomTable::with_common_atoms();
1178        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1179        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1180        Self::new(
1181            services,
1182            control_atom,
1183            ready_atom,
1184            Weak::new(),
1185            None,
1186            None,
1187            LimitsConfig::default(),
1188        )
1189    }
1190
1191    /// Builds a runtime wrapping `services` with explicit `limits` for unit tests
1192    /// that exercise the §5 admission caps without a live scheduler.
1193    #[cfg(test)]
1194    pub(super) fn for_tests_with_limits(
1195        services: Arc<dyn ConnectionServices>,
1196        limits: LimitsConfig,
1197    ) -> Self {
1198        let atoms = AtomTable::with_common_atoms();
1199        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1200        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1201        Self::new(
1202            services,
1203            control_atom,
1204            ready_atom,
1205            Weak::new(),
1206            None,
1207            None,
1208            limits,
1209        )
1210    }
1211
1212    /// Builds a runtime wrapping `services` with a configured auth `token` for unit
1213    /// tests that exercise the `Connect` handshake enforcement without a live
1214    /// scheduler. Uses a fresh interned control atom and no notifier.
1215    #[cfg(test)]
1216    pub(super) fn for_tests_with_auth_token(
1217        services: Arc<dyn ConnectionServices>,
1218        token: Vec<u8>,
1219    ) -> Self {
1220        let atoms = AtomTable::with_common_atoms();
1221        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1222        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1223        Self::new(
1224            services,
1225            control_atom,
1226            ready_atom,
1227            Weak::new(),
1228            None,
1229            Some(token),
1230            LimitsConfig::default(),
1231        )
1232    }
1233
1234    /// Builds a runtime wrapping `services` with a `notifier` for unit tests that
1235    /// exercise `apply_frame` and the close path without a live scheduler.
1236    #[cfg(test)]
1237    pub(super) fn for_tests_with_notifier(
1238        services: Arc<dyn ConnectionServices>,
1239        notifier: Arc<dyn ConnectionNotifier>,
1240    ) -> Self {
1241        let atoms = AtomTable::with_common_atoms();
1242        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1243        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1244        Self::new(
1245            services,
1246            control_atom,
1247            ready_atom,
1248            Weak::new(),
1249            Some(notifier),
1250            None,
1251            LimitsConfig::default(),
1252        )
1253    }
1254
1255    pub(super) fn services(&self) -> &dyn ConnectionServices {
1256        self.services.as_ref()
1257    }
1258
1259    /// Returns the configured connection auth token as opaque bytes, or `None` when
1260    /// no `[auth]` section was configured (open access).
1261    pub(super) fn auth_token(&self) -> Option<&[u8]> {
1262        self.auth_token.as_deref()
1263    }
1264
1265    /// Returns the configured connection-keyed notifier, if any.
1266    pub(super) fn notifier(&self) -> Option<&Arc<dyn ConnectionNotifier>> {
1267        self.notifier.as_ref()
1268    }
1269
1270    /// Offers a channel publish to the notifier's observability-drain tap, returning
1271    /// `true` when the application consumed it (so the connection process skips the
1272    /// normal fan-out). `false` when no notifier is installed (liminal standalone) or
1273    /// the notifier did not recognise the channel, so the caller can invoke it
1274    /// unconditionally and fall through to the normal publish path.
1275    pub(super) fn notifier_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
1276        self.notifier
1277            .as_ref()
1278            .is_some_and(|notifier| notifier.on_channel_publish(pid, channel, payload))
1279    }
1280
1281    /// Stores `registration` on the connection record for `pid`, so the close
1282    /// path can later fire `on_worker_unregistered` for exactly the connections
1283    /// that registered. A missing record (the connection already closed) is a
1284    /// no-op.
1285    ///
1286    /// # Errors
1287    /// Returns [`ServerError`] when the connection registry mutex is poisoned.
1288    pub(super) fn set_registration(
1289        &self,
1290        pid: u64,
1291        registration: WorkerRegistration,
1292    ) -> Result<(), ServerError> {
1293        if let Some(record) = lock(&self.records, "connection registry")?.get_mut(&pid) {
1294            record.registration = Some(registration);
1295        }
1296        Ok(())
1297    }
1298
1299    /// Allocates the next monotonic push correlation id.
1300    fn next_push_correlation_id(&self) -> u64 {
1301        self.next_push_id.fetch_add(1, Ordering::Relaxed)
1302    }
1303
1304    /// Registers a one-shot reply slot for `correlation_id`, owned by connection
1305    /// `pid`, and returns its receiver. `deadline` is the slot's optional absolute
1306    /// reply expiry (`None` = the default no-deadline shape). The connection
1307    /// process resolves the slot via [`resolve_push`]; the close path drops the
1308    /// connection's outstanding slots via [`cancel_pushes_for_connection`]; an
1309    /// explicit deadline resolves it via [`expire_push_if_due`].
1310    ///
1311    /// # Errors
1312    /// Returns [`ServerError`] when the correlation registry mutex is poisoned.
1313    fn register_push(
1314        &self,
1315        pid: u64,
1316        correlation_id: u64,
1317        deadline: Option<Instant>,
1318    ) -> Result<Receiver<Vec<u8>>, ServerError> {
1319        let (sender, receiver) = channel();
1320        let limit = self.limits.max_pending_pushes_per_connection;
1321        {
1322            let mut slots = lock(&self.push_replies, "push correlation registry")?;
1323            // §5 `max_pending_pushes_per_connection`: refuse a new in-flight push
1324            // once this connection already holds the cap. Counted per owning pid so
1325            // one connection cannot exhaust the shared registry; slots free on
1326            // reply, deadline expiry, or connection close. The count-and-insert
1327            // stays under the one lock so the cap is enforced atomically.
1328            let outstanding = slots.values().filter(|pending| pending.pid == pid).count();
1329            if outstanding >= limit {
1330                return Err(ServerError::ConnectionCapReached {
1331                    operation: "server push".to_owned(),
1332                    cap: "max_pending_pushes_per_connection",
1333                    limit,
1334                });
1335            }
1336            slots.insert(
1337                correlation_id,
1338                PendingPush {
1339                    pid,
1340                    sender,
1341                    deadline,
1342                },
1343            );
1344        }
1345        Ok(receiver)
1346    }
1347
1348    /// Host-side, lazy evaluation of a push's reply deadline, called from an
1349    /// elapsed [`PushReplyAwaiter::receive`] quantum. This NEVER wakes the
1350    /// connection process and runs no timer — it inspects supervisor-owned state
1351    /// under the registry lock only.
1352    ///
1353    /// A slot with an explicit deadline that has passed is removed here (dropping
1354    /// its `Sender` and releasing its §5 `max_pending_pushes_per_connection` cap
1355    /// admission, since the cap is the per-pid slot count) and reported
1356    /// [`PushSlotDisposition::Expired`]. A slot with no deadline, or a deadline
1357    /// still in the future, is left UNTOUCHED and reported
1358    /// [`PushSlotDisposition::Live`] — the elapsed quantum is a benign re-arm. A
1359    /// missing slot is [`PushSlotDisposition::Absent`].
1360    fn expire_push_if_due(&self, correlation_id: u64) -> PushSlotDisposition {
1361        // S4: a poisoned registry must NOT read as slot absence — the slot (and
1362        // its cap admission) may still be in the map. Reclamation recovers the
1363        // guard: removal-only operations are sound on a recovered map (a panic
1364        // in another critical section cannot leave the HashMap itself in a
1365        // partial state; only our bookkeeping invariants could be stale, and
1366        // removal restores them). Admission (`register_push`) stays fail-closed.
1367        let mut slots = recover_lock(&self.push_replies);
1368        let Some(pending) = slots.get(&correlation_id) else {
1369            return PushSlotDisposition::Absent;
1370        };
1371        // Copy the deadline out so the immutable borrow of `slots` ends before the
1372        // conditional `remove` below takes a mutable one.
1373        let deadline = pending.deadline;
1374        match deadline {
1375            Some(at) if Instant::now() >= at => {
1376                slots.remove(&correlation_id);
1377                PushSlotDisposition::Expired
1378            }
1379            _ => PushSlotDisposition::Live,
1380        }
1381    }
1382
1383    /// Drops a registered reply slot without resolving it, used on the
1384    /// push-enqueue failure path (the control could not be delivered to a
1385    /// now-gone process, so the just-reserved slot is unreachable). Dropping the
1386    /// slot's `Sender` wakes a still-waiting awaiter with a disconnected error.
1387    ///
1388    /// Returns whether THIS call removed the slot. Removal under the registry
1389    /// mutex is the atomic resolved-vs-cancelled transition: `false` means
1390    /// another path won — [`resolve_push`](Self::resolve_push) already sent the
1391    /// reply (its send happens under the same lock, so the payload is already in
1392    /// the channel when this returns), or the connection's close path dropped
1393    /// the slot (sender gone, channel disconnected).
1394    pub(super) fn cancel_push(&self, correlation_id: u64) -> bool {
1395        // S4: reclamation recovers a poisoned guard — a rollback that silently
1396        // skipped its removal would strand the slot and its cap admission.
1397        recover_lock(&self.push_replies)
1398            .remove(&correlation_id)
1399            .is_some()
1400    }
1401
1402    /// Drops every reply slot owned by connection `pid`, waking each awaiter with a
1403    /// disconnected error (the dropped `Sender` disconnects the awaiter's
1404    /// `Receiver`). Called from the close path so a connection that exits with
1405    /// in-flight pushes signals worker death immediately instead of leaving each
1406    /// awaiter to block the full push-reply timeout. A slot that [`resolve_push`]
1407    /// already removed is gone, so it is untouched here; an unknown pid is a no-op.
1408    fn cancel_pushes_for_connection(&self, pid: u64) {
1409        // S4: the close sweep is the reclamation of last resort ("connection
1410        // close at the latest") — it must complete on a poisoned map too.
1411        recover_lock(&self.push_replies).retain(|_correlation_id, pending| pending.pid != pid);
1412    }
1413
1414    /// S3 second half (shape (b), check-after-insert): pre-publication
1415    /// confirmation that the connection record for `pid` still exists, run in
1416    /// the INSERT -> CONFIRM -> PUBLISH order (S7 — confirming after the
1417    /// enqueue let a close-swept-then-answered push report `Err` for a Push the
1418    /// client had received). `true` leaves the slot in place and the caller may
1419    /// publish; `false` means a concurrent close already removed the record —
1420    /// this call then removes the caller's own just-inserted slot (rolling back
1421    /// its cap admission) so nothing is stranded, and the caller returns
1422    /// WITHOUT publishing: an `Err` from the push methods guarantees no `Push`
1423    /// control was published.
1424    ///
1425    /// Why exactly one side always observes the slot: `remove` (the single
1426    /// record-removal path) removes the host record BEFORE sweeping the pid's
1427    /// push slots, and this check reads the record AFTER inserting the slot and
1428    /// BEFORE the control is published. Both records accesses are serialized by
1429    /// the `records` mutex, so either (i) this read precedes the record removal
1430    /// — then the slot insert precedes the sweep (insert < read < removal <
1431    /// sweep in the happens-before order) and the SWEEP observes and removes
1432    /// the slot: if the control was published in the meantime the awaiter reads
1433    /// the truthful disconnected outcome and a late client reply is a harmless
1434    /// no-op; or (ii) this read follows the record removal — then THIS call
1435    /// observes the absence, rolls the slot back itself, and nothing was
1436    /// published. When both observe (a sweep and a rollback can both run in
1437    /// case (ii) if the insert also preceded the sweep), removal is idempotent
1438    /// and the cap is derived from map membership, so nothing double-releases.
1439    ///
1440    /// Lock discipline: `records` and `push_replies` are NEVER held together —
1441    /// here (`records` read, released, then `push_replies` on rollback), in
1442    /// `remove` (`records` removal, released, then the sweep), and everywhere
1443    /// else in this file the two mutexes are taken strictly sequentially, so no
1444    /// lock-order inversion is possible. This adds ZERO work to the connection
1445    /// slice path: the re-check runs on the push caller's thread only.
1446    pub(super) fn confirm_push_registration(&self, pid: u64, correlation_id: u64) -> bool {
1447        if self.is_registered(pid) {
1448            return true;
1449        }
1450        self.cancel_push(correlation_id);
1451        false
1452    }
1453
1454    /// Number of reserved push reply slots outstanding. A benign wait-quantum
1455    /// timeout must NOT change this (the slot survives); an explicit-deadline
1456    /// expiry, a consumed reply, and connection close each release exactly one.
1457    #[cfg(test)]
1458    pub(super) fn pending_push_count(&self) -> usize {
1459        recover_lock(&self.push_replies).len()
1460    }
1461
1462    /// Reserved push reply slots owned by connection `pid` — the exact quantity
1463    /// the §5 `max_pending_pushes_per_connection` cap counts (test instrument).
1464    #[cfg(test)]
1465    pub(super) fn pending_push_count_for(&self, pid: u64) -> usize {
1466        recover_lock(&self.push_replies)
1467            .values()
1468            .filter(|pending| pending.pid == pid)
1469            .count()
1470    }
1471
1472    /// Resolves the reply slot for `correlation_id` with the client's reply
1473    /// payload, waking the [`PushReplyAwaiter`]. Called by the connection process
1474    /// when a correlated `PushReply` frame arrives. A missing slot — already
1475    /// resolved, expired at its explicit deadline, dropped by connection close, or
1476    /// an unknown id — is a harmless no-op: a late `PushReply` for a slot that is
1477    /// gone is discarded here, never delivered and never a panic or desync.
1478    pub(super) fn resolve_push(&self, correlation_id: u64, payload: Vec<u8>) {
1479        // S4: delivery-plus-removal recovers a poisoned guard — dropping a real
1480        // reply (and stranding its slot) because an unrelated critical section
1481        // panicked would kill reclamation and exact cap accounting.
1482        let mut slots = recover_lock(&self.push_replies);
1483        if let Some(pending) = slots.remove(&correlation_id) {
1484            // The send stays under the registry lock so removal and delivery are
1485            // one atomic step: a timed-out awaiter that observes the slot gone
1486            // (its `cancel_push` returned false) is then GUARANTEED to find the
1487            // payload already in the channel — without this ordering the awaiter
1488            // could see the removal, find the channel still empty, and report a
1489            // timeout for a reply that was about to land. The send itself never
1490            // blocks (unbounded channel), and a receiver dropped after an
1491            // abandoned wait makes it a benign discard.
1492            pending.sender.send(payload).ok();
1493        }
1494    }
1495
1496    pub(super) const fn control_atom(&self) -> Atom {
1497        self.control_atom
1498    }
1499
1500    /// Sole registration path for a connection: the spawn thread inserts the
1501    /// record synchronously, before `spawn_connection` returns the handle, so
1502    /// `is_tracked`/`active_connection_count` reflect the connection
1503    /// immediately. The connection handler never writes the registry (it only
1504    /// reads via `mark_crashed`/`finish`), so there is a single writer here and
1505    /// no register/ensure-register race.
1506    ///
1507    /// Ordering note: `spawn_native` only enqueues the process, so its first
1508    /// slice may run on another worker thread before this insert lands. If that
1509    /// first slice exits immediately (e.g. a missing-stream crash) its
1510    /// `mark_crashed`/`finish` removes nothing and this insert then leaves a
1511    /// record for an already-dead pid. That orphan is self-healing:
1512    /// `reap_crashed`, driven continuously by the listener loop, removes any
1513    /// record whose pid is absent from the scheduler process table.
1514    fn register_with_fd(
1515        &self,
1516        pid: u64,
1517        peer_addr: Option<SocketAddr>,
1518        fd_guard: TcpStream,
1519    ) -> Result<(), ServerError> {
1520        self.register_record(pid, peer_addr, Some(fd_guard))
1521    }
1522
1523    #[cfg(test)]
1524    fn register(&self, pid: u64, peer_addr: Option<SocketAddr>) -> Result<(), ServerError> {
1525        self.register_record(pid, peer_addr, None)
1526    }
1527
1528    fn register_record(
1529        &self,
1530        pid: u64,
1531        peer_addr: Option<SocketAddr>,
1532        fd_guard: Option<TcpStream>,
1533    ) -> Result<(), ServerError> {
1534        lock(&self.records, "connection registry")?.insert(
1535            pid,
1536            ConnectionRecord {
1537                peer_addr,
1538                registration: None,
1539                readiness: None,
1540                fd_guard,
1541            },
1542        );
1543        // Single-writer insert (see doc above) pairs one gauge increment with the
1544        // decrement in `remove`, keeping `liminal_connections_active` equal to the
1545        // live record count on every teardown route.
1546        crate::metrics::connection_spawned();
1547        Ok(())
1548    }
1549
1550    pub(super) fn mark_crashed(&self, pid: u64, reason: ExitReason, peer_addr: Option<SocketAddr>) {
1551        let removed = self.remove(pid);
1552        if let Some(record) = removed.as_ref() {
1553            self.fire_unregistered(pid, record);
1554        }
1555        let removed_peer_addr = removed
1556            .as_ref()
1557            .and_then(|record| record.peer_addr)
1558            .or(peer_addr);
1559        tracing::warn!(
1560            connection_pid = pid,
1561            peer_addr = ?removed_peer_addr,
1562            reason = ?reason,
1563            "connection process crashed"
1564        );
1565    }
1566
1567    /// Whether the spawn thread has installed the host record. A first native
1568    /// slice can win the enqueue-vs-record race and must remain runnable until it
1569    /// has somewhere host-reachable to publish its readiness token.
1570    pub(super) fn is_registered(&self, pid: u64) -> bool {
1571        self.contains(pid)
1572    }
1573
1574    /// Removes a token minted in-slice when publishing it to the host record fails.
1575    pub(super) fn deregister_unpublished_readiness(&self, token: ReadinessToken) {
1576        if let Some(scheduler) = self.scheduler.upgrade() {
1577            scheduler.readiness_deregister(token);
1578        }
1579    }
1580
1581    /// Cancels deadline timers detached by reply completion or connection close.
1582    pub(super) fn cancel_deadline_timers(&self, timers: Vec<TimerRef>) {
1583        let Some(scheduler) = self.scheduler.upgrade() else {
1584            return;
1585        };
1586        if let Ok(mut wheel) = scheduler.timers().lock() {
1587            for timer in timers {
1588                wheel.cancel(timer);
1589            }
1590        }
1591    }
1592
1593    pub(super) fn finish(&self, pid: u64) {
1594        if let Some(removed) = self.remove(pid) {
1595            self.fire_unregistered(pid, &removed);
1596        }
1597    }
1598
1599    /// Records the one readiness token minted for this connection. A live
1600    /// connection never re-registers: later parked slices rearm this identity.
1601    pub(super) fn set_readiness_token_once(
1602        &self,
1603        pid: u64,
1604        token: ReadinessToken,
1605        fd: RawFd,
1606    ) -> Result<(), ServerError> {
1607        let mut records = lock(&self.records, "connection registry")?;
1608        let record = records
1609            .get_mut(&pid)
1610            .ok_or_else(|| ServerError::ListenerAccept {
1611                message: format!("connection {pid} has no host record for readiness registration"),
1612            })?;
1613        if record.readiness.is_some() {
1614            return Err(ServerError::ListenerAccept {
1615                message: format!("connection {pid} attempted to replace its readiness token"),
1616            });
1617        }
1618        record.readiness = Some(ReadinessRegistration { token, fd });
1619        drop(records);
1620        Ok(())
1621    }
1622
1623    /// Invokes `on_worker_unregistered` for a removed connection record that
1624    /// carried a worker registration. A record with no registration (a plain
1625    /// connection, or a worker connection that never registered) is a no-op, so
1626    /// only connections that actually registered deregister.
1627    fn fire_unregistered(&self, pid: u64, record: &ConnectionRecord) {
1628        if record.registration.is_some() {
1629            if let Some(notifier) = self.notifier.as_ref() {
1630                notifier.on_worker_unregistered(pid);
1631            }
1632        }
1633    }
1634
1635    fn reap_crashed(&self, scheduler: &Scheduler) -> usize {
1636        let pids = match self.records.lock() {
1637            Ok(records) => records.keys().copied().collect::<Vec<_>>(),
1638            Err(error) => {
1639                tracing::warn!(%error, "connection registry unavailable during crash reap");
1640                return 0;
1641            }
1642        };
1643        let mut reaped = 0;
1644        for pid in pids {
1645            if scheduler.process_table().get(pid).is_none() {
1646                let removed = self.remove(pid);
1647                if let Some(record) = removed.as_ref() {
1648                    self.fire_unregistered(pid, record);
1649                }
1650                let peer_addr = removed.and_then(|record| record.peer_addr);
1651                // This process exited without ever reaching `mark_crashed`/`finish`
1652                // (e.g. the beamr scheduler terminated it externally). beamr records
1653                // the real `ExitReason` in its private `exit_tombstones` map, but its
1654                // public `Scheduler` API exposes no non-blocking accessor for it
1655                // (only `run_until_exit`, which blocks). So we cannot recover the true
1656                // reason here; log a truthful, specific message rather than the
1657                // misleading literal "unknown". If beamr later grows a public,
1658                // non-blocking exit-reason query for a dead pid, read it here instead.
1659                tracing::warn!(
1660                    connection_pid = pid,
1661                    ?peer_addr,
1662                    reason = "terminated externally (no exit reason recorded by supervisor)",
1663                    "connection process crashed"
1664                );
1665                reaped += 1;
1666            }
1667        }
1668        reaped
1669    }
1670
1671    fn contains(&self, pid: u64) -> bool {
1672        self.records
1673            .lock()
1674            .is_ok_and(|records| records.contains_key(&pid))
1675    }
1676
1677    #[cfg(test)]
1678    fn readiness_registration_count(&self) -> usize {
1679        self.records.lock().map_or(0, |records| {
1680            records
1681                .values()
1682                .filter(|record| record.readiness.is_some())
1683                .count()
1684        })
1685    }
1686
1687    #[cfg(test)]
1688    fn readiness_fd(&self, pid: u64) -> Option<RawFd> {
1689        self.records
1690            .lock()
1691            .ok()?
1692            .get(&pid)
1693            .and_then(|record| record.readiness.map(|registration| registration.fd))
1694    }
1695
1696    fn active_connections(&self) -> Vec<ActiveConnection> {
1697        self.records.lock().map_or_else(
1698            |_| Vec::new(),
1699            |records| {
1700                records
1701                    .iter()
1702                    .map(|(&pid, record)| ActiveConnection {
1703                        pid,
1704                        peer_addr: record.peer_addr,
1705                    })
1706                    .collect()
1707            },
1708        )
1709    }
1710
1711    fn push_control(&self, pid: u64, control: ConnectionControl) -> Result<(), ServerError> {
1712        lock(&self.controls, "connection control queue")?
1713            .push(QueuedConnectionControl { pid, control });
1714        Ok(())
1715    }
1716
1717    pub(super) fn pop_control(&self, pid: u64) -> Option<ConnectionControl> {
1718        let mut controls = self.controls.lock().ok()?;
1719        let index = controls.iter().position(|queued| queued.pid == pid)?;
1720        Some(controls.remove(index).control)
1721    }
1722
1723    /// Non-consuming final-probe query for controls enqueued after mailbox drain.
1724    pub(super) fn has_control(&self, pid: u64) -> bool {
1725        self.controls
1726            .lock()
1727            .is_ok_and(|controls| controls.iter().any(|queued| queued.pid == pid))
1728    }
1729
1730    /// Pulls a queued-but-unconsumed control back out of the queue. Returns
1731    /// whether THIS call removed it — `false` means the entry already left the
1732    /// queue, and since [`Self::pop_control`] is the only other remover, a
1733    /// consumer drain took it (S8's publication disambiguator). Matching is
1734    /// `pid` + full control equality; a `Push` control embeds its
1735    /// runtime-unique correlation id, so this can never remove a different
1736    /// push's entry and misreport.
1737    fn remove_control(&self, pid: u64, control: &ConnectionControl) -> bool {
1738        let Ok(mut controls) = self.controls.lock() else {
1739            return false;
1740        };
1741        let Some(index) = controls
1742            .iter()
1743            .position(|queued| queued.pid == pid && &queued.control == control)
1744        else {
1745            return false;
1746        };
1747        controls.remove(index);
1748        true
1749    }
1750
1751    fn active_count(&self) -> usize {
1752        self.records.lock().map_or(0, |records| records.len())
1753    }
1754
1755    /// Removes the connection record for `pid` and, in the same close step, drops
1756    /// every push reply slot that connection still owns so each waiting
1757    /// [`PushReplyAwaiter`] wakes immediately with a disconnected error. This runs
1758    /// on every close route — `finish`, `mark_crashed`, and `reap_crashed` all
1759    /// remove through here — and fires regardless of whether the connection ever
1760    /// registered a worker, so a plain push target is covered too.
1761    ///
1762    /// ORDER MATTERS (S3/S7): the record is removed BEFORE the push sweep. A
1763    /// push registering concurrently runs INSERT -> CONFIRM -> PUBLISH
1764    /// (`confirm_push_registration` reads the record after inserting its slot
1765    /// and before publishing its control), so with this ordering exactly one
1766    /// side always observes a racing slot: a confirm that ran before this
1767    /// removal implies the slot was inserted before the sweep below (which then
1768    /// reaps it — a control published after that confirm is answered into a
1769    /// swept slot, read as the truthful disconnected outcome); a confirm after
1770    /// this removal sees the absence, rolls the slot back itself, and never
1771    /// publishes. Sweeping first (the original order) left a window — sweep,
1772    /// then insert+confirm, then record removal — where NEITHER side observed
1773    /// the slot and it leaked past connection close. The two locks are taken
1774    /// strictly sequentially (never nested), so no lock-order inversion.
1775    fn remove(&self, pid: u64) -> Option<ConnectionRecord> {
1776        let mut removed = self
1777            .records
1778            .lock()
1779            .ok()
1780            .and_then(|mut records| records.remove(&pid));
1781        self.cancel_pushes_for_connection(pid);
1782        if let Some(registration) = removed.as_mut().and_then(|record| record.readiness.take()) {
1783            if let Some(scheduler) = self.scheduler.upgrade() {
1784                // This call is ACK'd: it returns only after the poll owner has
1785                // removed the registration. `fd_guard` is still live here.
1786                scheduler.readiness_deregister(registration.token);
1787                tracing::debug!(
1788                    registered_fd = registration.fd,
1789                    "connection readiness deregistration acknowledged"
1790                );
1791            }
1792        }
1793        // Decrement only when a record was actually present so a double-remove
1794        // (e.g. `finish` after `reap_crashed`) cannot drive the gauge negative.
1795        // The §5 admission slot is released on the same guard: the reservation
1796        // acquired in `spawn_connection` converted into this record at
1797        // `register`, so its removal is exactly one release per reservation.
1798        if removed.is_some() {
1799            crate::metrics::connection_closed();
1800            self.release_admission();
1801        }
1802        if let Some(record) = removed.as_mut() {
1803            // Explicit after-deregister drop documents and enforces the fd wall.
1804            drop(record.fd_guard.take());
1805        }
1806        removed
1807    }
1808}
1809
1810/// One in-flight server-push reply slot, associating the awaiter's reply `sender`
1811/// with the `pid` of the connection that owns the push. The pid lets the close
1812/// path drop exactly that connection's slots; the correlation id (the map key)
1813/// still drives [`ConnectionRuntime::resolve_push`] and
1814/// [`ConnectionRuntime::cancel_push`].
1815#[derive(Debug)]
1816struct PendingPush {
1817    pid: u64,
1818    sender: Sender<Vec<u8>>,
1819    /// Absolute reply deadline for this push, when one was requested via
1820    /// [`ConnectionSupervisor::push_to_connection_with_deadline`]. `None` is the
1821    /// default 0.2.3 shape: the slot has no per-slot deadline and is reclaimed
1822    /// only by reply-consumed or connection-close. `Some` is evaluated host-side
1823    /// and lazily in [`ConnectionRuntime::expire_push_if_due`].
1824    deadline: Option<Instant>,
1825}
1826
1827/// Host-side disposition of a reply slot at an elapsed `receive` quantum.
1828enum PushSlotDisposition {
1829    /// The slot carried an explicit deadline that has passed; this call removed
1830    /// it (releasing its §5 cap admission).
1831    Expired,
1832    /// The slot is present with no deadline, or a deadline still in the future:
1833    /// the elapsed quantum is a benign re-arm and the slot is untouched.
1834    Live,
1835    /// No slot for this correlation id — a concurrent resolve or connection close
1836    /// already removed it.
1837    Absent,
1838}
1839
1840#[derive(Debug)]
1841struct ConnectionRecord {
1842    peer_addr: Option<SocketAddr>,
1843    /// Worker registration declared on this connection, set by `set_registration`
1844    /// when a `WorkerRegister` frame is accepted. `Some` marks a connection whose
1845    /// close must fire `on_worker_unregistered`.
1846    registration: Option<WorkerRegistration>,
1847    /// Host-reachable identity for ACK'd deregistration after external death.
1848    readiness: Option<ReadinessRegistration>,
1849    /// Keeps the fd alive until deregistration has been acknowledged, preventing
1850    /// stale registration delivery to a subsequently reused descriptor number.
1851    fd_guard: Option<TcpStream>,
1852}
1853
1854#[derive(Debug, Clone, Copy)]
1855struct ReadinessRegistration {
1856    token: ReadinessToken,
1857    fd: RawFd,
1858}
1859
1860#[derive(Debug, Clone, PartialEq, Eq)]
1861struct QueuedConnectionControl {
1862    pid: u64,
1863    control: ConnectionControl,
1864}
1865
1866fn lock<'a, T>(mutex: &'a Mutex<T>, context: &str) -> Result<MutexGuard<'a, T>, ServerError> {
1867    mutex.lock().map_err(|error| ServerError::ListenerAccept {
1868        message: format!("{context} unavailable: {error}"),
1869    })
1870}
1871
1872/// Locks `mutex`, RECOVERING a poisoned guard instead of failing (S4). For
1873/// lifecycle-cleanup paths only (reply delivery, expiry, cancellation, the
1874/// close sweep): removal-style operations are sound on a recovered map, and a
1875/// cleanup that silently skipped its removal would strand slots and their §5
1876/// cap admissions forever. Admission paths keep the fail-closed [`lock`].
1877fn recover_lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1878    mutex
1879        .lock()
1880        .unwrap_or_else(std::sync::PoisonError::into_inner)
1881}