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