Skip to main content

liminal_server/server/connection/
supervisor.rs

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