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 observation for the process-owned stream at `fd` being
439    /// dropped. External scheduler termination removes the process-table entry
440    /// before an executing native handler is destroyed, so table absence is not
441    /// sufficient evidence that the descriptor is reusable.
442    #[cfg(test)]
443    pub(super) fn observe_process_stream_drop(&self, fd: RawFd) -> Receiver<()> {
444        self.inner.runtime.observe_process_stream_drop(fd)
445    }
446
447    /// Installs a one-use arm-to-probe barrier and returns its test endpoints.
448    #[cfg(test)]
449    pub(super) fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
450        self.inner.runtime.install_pre_wait_barrier()
451    }
452
453    /// Barrier-staged final probes that observed newly arrived work.
454    #[cfg(test)]
455    pub(super) fn pre_wait_probe_hits(&self) -> u64 {
456        self.inner.runtime.pre_wait_probe_hits()
457    }
458}
459
460/// Handle for one supervised connection process.
461#[derive(Clone, Debug)]
462pub struct ConnectionHandle {
463    pid: u64,
464    peer_addr: Option<SocketAddr>,
465    supervisor: Arc<SupervisorInner>,
466}
467
468impl ConnectionHandle {
469    /// Returns the beamr process id for this connection.
470    #[must_use]
471    pub const fn pid(&self) -> u64 {
472        self.pid
473    }
474
475    /// Returns the peer address if it was available from the accepted stream.
476    #[must_use]
477    pub const fn peer_addr(&self) -> Option<SocketAddr> {
478        self.peer_addr
479    }
480
481    /// Returns whether the beamr process is still live.
482    #[must_use]
483    pub fn is_live(&self) -> bool {
484        self.supervisor
485            .scheduler
486            .process_table()
487            .get(self.pid)
488            .is_some()
489    }
490
491    /// Requests an error exit for tests and supervisor control paths.
492    ///
493    /// # Errors
494    /// Returns [`ServerError`] when the process is no longer live.
495    pub fn request_crash(&self) -> Result<(), ServerError> {
496        if self
497            .supervisor
498            .scheduler
499            .enqueue_atom_message(self.pid, Atom::ERROR)
500        {
501            Ok(())
502        } else {
503            Err(ServerError::ListenerAccept {
504                message: format!("connection process {} is not live", self.pid),
505            })
506        }
507    }
508}
509
510/// Awaits the correlated reply to a single server-initiated push.
511///
512/// Returned by [`ConnectionSupervisor::push_to_connection`]. The reply slot is
513/// resolved when the originating connection process receives a `PushReply` frame
514/// carrying the same correlation id, so [`PushReplyAwaiter::receive`] blocks
515/// (bounded) for that one correlated answer.
516#[derive(Debug)]
517pub struct PushReplyAwaiter {
518    correlation_id: u64,
519    receiver: Receiver<Vec<u8>>,
520    /// This push's absolute reply deadline, mirrored from its slot. `None` (the
521    /// default push) selects the no-deadline receive path, which NEVER touches
522    /// the runtime — byte-compatible with 0.2.3, no shared-lock exposure.
523    /// `Some` lets `receive` wait `min(caller quantum, time until deadline)` and
524    /// resolve expiry promptly, so the caller's quantum can never select a
525    /// deadlined push's terminal outcome.
526    deadline: Option<Instant>,
527    /// Weak handle to the owning runtime, used ONLY by the explicit-deadline
528    /// path to resolve expiry host-side at [`receive`](Self::receive). A
529    /// no-deadline push never upgrades it. `Weak` so the awaiter never keeps the
530    /// runtime alive; if it is already gone, the slot (and its sender) is gone
531    /// with it — the connection side is torn down.
532    runtime: Weak<ConnectionRuntime>,
533}
534
535impl PushReplyAwaiter {
536    /// Returns the correlation id this awaiter is matched on.
537    #[must_use]
538    pub const fn correlation_id(&self) -> u64 {
539        self.correlation_id
540    }
541
542    /// Blocks up to `timeout` for the client's correlated reply payload.
543    ///
544    /// `timeout` is a WAIT QUANTUM ONLY — a MAXIMUM wait, not a promise to
545    /// block: an elapsed poll is a benign re-arm, never a failure; the reply's
546    /// lifetime belongs to the push. A caller may re-invoke `receive`
547    /// indefinitely after a [`ServerError::PushReplyTimeout`]: the reserved slot
548    /// is untouched and a later reply is still delivered byte-exact. The poll
549    /// quantum never changes the protocol outcome — for a deadlined push the
550    /// call waits no longer than the EARLIER of the caller's quantum and the
551    /// push's deadline, so the terminal expiry is returned promptly once due,
552    /// never held until the quantum ends and never deferred past it.
553    ///
554    /// A push with no explicit deadline never touches shared supervisor state
555    /// here: the elapsed quantum returns straight from the channel wait
556    /// (behaviour-compatible with 0.2.3 — no registry lock, no contention, no
557    /// poison exposure on the unchanged API).
558    ///
559    /// # Errors
560    /// Returns [`ServerError::PushReplyTimeout`] when no reply arrived within this
561    /// `timeout` quantum and the push's deadline (if any) is not yet due (a
562    /// benign re-arm — call again to keep waiting);
563    /// [`ServerError::PushReplyExpired`] when the push carried an explicit reply
564    /// deadline (via
565    /// [`push_to_connection_with_deadline`](ConnectionSupervisor::push_to_connection_with_deadline))
566    /// and that deadline is due (terminal: the slot is removed and its §5 cap
567    /// admission released; returned as soon as the deadline passes, even
568    /// mid-quantum — but evaluated at observation points, not against the wall
569    /// clock: a reply already delivered when this call observes the slot wins
570    /// over expiry, even if it arrived after the deadline instant); or
571    /// [`ServerError::PushReplyDisconnected`] when the connection process
572    /// dropped the reply slot (the connection closed — the prompt worker-death
573    /// signal). The variants are distinct so callers classify by type, not
574    /// message.
575    pub fn receive(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
576        self.deadline.map_or_else(
577            || self.receive_no_deadline(timeout),
578            |deadline| self.receive_deadlined(timeout, deadline),
579        )
580    }
581
582    /// The default-push receive: exactly the 0.2.3 shape. One bounded channel
583    /// wait; an elapsed quantum is a benign timeout straight from the channel —
584    /// no runtime upgrade, no registry lock, EVER (unrelated registry work can
585    /// never stretch this call past its quantum, and registry poison cannot
586    /// reach it).
587    fn receive_no_deadline(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
588        match self.receiver.recv_timeout(timeout) {
589            Ok(payload) => Ok(payload),
590            Err(RecvTimeoutError::Timeout) => Err(ServerError::PushReplyTimeout {
591                correlation_id: self.correlation_id,
592            }),
593            Err(RecvTimeoutError::Disconnected) => Err(ServerError::PushReplyDisconnected {
594                correlation_id: self.correlation_id,
595            }),
596        }
597    }
598
599    /// The deadlined receive: waits `min(caller quantum, time until deadline)`
600    /// and re-evaluates reply-first-then-expiry on every wake, so the caller's
601    /// quantum can never select the terminal outcome (S1). Order per iteration:
602    ///
603    /// 1. Deliver a reply already in hand — an answer that is here must never be
604    ///    reported as a timeout OR an expiry (the observation-point rule).
605    /// 2. If the deadline is due, resolve expiry atomically against the registry
606    ///    (`expire_slot`) and return the terminal outcome promptly — even when
607    ///    the caller's quantum has time left (the quantum is a max wait).
608    /// 3. Otherwise wait for the earlier of quantum-remaining and deadline; a
609    ///    wake re-runs 1-2, and an exhausted quantum before the deadline is the
610    ///    benign `PushReplyTimeout` re-arm with the slot untouched.
611    fn receive_deadlined(
612        &self,
613        timeout: Duration,
614        deadline: Instant,
615    ) -> Result<Vec<u8>, ServerError> {
616        let started = Instant::now();
617        loop {
618            if let Some(result) = self.try_take_reply() {
619                return result;
620            }
621            let now = Instant::now();
622            if now >= deadline {
623                return self.expire_slot();
624            }
625            let quantum_left = timeout.saturating_sub(now.duration_since(started));
626            if quantum_left.is_zero() {
627                return Err(ServerError::PushReplyTimeout {
628                    correlation_id: self.correlation_id,
629                });
630            }
631            match self
632                .receiver
633                .recv_timeout(quantum_left.min(deadline.duration_since(now)))
634            {
635                Ok(payload) => return Ok(payload),
636                Err(RecvTimeoutError::Disconnected) => {
637                    return Err(ServerError::PushReplyDisconnected {
638                        correlation_id: self.correlation_id,
639                    });
640                }
641                // Re-loop: deliver a reply that raced the wake, expire a
642                // now-due deadline, or report the exhausted quantum benignly.
643                Err(RecvTimeoutError::Timeout) => {}
644            }
645        }
646    }
647
648    /// Resolves a due deadline against the registry's atomic removal transition.
649    fn expire_slot(&self) -> Result<Vec<u8>, ServerError> {
650        let timeout_error = || ServerError::PushReplyTimeout {
651            correlation_id: self.correlation_id,
652        };
653        let Some(runtime) = self.runtime.upgrade() else {
654            // The runtime is gone, and the slot map (with every sender) with it:
655            // the connection side is torn down. Re-check the channel so the
656            // dropped sender reads as the established DISCONNECTED outcome — a
657            // dead runtime must not be misreported as a benign healthy-but-slow
658            // timeout (S4).
659            return self
660                .try_take_reply()
661                .unwrap_or(Err(ServerError::PushReplyDisconnected {
662                    correlation_id: self.correlation_id,
663                }));
664        };
665        match runtime.expire_push_if_due(self.correlation_id) {
666            PushSlotDisposition::Expired => Err(ServerError::PushReplyExpired {
667                correlation_id: self.correlation_id,
668            }),
669            // Unreachable by construction (this is only called with the deadline
670            // due, and the registry re-reads a monotonic clock); honest benign
671            // fallback rather than a panic.
672            PushSlotDisposition::Live => Err(timeout_error()),
673            // Another path (a concurrent `resolve_push`, or connection close)
674            // removed the slot under the registry lock while we waited on it. Its
675            // send, if any, happens under that same lock, so re-check the channel:
676            // a delivered reply is present now; a dropped sender is disconnected.
677            PushSlotDisposition::Absent => self
678                .try_take_reply()
679                .unwrap_or_else(|| Err(timeout_error())),
680        }
681    }
682
683    /// Non-blocking check for a reply already sitting in the channel. `Some` with
684    /// the payload or a disconnected error; `None` when the channel is still empty
685    /// (no reply yet — the caller re-arms).
686    fn try_take_reply(&self) -> Option<Result<Vec<u8>, ServerError>> {
687        match self.receiver.try_recv() {
688            Ok(payload) => Some(Ok(payload)),
689            Err(TryRecvError::Disconnected) => Some(Err(ServerError::PushReplyDisconnected {
690                correlation_id: self.correlation_id,
691            })),
692            Err(TryRecvError::Empty) => None,
693        }
694    }
695}
696
697pub(super) struct SupervisorInner {
698    scheduler: Arc<Scheduler>,
699    runtime: Arc<ConnectionRuntime>,
700}
701
702impl std::fmt::Debug for SupervisorInner {
703    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
704        formatter
705            .debug_struct("SupervisorInner")
706            .field("runtime", &self.runtime)
707            .finish_non_exhaustive()
708    }
709}
710
711impl SupervisorInner {
712    fn new(
713        services: Arc<dyn ConnectionServices>,
714        notifier: Option<Arc<dyn ConnectionNotifier>>,
715        auth_token: Option<Vec<u8>>,
716        limits: LimitsConfig,
717    ) -> Result<Self, ServerError> {
718        let atoms = AtomTable::with_common_atoms();
719        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
720        let registry = Arc::new(ModuleRegistry::new());
721
722        let scheduler = Scheduler::with_services(
723            SchedulerConfig {
724                thread_count: Some(CONNECTION_SCHEDULER_THREADS),
725                ..SchedulerConfig::default()
726            },
727            SchedulerServices::from_config().owned_readiness(),
728            registry,
729        )
730        .map_err(|message| ServerError::ListenerAccept {
731            message: format!("failed to start connection scheduler: {message}"),
732        })?;
733        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
734        let scheduler = Arc::new(scheduler);
735        // The runtime captures a WEAK handle to the connection scheduler so
736        // notifier wakes (R3/R1(vi)) can be fired from another actor's slice
737        // without a strong scheduler↔process↔runtime cycle that would leak the
738        // whole connection scheduler.
739        Ok(Self {
740            runtime: Arc::new(ConnectionRuntime::new(
741                services,
742                control_atom,
743                ready_atom,
744                Arc::downgrade(&scheduler),
745                notifier,
746                auth_token,
747                limits,
748            )),
749            scheduler,
750        })
751    }
752
753    fn spawn_connection(
754        self: &Arc<Self>,
755        stream: TcpStream,
756    ) -> Result<ConnectionHandle, ServerError> {
757        // §5 `max_connections`: ATOMIC admission reservation acquired BEFORE any
758        // process construction (review round 1 item 7 — a signed bound must not
759        // be exceedable by concurrent callers; check-then-spawn across an
760        // unlocked window was). The CAS reservation is released on every failure
761        // path below and converts into the connection record at `register`;
762        // thereafter the single record-removal path (`remove`) releases it. An
763        // over-cap accept therefore costs nothing and the bound holds under any
764        // concurrency.
765        self.runtime.try_reserve_admission()?;
766        let reservation = AdmissionReservation {
767            runtime: &self.runtime,
768            armed: true,
769        };
770        stream
771            .set_nonblocking(true)
772            .map_err(|error| ServerError::ListenerAccept {
773                message: format!("failed to configure connection stream: {error}"),
774            })?;
775        let peer_addr = stream.peer_addr().ok();
776        // The host-held duplicate keeps the fd alive until the single record-removal
777        // path has synchronously deregistered readiness. External process death can
778        // therefore never let fd reuse overtake host-side deregistration.
779        let fd_guard = stream
780            .try_clone()
781            .map_err(|error| ServerError::ListenerAccept {
782                message: format!("failed to retain connection fd for teardown: {error}"),
783            })?;
784        let holder = Arc::new(Mutex::new(Some(stream)));
785        let runtime = Arc::clone(&self.runtime);
786        let process_holder = Arc::clone(&holder);
787        let factory: NativeHandlerFactory = Box::new(move || {
788            Box::new(ConnectionProcess::from_holder(
789                Arc::clone(&runtime),
790                peer_addr,
791                &process_holder,
792            ))
793        });
794        let pid =
795            self.scheduler
796                .spawn_native(factory)
797                .map_err(|error| ServerError::ListenerAccept {
798                    message: format!("failed to spawn connection process: {error}"),
799                })?;
800        if let Err(error) = self.runtime.register_with_fd(pid, peer_addr, fd_guard) {
801            // Registration failure leaves no host record to reap. Terminate the
802            // just-spawned process explicitly so neither its stream nor admission
803            // reservation can escape this failed spawn.
804            self.scheduler.terminate_process(pid, ExitReason::Error);
805            return Err(error);
806        }
807        // The reservation is now owned by the registered record: `remove` (the
808        // single record-removal path — finish/mark_crashed/reap all funnel
809        // through it) releases the admission when the record goes away.
810        reservation.convert();
811        Ok(ConnectionHandle {
812            pid,
813            peer_addr,
814            supervisor: Arc::clone(self),
815        })
816    }
817
818    fn broadcast_control(&self, control: &ConnectionControl) {
819        for connection in self.runtime.active_connections() {
820            if !self.enqueue_control(connection.pid, control.clone()) {
821                tracing::debug!(
822                    connection_pid = connection.pid,
823                    peer_addr = ?connection.peer_addr,
824                    ?control,
825                    "connection control message skipped because process is not live"
826                );
827            }
828        }
829    }
830
831    /// Queues `control` for `pid` and wakes the process. Returns whether the
832    /// control was PUBLISHED (left in the queue with a successful wake, or
833    /// already consumed by a drain) — `false` guarantees no consumer ever saw
834    /// it.
835    ///
836    /// S8: a failed wake does NOT prove the queued control was never consumed.
837    /// The insert releases the queue lock before the wake attempt, and a
838    /// process already executing a control drain (each control atom drains ALL
839    /// queued controls for the pid) can pop the just-inserted entry in that
840    /// window, then exit before the wake check. Publication is therefore
841    /// disambiguated BY OBSERVATION on the failed-wake path: `remove_control`
842    /// finding and removing the entry proves no consumer saw it (truly
843    /// unpublished — `false`); finding nothing proves a drain consumed it
844    /// (`pop_control` is the only other remover of queue entries, and the
845    /// removal key embeds the push's runtime-unique correlation id, so it can
846    /// never match a different entry) — the control was published and the
847    /// caller's slot lifecycle carries the delivery truth (`true`).
848    fn enqueue_control(&self, pid: u64, control: ConnectionControl) -> bool {
849        // Keep a key for the failure-path removal before the control is moved into
850        // the queue, so a non-`Copy` (push) control can still be located and pulled
851        // back out if the scheduler wakeup fails.
852        let removal_key = control.clone();
853        if self.runtime.push_control(pid, control).is_err() {
854            return false;
855        }
856        // Deterministic test seam in the insert->wake window (S8 staging).
857        #[cfg(test)]
858        self.runtime.run_pre_wake_barrier();
859        if self
860            .scheduler
861            .enqueue_atom_message(pid, self.runtime.control_atom())
862        {
863            true
864        } else {
865            // Failed wake: the entry's fate is the publication verdict. Removed
866            // here => nobody consumed it => unpublished. Already gone => a
867            // drain consumed it before the wake check => published. (A poisoned
868            // queue lock reads as not-removed => published — the safe
869            // direction: the slot lifecycle then reports the truthful outcome,
870            // whereas claiming "unpublished" could be a lie.)
871            !self.runtime.remove_control(pid, &removal_key)
872        }
873    }
874}
875
876/// RAII guard for one §5 `max_connections` admission reservation.
877///
878/// Acquired (via [`ConnectionRuntime::try_reserve_admission`]) before any process
879/// construction in `spawn_connection`; every early-return failure path releases
880/// it through `Drop`, and a successful `register` converts it into the
881/// connection record (whose removal releases the admission instead). RAII means
882/// no failure path — present or future — can leak a reservation.
883struct AdmissionReservation<'a> {
884    runtime: &'a ConnectionRuntime,
885    armed: bool,
886}
887
888impl AdmissionReservation<'_> {
889    /// Converts the reservation into record ownership: `Drop` no longer releases
890    /// it, because the registered record's removal will.
891    fn convert(mut self) {
892        self.armed = false;
893    }
894}
895
896impl Drop for AdmissionReservation<'_> {
897    fn drop(&mut self) {
898        if self.armed {
899            self.runtime.release_admission();
900        }
901    }
902}
903
904#[derive(Debug, Clone, PartialEq, Eq)]
905pub(super) enum ConnectionControl {
906    NotifyShutdown,
907    ForceClose,
908    /// Server-initiated push of an opaque payload, correlated by `correlation_id`,
909    /// to be written out as a [`Frame::Push`] by the receiving connection process.
910    Push {
911        correlation_id: u64,
912        payload: Vec<u8>,
913    },
914}
915
916#[derive(Debug, Clone, Copy, PartialEq, Eq)]
917pub struct ActiveConnection {
918    pid: u64,
919    peer_addr: Option<SocketAddr>,
920}
921
922#[cfg(test)]
923#[derive(Debug, Clone)]
924struct PreWaitBarrier {
925    armed: Arc<Barrier>,
926    release: Arc<Barrier>,
927}
928
929#[derive(Debug)]
930pub(super) struct ConnectionRuntime {
931    services: Arc<dyn ConnectionServices>,
932    records: Mutex<HashMap<u64, ConnectionRecord>>,
933    controls: Mutex<Vec<QueuedConnectionControl>>,
934    control_atom: Atom,
935    /// R6 single `READY` wake atom for this connection scheduler. Fired by every
936    /// wake source's notifier (R3/R1(vi)); coalescing and duplicates are harmless.
937    ready_atom: Atom,
938    /// Weak handle to the connection scheduler, used to build [`ReadyWaker`]s a
939    /// notifier fires from another actor's slice. Weak so it never keeps the
940    /// scheduler alive (the scheduler owns the processes that own this runtime).
941    scheduler: Weak<Scheduler>,
942    /// R7 (§1.2(6)) test-only per-connection slice counter, keyed by pid. Bumped
943    /// once at the head of every serviced slice. The park-flip's permanent rule-1
944    /// assertion (a parked connection's counter must not advance without an event)
945    /// reads this; the instrument lands now with a test proving it counts slices.
946    #[cfg(test)]
947    slice_counts: Mutex<HashMap<u64, u64>>,
948    /// Deterministic test gate placed after arm and before the final probe.
949    #[cfg(test)]
950    pre_wait_barrier: Mutex<Option<PreWaitBarrier>>,
951    /// Deterministic test gate in `enqueue_control`'s insert->wake window (S8).
952    #[cfg(test)]
953    pre_wake_barrier: Mutex<Option<PreWaitBarrier>>,
954    /// Barrier-staged slices where the final probe found newly arrived work.
955    #[cfg(test)]
956    pre_wait_probe_hits: AtomicU64,
957    /// One-use observers for process-owned streams reaching their actual drop
958    /// boundary after external scheduler termination.
959    #[cfg(test)]
960    process_stream_drop_observers: Mutex<HashMap<RawFd, Sender<()>>>,
961    /// One-shot reply slots for in-flight server pushes, keyed by correlation id.
962    /// The supervisor registers a slot in `push_to_connection`; the connection
963    /// process resolves it when the matching `PushReply` frame arrives. Each slot
964    /// records the owning connection pid so the close path can drop a connection's
965    /// outstanding slots and wake their awaiters with a prompt disconnected error.
966    push_replies: Mutex<HashMap<u64, PendingPush>>,
967    /// Monotonic source of push correlation ids. Server-allocated, so it never
968    /// collides with a client-chosen id on this connection.
969    next_push_id: AtomicU64,
970    /// §5 `max_connections` admission counter. Incremented atomically (CAS
971    /// against the limit) BEFORE a connection process is constructed and
972    /// decremented on every spawn-failure path and on final record removal, so
973    /// the signed bound holds under concurrent spawns — admission is never
974    /// derived from the records-map length across an unlocked window.
975    admissions: AtomicU64,
976    /// Optional application hook invoked on worker registration and on the close
977    /// of a connection that had registered. `None` keeps liminal standalone: a
978    /// `WorkerRegister` is accepted with no callback.
979    notifier: Option<Arc<dyn ConnectionNotifier>>,
980    /// Configured connection auth token (the `[auth]` section's token as opaque
981    /// bytes). `Some` gates the `Connect` handshake — the frame's `auth_token` must
982    /// match under a constant-time comparison; `None` leaves the server open-access,
983    /// byte-identical to the pre-auth behaviour.
984    auth_token: Option<Vec<u8>>,
985    /// Operational caps (§5). Enforced with typed refusals at admission:
986    /// per-connection subscription, conversation, push, and pending-reply counts,
987    /// plus the shared inbox byte budget. Non-config constructors carry the signed
988    /// defaults ([`LimitsConfig::default`]).
989    limits: LimitsConfig,
990}
991
992impl ConnectionRuntime {
993    fn new(
994        services: Arc<dyn ConnectionServices>,
995        control_atom: Atom,
996        ready_atom: Atom,
997        scheduler: Weak<Scheduler>,
998        notifier: Option<Arc<dyn ConnectionNotifier>>,
999        auth_token: Option<Vec<u8>>,
1000        limits: LimitsConfig,
1001    ) -> Self {
1002        Self {
1003            services,
1004            records: Mutex::new(HashMap::new()),
1005            controls: Mutex::new(Vec::new()),
1006            control_atom,
1007            ready_atom,
1008            scheduler,
1009            #[cfg(test)]
1010            slice_counts: Mutex::new(HashMap::new()),
1011            #[cfg(test)]
1012            pre_wait_barrier: Mutex::new(None),
1013            #[cfg(test)]
1014            pre_wake_barrier: Mutex::new(None),
1015            #[cfg(test)]
1016            pre_wait_probe_hits: AtomicU64::new(0),
1017            #[cfg(test)]
1018            process_stream_drop_observers: Mutex::new(HashMap::new()),
1019            push_replies: Mutex::new(HashMap::new()),
1020            next_push_id: AtomicU64::new(1),
1021            admissions: AtomicU64::new(0),
1022            notifier,
1023            auth_token,
1024            limits,
1025        }
1026    }
1027
1028    /// Atomically reserves one §5 `max_connections` admission slot: a CAS loop
1029    /// against the configured limit, so N concurrent callers racing for the last
1030    /// slot admit EXACTLY one — the bound cannot be transiently exceeded.
1031    ///
1032    /// # Errors
1033    /// Returns [`ServerError::ConnectionLimitReached`] when every slot is taken.
1034    fn try_reserve_admission(&self) -> Result<(), ServerError> {
1035        let limit = self.limits.max_connections as u64;
1036        let mut current = self.admissions.load(Ordering::Acquire);
1037        loop {
1038            if current >= limit {
1039                return Err(ServerError::ConnectionLimitReached {
1040                    limit: self.limits.max_connections,
1041                });
1042            }
1043            match self.admissions.compare_exchange_weak(
1044                current,
1045                current + 1,
1046                Ordering::AcqRel,
1047                Ordering::Acquire,
1048            ) {
1049                Ok(_) => return Ok(()),
1050                Err(observed) => current = observed,
1051            }
1052        }
1053    }
1054
1055    /// Releases one admission slot. Called by the spawn failure paths (via the
1056    /// [`AdmissionReservation`] guard) and by [`Self::remove`] when a registered
1057    /// record is removed — exactly one release per reservation. Saturating so a
1058    /// spurious release can never wrap the counter.
1059    fn release_admission(&self) {
1060        let mut current = self.admissions.load(Ordering::Acquire);
1061        loop {
1062            let next = current.saturating_sub(1);
1063            match self.admissions.compare_exchange_weak(
1064                current,
1065                next,
1066                Ordering::AcqRel,
1067                Ordering::Acquire,
1068            ) {
1069                Ok(_) => return,
1070                Err(observed) => current = observed,
1071            }
1072        }
1073    }
1074
1075    /// The operational caps (§5) this runtime enforces.
1076    pub(super) const fn limits(&self) -> &LimitsConfig {
1077        &self.limits
1078    }
1079
1080    /// The connection's single R6 `READY` wake atom.
1081    pub(super) const fn ready_atom(&self) -> Atom {
1082        self.ready_atom
1083    }
1084
1085    /// Builds a [`ReadyWaker`] targeting `pid` on the connection scheduler, if the
1086    /// scheduler is still live. `None` when the scheduler is gone (teardown) or in
1087    /// scheduler-free unit tests — a notifier with no waker simply never wakes,
1088    /// which under the busy loop is redundant anyway (the every-slice pump still
1089    /// services the source). This is the seam every wake source installs its
1090    /// notifier through (R3/R1(vi)).
1091    pub(super) fn ready_waker(&self, pid: u64) -> Option<super::wake::ReadyWaker> {
1092        let scheduler = self.scheduler.upgrade()?;
1093        Some(super::wake::ReadyWaker::new(
1094            &scheduler,
1095            pid,
1096            self.ready_atom,
1097        ))
1098    }
1099
1100    /// R7: records one serviced slice for `pid`. Bumped at the head of every
1101    /// slice; the park-flip's quiescence assertion reads [`Self::slice_count`].
1102    #[cfg(test)]
1103    pub(super) fn record_slice(&self, pid: u64) {
1104        if let Ok(mut counts) = self.slice_counts.lock() {
1105            *counts.entry(pid).or_insert(0) += 1;
1106        }
1107    }
1108
1109    /// R7: slices serviced by connection `pid` since spawn (test instrument).
1110    #[cfg(test)]
1111    pub(super) fn slice_count(&self, pid: u64) -> u64 {
1112        self.slice_counts
1113            .lock()
1114            .map_or(0, |counts| counts.get(&pid).copied().unwrap_or(0))
1115    }
1116
1117    #[cfg(test)]
1118    fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
1119        let armed = Arc::new(Barrier::new(2));
1120        let release = Arc::new(Barrier::new(2));
1121        if let Ok(mut slot) = self.pre_wait_barrier.lock() {
1122            *slot = Some(PreWaitBarrier {
1123                armed: Arc::clone(&armed),
1124                release: Arc::clone(&release),
1125            });
1126        }
1127        (armed, release)
1128    }
1129
1130    /// Runs a one-use deterministic test gate after arm. Returns whether the gate
1131    /// was installed so only that staged probe contributes to observability.
1132    #[cfg(test)]
1133    pub(super) fn run_pre_wait_barrier(&self) -> bool {
1134        let barrier = self
1135            .pre_wait_barrier
1136            .lock()
1137            .ok()
1138            .and_then(|mut slot| slot.take());
1139        let Some(barrier) = barrier else {
1140            return false;
1141        };
1142        barrier.armed.wait();
1143        barrier.release.wait();
1144        true
1145    }
1146
1147    /// Installs a one-use barrier in `enqueue_control`'s insert->wake window
1148    /// (S8 staging: lets a test act as the control-drain consumer between the
1149    /// queue insertion and the wake attempt) and returns its test endpoints.
1150    #[cfg(test)]
1151    pub(super) fn install_pre_wake_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
1152        let armed = Arc::new(Barrier::new(2));
1153        let release = Arc::new(Barrier::new(2));
1154        if let Ok(mut slot) = self.pre_wake_barrier.lock() {
1155            *slot = Some(PreWaitBarrier {
1156                armed: Arc::clone(&armed),
1157                release: Arc::clone(&release),
1158            });
1159        }
1160        (armed, release)
1161    }
1162
1163    /// Runs the one-use insert->wake test gate, if installed.
1164    #[cfg(test)]
1165    pub(super) fn run_pre_wake_barrier(&self) {
1166        let barrier = self
1167            .pre_wake_barrier
1168            .lock()
1169            .ok()
1170            .and_then(|mut slot| slot.take());
1171        if let Some(barrier) = barrier {
1172            barrier.armed.wait();
1173            barrier.release.wait();
1174        }
1175    }
1176
1177    #[cfg(test)]
1178    pub(super) fn record_pre_wait_probe_hit(&self) {
1179        self.pre_wait_probe_hits.fetch_add(1, Ordering::AcqRel);
1180    }
1181
1182    #[cfg(test)]
1183    fn pre_wait_probe_hits(&self) -> u64 {
1184        self.pre_wait_probe_hits.load(Ordering::Acquire)
1185    }
1186
1187    #[cfg(test)]
1188    fn observe_process_stream_drop(&self, fd: RawFd) -> Receiver<()> {
1189        let (sender, receiver) = channel();
1190        if let Ok(mut observers) = self.process_stream_drop_observers.lock() {
1191            observers.insert(fd, sender);
1192        }
1193        receiver
1194    }
1195
1196    /// Publishes the process-owned stream's real drop boundary to a waiting test.
1197    #[cfg(test)]
1198    pub(super) fn record_process_stream_drop(&self, fd: RawFd) {
1199        let observer = self
1200            .process_stream_drop_observers
1201            .lock()
1202            .ok()
1203            .and_then(|mut observers| observers.remove(&fd));
1204        if let Some(observer) = observer {
1205            let _ = observer.send(());
1206        }
1207    }
1208
1209    /// Builds a runtime wrapping `services` for unit tests that exercise
1210    /// `apply_frame` without a live scheduler. Uses a fresh interned control atom
1211    /// and no notifier.
1212    #[cfg(test)]
1213    pub(super) fn for_tests(services: Arc<dyn ConnectionServices>) -> Self {
1214        let atoms = AtomTable::with_common_atoms();
1215        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1216        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1217        Self::new(
1218            services,
1219            control_atom,
1220            ready_atom,
1221            Weak::new(),
1222            None,
1223            None,
1224            LimitsConfig::default(),
1225        )
1226    }
1227
1228    /// Builds a runtime wrapping `services` with explicit `limits` for unit tests
1229    /// that exercise the §5 admission caps without a live scheduler.
1230    #[cfg(test)]
1231    pub(super) fn for_tests_with_limits(
1232        services: Arc<dyn ConnectionServices>,
1233        limits: LimitsConfig,
1234    ) -> Self {
1235        let atoms = AtomTable::with_common_atoms();
1236        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1237        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1238        Self::new(
1239            services,
1240            control_atom,
1241            ready_atom,
1242            Weak::new(),
1243            None,
1244            None,
1245            limits,
1246        )
1247    }
1248
1249    /// Builds a runtime wrapping `services` with a configured auth `token` for unit
1250    /// tests that exercise the `Connect` handshake enforcement without a live
1251    /// scheduler. Uses a fresh interned control atom and no notifier.
1252    #[cfg(test)]
1253    pub(super) fn for_tests_with_auth_token(
1254        services: Arc<dyn ConnectionServices>,
1255        token: Vec<u8>,
1256    ) -> Self {
1257        let atoms = AtomTable::with_common_atoms();
1258        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1259        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1260        Self::new(
1261            services,
1262            control_atom,
1263            ready_atom,
1264            Weak::new(),
1265            None,
1266            Some(token),
1267            LimitsConfig::default(),
1268        )
1269    }
1270
1271    /// Builds a runtime wrapping `services` with a `notifier` for unit tests that
1272    /// exercise `apply_frame` and the close path without a live scheduler.
1273    #[cfg(test)]
1274    pub(super) fn for_tests_with_notifier(
1275        services: Arc<dyn ConnectionServices>,
1276        notifier: Arc<dyn ConnectionNotifier>,
1277    ) -> Self {
1278        let atoms = AtomTable::with_common_atoms();
1279        let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1280        let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1281        Self::new(
1282            services,
1283            control_atom,
1284            ready_atom,
1285            Weak::new(),
1286            Some(notifier),
1287            None,
1288            LimitsConfig::default(),
1289        )
1290    }
1291
1292    pub(super) fn services(&self) -> &dyn ConnectionServices {
1293        self.services.as_ref()
1294    }
1295
1296    /// Returns the configured connection auth token as opaque bytes, or `None` when
1297    /// no `[auth]` section was configured (open access).
1298    pub(super) fn auth_token(&self) -> Option<&[u8]> {
1299        self.auth_token.as_deref()
1300    }
1301
1302    /// Returns the configured connection-keyed notifier, if any.
1303    pub(super) fn notifier(&self) -> Option<&Arc<dyn ConnectionNotifier>> {
1304        self.notifier.as_ref()
1305    }
1306
1307    /// Offers a channel publish to the notifier's observability-drain tap, returning
1308    /// `true` when the application consumed it (so the connection process skips the
1309    /// normal fan-out). `false` when no notifier is installed (liminal standalone) or
1310    /// the notifier did not recognise the channel, so the caller can invoke it
1311    /// unconditionally and fall through to the normal publish path.
1312    pub(super) fn notifier_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
1313        self.notifier
1314            .as_ref()
1315            .is_some_and(|notifier| notifier.on_channel_publish(pid, channel, payload))
1316    }
1317
1318    /// Stores `registration` on the connection record for `pid`, so the close
1319    /// path can later fire `on_worker_unregistered` for exactly the connections
1320    /// that registered. A missing record (the connection already closed) is a
1321    /// no-op.
1322    ///
1323    /// # Errors
1324    /// Returns [`ServerError`] when the connection registry mutex is poisoned.
1325    pub(super) fn set_registration(
1326        &self,
1327        pid: u64,
1328        registration: WorkerRegistration,
1329    ) -> Result<(), ServerError> {
1330        if let Some(record) = lock(&self.records, "connection registry")?.get_mut(&pid) {
1331            record.registration = Some(registration);
1332        }
1333        Ok(())
1334    }
1335
1336    /// Allocates the next monotonic push correlation id.
1337    fn next_push_correlation_id(&self) -> u64 {
1338        self.next_push_id.fetch_add(1, Ordering::Relaxed)
1339    }
1340
1341    /// Registers a one-shot reply slot for `correlation_id`, owned by connection
1342    /// `pid`, and returns its receiver. `deadline` is the slot's optional absolute
1343    /// reply expiry (`None` = the default no-deadline shape). The connection
1344    /// process resolves the slot via [`resolve_push`]; the close path drops the
1345    /// connection's outstanding slots via [`cancel_pushes_for_connection`]; an
1346    /// explicit deadline resolves it via [`expire_push_if_due`].
1347    ///
1348    /// # Errors
1349    /// Returns [`ServerError`] when the correlation registry mutex is poisoned.
1350    fn register_push(
1351        &self,
1352        pid: u64,
1353        correlation_id: u64,
1354        deadline: Option<Instant>,
1355    ) -> Result<Receiver<Vec<u8>>, ServerError> {
1356        let (sender, receiver) = channel();
1357        let limit = self.limits.max_pending_pushes_per_connection;
1358        {
1359            let mut slots = lock(&self.push_replies, "push correlation registry")?;
1360            // §5 `max_pending_pushes_per_connection`: refuse a new in-flight push
1361            // once this connection already holds the cap. Counted per owning pid so
1362            // one connection cannot exhaust the shared registry; slots free on
1363            // reply, deadline expiry, or connection close. The count-and-insert
1364            // stays under the one lock so the cap is enforced atomically.
1365            let outstanding = slots.values().filter(|pending| pending.pid == pid).count();
1366            if outstanding >= limit {
1367                return Err(ServerError::ConnectionCapReached {
1368                    operation: "server push".to_owned(),
1369                    cap: "max_pending_pushes_per_connection",
1370                    limit,
1371                });
1372            }
1373            slots.insert(
1374                correlation_id,
1375                PendingPush {
1376                    pid,
1377                    sender,
1378                    deadline,
1379                },
1380            );
1381        }
1382        Ok(receiver)
1383    }
1384
1385    /// Host-side, lazy evaluation of a push's reply deadline, called from an
1386    /// elapsed [`PushReplyAwaiter::receive`] quantum. This NEVER wakes the
1387    /// connection process and runs no timer — it inspects supervisor-owned state
1388    /// under the registry lock only.
1389    ///
1390    /// A slot with an explicit deadline that has passed is removed here (dropping
1391    /// its `Sender` and releasing its §5 `max_pending_pushes_per_connection` cap
1392    /// admission, since the cap is the per-pid slot count) and reported
1393    /// [`PushSlotDisposition::Expired`]. A slot with no deadline, or a deadline
1394    /// still in the future, is left UNTOUCHED and reported
1395    /// [`PushSlotDisposition::Live`] — the elapsed quantum is a benign re-arm. A
1396    /// missing slot is [`PushSlotDisposition::Absent`].
1397    fn expire_push_if_due(&self, correlation_id: u64) -> PushSlotDisposition {
1398        // S4: a poisoned registry must NOT read as slot absence — the slot (and
1399        // its cap admission) may still be in the map. Reclamation recovers the
1400        // guard: removal-only operations are sound on a recovered map (a panic
1401        // in another critical section cannot leave the HashMap itself in a
1402        // partial state; only our bookkeeping invariants could be stale, and
1403        // removal restores them). Admission (`register_push`) stays fail-closed.
1404        let mut slots = recover_lock(&self.push_replies);
1405        let Some(pending) = slots.get(&correlation_id) else {
1406            return PushSlotDisposition::Absent;
1407        };
1408        // Copy the deadline out so the immutable borrow of `slots` ends before the
1409        // conditional `remove` below takes a mutable one.
1410        let deadline = pending.deadline;
1411        match deadline {
1412            Some(at) if Instant::now() >= at => {
1413                slots.remove(&correlation_id);
1414                PushSlotDisposition::Expired
1415            }
1416            _ => PushSlotDisposition::Live,
1417        }
1418    }
1419
1420    /// Drops a registered reply slot without resolving it, used on the
1421    /// push-enqueue failure path (the control could not be delivered to a
1422    /// now-gone process, so the just-reserved slot is unreachable). Dropping the
1423    /// slot's `Sender` wakes a still-waiting awaiter with a disconnected error.
1424    ///
1425    /// Returns whether THIS call removed the slot. Removal under the registry
1426    /// mutex is the atomic resolved-vs-cancelled transition: `false` means
1427    /// another path won — [`resolve_push`](Self::resolve_push) already sent the
1428    /// reply (its send happens under the same lock, so the payload is already in
1429    /// the channel when this returns), or the connection's close path dropped
1430    /// the slot (sender gone, channel disconnected).
1431    pub(super) fn cancel_push(&self, correlation_id: u64) -> bool {
1432        // S4: reclamation recovers a poisoned guard — a rollback that silently
1433        // skipped its removal would strand the slot and its cap admission.
1434        recover_lock(&self.push_replies)
1435            .remove(&correlation_id)
1436            .is_some()
1437    }
1438
1439    /// Drops every reply slot owned by connection `pid`, waking each awaiter with a
1440    /// disconnected error (the dropped `Sender` disconnects the awaiter's
1441    /// `Receiver`). Called from the close path so a connection that exits with
1442    /// in-flight pushes signals worker death immediately instead of leaving each
1443    /// awaiter to block the full push-reply timeout. A slot that [`resolve_push`]
1444    /// already removed is gone, so it is untouched here; an unknown pid is a no-op.
1445    fn cancel_pushes_for_connection(&self, pid: u64) {
1446        // S4: the close sweep is the reclamation of last resort ("connection
1447        // close at the latest") — it must complete on a poisoned map too.
1448        recover_lock(&self.push_replies).retain(|_correlation_id, pending| pending.pid != pid);
1449    }
1450
1451    /// S3 second half (shape (b), check-after-insert): pre-publication
1452    /// confirmation that the connection record for `pid` still exists, run in
1453    /// the INSERT -> CONFIRM -> PUBLISH order (S7 — confirming after the
1454    /// enqueue let a close-swept-then-answered push report `Err` for a Push the
1455    /// client had received). `true` leaves the slot in place and the caller may
1456    /// publish; `false` means a concurrent close already removed the record —
1457    /// this call then removes the caller's own just-inserted slot (rolling back
1458    /// its cap admission) so nothing is stranded, and the caller returns
1459    /// WITHOUT publishing: an `Err` from the push methods guarantees no `Push`
1460    /// control was published.
1461    ///
1462    /// Why exactly one side always observes the slot: `remove` (the single
1463    /// record-removal path) removes the host record BEFORE sweeping the pid's
1464    /// push slots, and this check reads the record AFTER inserting the slot and
1465    /// BEFORE the control is published. Both records accesses are serialized by
1466    /// the `records` mutex, so either (i) this read precedes the record removal
1467    /// — then the slot insert precedes the sweep (insert < read < removal <
1468    /// sweep in the happens-before order) and the SWEEP observes and removes
1469    /// the slot: if the control was published in the meantime the awaiter reads
1470    /// the truthful disconnected outcome and a late client reply is a harmless
1471    /// no-op; or (ii) this read follows the record removal — then THIS call
1472    /// observes the absence, rolls the slot back itself, and nothing was
1473    /// published. When both observe (a sweep and a rollback can both run in
1474    /// case (ii) if the insert also preceded the sweep), removal is idempotent
1475    /// and the cap is derived from map membership, so nothing double-releases.
1476    ///
1477    /// Lock discipline: `records` and `push_replies` are NEVER held together —
1478    /// here (`records` read, released, then `push_replies` on rollback), in
1479    /// `remove` (`records` removal, released, then the sweep), and everywhere
1480    /// else in this file the two mutexes are taken strictly sequentially, so no
1481    /// lock-order inversion is possible. This adds ZERO work to the connection
1482    /// slice path: the re-check runs on the push caller's thread only.
1483    pub(super) fn confirm_push_registration(&self, pid: u64, correlation_id: u64) -> bool {
1484        if self.is_registered(pid) {
1485            return true;
1486        }
1487        self.cancel_push(correlation_id);
1488        false
1489    }
1490
1491    /// Number of reserved push reply slots outstanding. A benign wait-quantum
1492    /// timeout must NOT change this (the slot survives); an explicit-deadline
1493    /// expiry, a consumed reply, and connection close each release exactly one.
1494    #[cfg(test)]
1495    pub(super) fn pending_push_count(&self) -> usize {
1496        recover_lock(&self.push_replies).len()
1497    }
1498
1499    /// Reserved push reply slots owned by connection `pid` — the exact quantity
1500    /// the §5 `max_pending_pushes_per_connection` cap counts (test instrument).
1501    #[cfg(test)]
1502    pub(super) fn pending_push_count_for(&self, pid: u64) -> usize {
1503        recover_lock(&self.push_replies)
1504            .values()
1505            .filter(|pending| pending.pid == pid)
1506            .count()
1507    }
1508
1509    /// Resolves the reply slot for `correlation_id` with the client's reply
1510    /// payload, waking the [`PushReplyAwaiter`]. Called by the connection process
1511    /// when a correlated `PushReply` frame arrives. A missing slot — already
1512    /// resolved, expired at its explicit deadline, dropped by connection close, or
1513    /// an unknown id — is a harmless no-op: a late `PushReply` for a slot that is
1514    /// gone is discarded here, never delivered and never a panic or desync.
1515    pub(super) fn resolve_push(&self, correlation_id: u64, payload: Vec<u8>) {
1516        // S4: delivery-plus-removal recovers a poisoned guard — dropping a real
1517        // reply (and stranding its slot) because an unrelated critical section
1518        // panicked would kill reclamation and exact cap accounting.
1519        let mut slots = recover_lock(&self.push_replies);
1520        if let Some(pending) = slots.remove(&correlation_id) {
1521            // The send stays under the registry lock so removal and delivery are
1522            // one atomic step: a timed-out awaiter that observes the slot gone
1523            // (its `cancel_push` returned false) is then GUARANTEED to find the
1524            // payload already in the channel — without this ordering the awaiter
1525            // could see the removal, find the channel still empty, and report a
1526            // timeout for a reply that was about to land. The send itself never
1527            // blocks (unbounded channel), and a receiver dropped after an
1528            // abandoned wait makes it a benign discard.
1529            pending.sender.send(payload).ok();
1530        }
1531    }
1532
1533    pub(super) const fn control_atom(&self) -> Atom {
1534        self.control_atom
1535    }
1536
1537    /// Sole registration path for a connection: the spawn thread inserts the
1538    /// record synchronously, before `spawn_connection` returns the handle, so
1539    /// `is_tracked`/`active_connection_count` reflect the connection
1540    /// immediately. The connection handler never writes the registry (it only
1541    /// reads via `mark_crashed`/`finish`), so there is a single writer here and
1542    /// no register/ensure-register race.
1543    ///
1544    /// Ordering note: `spawn_native` only enqueues the process, so its first
1545    /// slice may run on another worker thread before this insert lands. If that
1546    /// first slice exits immediately (e.g. a missing-stream crash) its
1547    /// `mark_crashed`/`finish` removes nothing and this insert then leaves a
1548    /// record for an already-dead pid. That orphan is self-healing:
1549    /// `reap_crashed`, driven continuously by the listener loop, removes any
1550    /// record whose pid is absent from the scheduler process table.
1551    fn register_with_fd(
1552        &self,
1553        pid: u64,
1554        peer_addr: Option<SocketAddr>,
1555        fd_guard: TcpStream,
1556    ) -> Result<(), ServerError> {
1557        self.register_record(pid, peer_addr, Some(fd_guard))
1558    }
1559
1560    #[cfg(test)]
1561    fn register(&self, pid: u64, peer_addr: Option<SocketAddr>) -> Result<(), ServerError> {
1562        self.register_record(pid, peer_addr, None)
1563    }
1564
1565    fn register_record(
1566        &self,
1567        pid: u64,
1568        peer_addr: Option<SocketAddr>,
1569        fd_guard: Option<TcpStream>,
1570    ) -> Result<(), ServerError> {
1571        lock(&self.records, "connection registry")?.insert(
1572            pid,
1573            ConnectionRecord {
1574                peer_addr,
1575                registration: None,
1576                readiness: None,
1577                fd_guard,
1578            },
1579        );
1580        // Single-writer insert (see doc above) pairs one gauge increment with the
1581        // decrement in `remove`, keeping `liminal_connections_active` equal to the
1582        // live record count on every teardown route.
1583        crate::metrics::connection_spawned();
1584        Ok(())
1585    }
1586
1587    pub(super) fn mark_crashed(&self, pid: u64, reason: ExitReason, peer_addr: Option<SocketAddr>) {
1588        let removed = self.remove(pid);
1589        if let Some(record) = removed.as_ref() {
1590            self.fire_unregistered(pid, record);
1591        }
1592        let removed_peer_addr = removed
1593            .as_ref()
1594            .and_then(|record| record.peer_addr)
1595            .or(peer_addr);
1596        tracing::warn!(
1597            connection_pid = pid,
1598            peer_addr = ?removed_peer_addr,
1599            reason = ?reason,
1600            "connection process crashed"
1601        );
1602    }
1603
1604    /// Whether the spawn thread has installed the host record. A first native
1605    /// slice can win the enqueue-vs-record race and must remain runnable until it
1606    /// has somewhere host-reachable to publish its readiness token.
1607    pub(super) fn is_registered(&self, pid: u64) -> bool {
1608        self.contains(pid)
1609    }
1610
1611    /// Removes a token minted in-slice when publishing it to the host record fails.
1612    pub(super) fn deregister_unpublished_readiness(&self, token: ReadinessToken) {
1613        if let Some(scheduler) = self.scheduler.upgrade() {
1614            scheduler.readiness_deregister(token);
1615        }
1616    }
1617
1618    /// Cancels deadline timers detached by reply completion or connection close.
1619    pub(super) fn cancel_deadline_timers(&self, timers: Vec<TimerRef>) {
1620        let Some(scheduler) = self.scheduler.upgrade() else {
1621            return;
1622        };
1623        if let Ok(mut wheel) = scheduler.timers().lock() {
1624            for timer in timers {
1625                wheel.cancel(timer);
1626            }
1627        }
1628    }
1629
1630    pub(super) fn finish(&self, pid: u64) {
1631        if let Some(removed) = self.remove(pid) {
1632            self.fire_unregistered(pid, &removed);
1633        }
1634    }
1635
1636    /// Records the one readiness token minted for this connection. A live
1637    /// connection never re-registers: later parked slices rearm this identity.
1638    pub(super) fn set_readiness_token_once(
1639        &self,
1640        pid: u64,
1641        token: ReadinessToken,
1642        fd: RawFd,
1643    ) -> Result<(), ServerError> {
1644        let mut records = lock(&self.records, "connection registry")?;
1645        let record = records
1646            .get_mut(&pid)
1647            .ok_or_else(|| ServerError::ListenerAccept {
1648                message: format!("connection {pid} has no host record for readiness registration"),
1649            })?;
1650        if record.readiness.is_some() {
1651            return Err(ServerError::ListenerAccept {
1652                message: format!("connection {pid} attempted to replace its readiness token"),
1653            });
1654        }
1655        record.readiness = Some(ReadinessRegistration { token, fd });
1656        drop(records);
1657        Ok(())
1658    }
1659
1660    /// Invokes `on_worker_unregistered` for a removed connection record that
1661    /// carried a worker registration. A record with no registration (a plain
1662    /// connection, or a worker connection that never registered) is a no-op, so
1663    /// only connections that actually registered deregister.
1664    fn fire_unregistered(&self, pid: u64, record: &ConnectionRecord) {
1665        if record.registration.is_some() {
1666            if let Some(notifier) = self.notifier.as_ref() {
1667                notifier.on_worker_unregistered(pid);
1668            }
1669        }
1670    }
1671
1672    fn reap_crashed(&self, scheduler: &Scheduler) -> usize {
1673        let pids = match self.records.lock() {
1674            Ok(records) => records.keys().copied().collect::<Vec<_>>(),
1675            Err(error) => {
1676                tracing::warn!(%error, "connection registry unavailable during crash reap");
1677                return 0;
1678            }
1679        };
1680        let mut reaped = 0;
1681        for pid in pids {
1682            if scheduler.process_table().get(pid).is_none() {
1683                let removed = self.remove(pid);
1684                if let Some(record) = removed.as_ref() {
1685                    self.fire_unregistered(pid, record);
1686                }
1687                let peer_addr = removed.and_then(|record| record.peer_addr);
1688                // This process exited without ever reaching `mark_crashed`/`finish`
1689                // (e.g. the beamr scheduler terminated it externally). beamr records
1690                // the real `ExitReason` in its private `exit_tombstones` map, but its
1691                // public `Scheduler` API exposes no non-blocking accessor for it
1692                // (only `run_until_exit`, which blocks). So we cannot recover the true
1693                // reason here; log a truthful, specific message rather than the
1694                // misleading literal "unknown". If beamr later grows a public,
1695                // non-blocking exit-reason query for a dead pid, read it here instead.
1696                tracing::warn!(
1697                    connection_pid = pid,
1698                    ?peer_addr,
1699                    reason = "terminated externally (no exit reason recorded by supervisor)",
1700                    "connection process crashed"
1701                );
1702                reaped += 1;
1703            }
1704        }
1705        reaped
1706    }
1707
1708    fn contains(&self, pid: u64) -> bool {
1709        self.records
1710            .lock()
1711            .is_ok_and(|records| records.contains_key(&pid))
1712    }
1713
1714    #[cfg(test)]
1715    fn readiness_registration_count(&self) -> usize {
1716        self.records.lock().map_or(0, |records| {
1717            records
1718                .values()
1719                .filter(|record| record.readiness.is_some())
1720                .count()
1721        })
1722    }
1723
1724    #[cfg(test)]
1725    fn readiness_fd(&self, pid: u64) -> Option<RawFd> {
1726        self.records
1727            .lock()
1728            .ok()?
1729            .get(&pid)
1730            .and_then(|record| record.readiness.map(|registration| registration.fd))
1731    }
1732
1733    fn active_connections(&self) -> Vec<ActiveConnection> {
1734        self.records.lock().map_or_else(
1735            |_| Vec::new(),
1736            |records| {
1737                records
1738                    .iter()
1739                    .map(|(&pid, record)| ActiveConnection {
1740                        pid,
1741                        peer_addr: record.peer_addr,
1742                    })
1743                    .collect()
1744            },
1745        )
1746    }
1747
1748    fn push_control(&self, pid: u64, control: ConnectionControl) -> Result<(), ServerError> {
1749        lock(&self.controls, "connection control queue")?
1750            .push(QueuedConnectionControl { pid, control });
1751        Ok(())
1752    }
1753
1754    pub(super) fn pop_control(&self, pid: u64) -> Option<ConnectionControl> {
1755        let mut controls = self.controls.lock().ok()?;
1756        let index = controls.iter().position(|queued| queued.pid == pid)?;
1757        Some(controls.remove(index).control)
1758    }
1759
1760    /// Non-consuming final-probe query for controls enqueued after mailbox drain.
1761    pub(super) fn has_control(&self, pid: u64) -> bool {
1762        self.controls
1763            .lock()
1764            .is_ok_and(|controls| controls.iter().any(|queued| queued.pid == pid))
1765    }
1766
1767    /// Pulls a queued-but-unconsumed control back out of the queue. Returns
1768    /// whether THIS call removed it — `false` means the entry already left the
1769    /// queue, and since [`Self::pop_control`] is the only other remover, a
1770    /// consumer drain took it (S8's publication disambiguator). Matching is
1771    /// `pid` + full control equality; a `Push` control embeds its
1772    /// runtime-unique correlation id, so this can never remove a different
1773    /// push's entry and misreport.
1774    fn remove_control(&self, pid: u64, control: &ConnectionControl) -> bool {
1775        let Ok(mut controls) = self.controls.lock() else {
1776            return false;
1777        };
1778        let Some(index) = controls
1779            .iter()
1780            .position(|queued| queued.pid == pid && &queued.control == control)
1781        else {
1782            return false;
1783        };
1784        controls.remove(index);
1785        true
1786    }
1787
1788    fn active_count(&self) -> usize {
1789        self.records.lock().map_or(0, |records| records.len())
1790    }
1791
1792    /// Removes the connection record for `pid` and, in the same close step, drops
1793    /// every push reply slot that connection still owns so each waiting
1794    /// [`PushReplyAwaiter`] wakes immediately with a disconnected error. This runs
1795    /// on every close route — `finish`, `mark_crashed`, and `reap_crashed` all
1796    /// remove through here — and fires regardless of whether the connection ever
1797    /// registered a worker, so a plain push target is covered too.
1798    ///
1799    /// ORDER MATTERS (S3/S7): the record is removed BEFORE the push sweep. A
1800    /// push registering concurrently runs INSERT -> CONFIRM -> PUBLISH
1801    /// (`confirm_push_registration` reads the record after inserting its slot
1802    /// and before publishing its control), so with this ordering exactly one
1803    /// side always observes a racing slot: a confirm that ran before this
1804    /// removal implies the slot was inserted before the sweep below (which then
1805    /// reaps it — a control published after that confirm is answered into a
1806    /// swept slot, read as the truthful disconnected outcome); a confirm after
1807    /// this removal sees the absence, rolls the slot back itself, and never
1808    /// publishes. Sweeping first (the original order) left a window — sweep,
1809    /// then insert+confirm, then record removal — where NEITHER side observed
1810    /// the slot and it leaked past connection close. The two locks are taken
1811    /// strictly sequentially (never nested), so no lock-order inversion.
1812    fn remove(&self, pid: u64) -> Option<ConnectionRecord> {
1813        let mut removed = self
1814            .records
1815            .lock()
1816            .ok()
1817            .and_then(|mut records| records.remove(&pid));
1818        self.cancel_pushes_for_connection(pid);
1819        if let Some(registration) = removed.as_mut().and_then(|record| record.readiness.take()) {
1820            if let Some(scheduler) = self.scheduler.upgrade() {
1821                // This call is ACK'd: it returns only after the poll owner has
1822                // removed the registration. `fd_guard` is still live here.
1823                scheduler.readiness_deregister(registration.token);
1824                tracing::debug!(
1825                    registered_fd = registration.fd,
1826                    "connection readiness deregistration acknowledged"
1827                );
1828            }
1829        }
1830        // Decrement only when a record was actually present so a double-remove
1831        // (e.g. `finish` after `reap_crashed`) cannot drive the gauge negative.
1832        // The §5 admission slot is released on the same guard: the reservation
1833        // acquired in `spawn_connection` converted into this record at
1834        // `register`, so its removal is exactly one release per reservation.
1835        if removed.is_some() {
1836            crate::metrics::connection_closed();
1837            self.release_admission();
1838        }
1839        if let Some(record) = removed.as_mut() {
1840            // Explicit after-deregister drop documents and enforces the fd wall.
1841            drop(record.fd_guard.take());
1842        }
1843        removed
1844    }
1845}
1846
1847/// One in-flight server-push reply slot, associating the awaiter's reply `sender`
1848/// with the `pid` of the connection that owns the push. The pid lets the close
1849/// path drop exactly that connection's slots; the correlation id (the map key)
1850/// still drives [`ConnectionRuntime::resolve_push`] and
1851/// [`ConnectionRuntime::cancel_push`].
1852#[derive(Debug)]
1853struct PendingPush {
1854    pid: u64,
1855    sender: Sender<Vec<u8>>,
1856    /// Absolute reply deadline for this push, when one was requested via
1857    /// [`ConnectionSupervisor::push_to_connection_with_deadline`]. `None` is the
1858    /// default 0.2.3 shape: the slot has no per-slot deadline and is reclaimed
1859    /// only by reply-consumed or connection-close. `Some` is evaluated host-side
1860    /// and lazily in [`ConnectionRuntime::expire_push_if_due`].
1861    deadline: Option<Instant>,
1862}
1863
1864/// Host-side disposition of a reply slot at an elapsed `receive` quantum.
1865enum PushSlotDisposition {
1866    /// The slot carried an explicit deadline that has passed; this call removed
1867    /// it (releasing its §5 cap admission).
1868    Expired,
1869    /// The slot is present with no deadline, or a deadline still in the future:
1870    /// the elapsed quantum is a benign re-arm and the slot is untouched.
1871    Live,
1872    /// No slot for this correlation id — a concurrent resolve or connection close
1873    /// already removed it.
1874    Absent,
1875}
1876
1877#[derive(Debug)]
1878struct ConnectionRecord {
1879    peer_addr: Option<SocketAddr>,
1880    /// Worker registration declared on this connection, set by `set_registration`
1881    /// when a `WorkerRegister` frame is accepted. `Some` marks a connection whose
1882    /// close must fire `on_worker_unregistered`.
1883    registration: Option<WorkerRegistration>,
1884    /// Host-reachable identity for ACK'd deregistration after external death.
1885    readiness: Option<ReadinessRegistration>,
1886    /// Keeps the fd alive until deregistration has been acknowledged, preventing
1887    /// stale registration delivery to a subsequently reused descriptor number.
1888    fd_guard: Option<TcpStream>,
1889}
1890
1891#[derive(Debug, Clone, Copy)]
1892struct ReadinessRegistration {
1893    token: ReadinessToken,
1894    fd: RawFd,
1895}
1896
1897#[derive(Debug, Clone, PartialEq, Eq)]
1898struct QueuedConnectionControl {
1899    pid: u64,
1900    control: ConnectionControl,
1901}
1902
1903fn lock<'a, T>(mutex: &'a Mutex<T>, context: &str) -> Result<MutexGuard<'a, T>, ServerError> {
1904    mutex.lock().map_err(|error| ServerError::ListenerAccept {
1905        message: format!("{context} unavailable: {error}"),
1906    })
1907}
1908
1909/// Locks `mutex`, RECOVERING a poisoned guard instead of failing (S4). For
1910/// lifecycle-cleanup paths only (reply delivery, expiry, cancellation, the
1911/// close sweep): removal-style operations are sound on a recovered map, and a
1912/// cleanup that silently skipped its removal would strand slots and their §5
1913/// cap admissions forever. Admission paths keep the fail-closed [`lock`].
1914fn recover_lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1915    mutex
1916        .lock()
1917        .unwrap_or_else(std::sync::PoisonError::into_inner)
1918}