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