liminal_server/server/connection/supervisor.rs
1use std::collections::HashMap;
2#[cfg(test)]
3use std::collections::VecDeque;
4use std::collections::hash_map::Entry;
5use std::net::{SocketAddr, TcpStream};
6use std::os::fd::RawFd;
7#[cfg(test)]
8use std::sync::Barrier;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
11use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, Weak};
12use std::thread;
13use std::time::{Duration, Instant};
14
15use beamr::atom::{Atom, AtomTable};
16use beamr::module::ModuleRegistry;
17use beamr::native::native_process::NativeHandlerFactory;
18use beamr::process::ExitReason;
19use beamr::scheduler::{
20 ExitEvent, ExitEventSubscription, ReadinessToken, Scheduler, SchedulerConfig, SchedulerServices,
21};
22use beamr::timer::TimerRef;
23
24use liminal::protocol::WorkerRegistration;
25use liminal_protocol::wire::ConnectionIncarnation;
26
27use super::incarnation::ConnectionIncarnationAuthority;
28use super::loopback::{LoopbackConnectionProcess, LoopbackServerEnd};
29use super::notifier::ConnectionNotifier;
30use super::process::ConnectionProcess;
31use super::services::{
32 ConnectionServices, LiminalConnectionServices, ProductionSubsystems, SubsystemFactory,
33 build_connection_services_via,
34};
35use crate::ServerError;
36use crate::config::types::{LimitsConfig, ServerConfig};
37use crate::server::mount::MountKind;
38use crate::server::participant::{
39 ConnectionFateClass, InstalledParticipantService, ParticipantSemanticHandler,
40 ParticipantServiceFatal,
41};
42use crate::server::shutdown::ShutdownHandle;
43
44const CONNECTION_SCHEDULER_THREADS: usize = 4;
45const CONNECTION_SHUTDOWN_CONTROL_ATOM: &str = "liminal_server_connection_shutdown_control";
46/// R6 (§1.2(4)): the single `READY` wake vocabulary for a connection. One atom;
47/// any marker (or N coalesced) triggers one full slice servicing all sources.
48const CONNECTION_READY_ATOM: &str = "liminal_server_connection_ready";
49
50#[cfg(test)]
51#[path = "supervisor_fate_tests.rs"]
52mod fate_tests;
53#[cfg(test)]
54#[path = "supervisor_tests.rs"]
55mod tests;
56
57/// Supervisor that owns the beamr scheduler for per-connection processes.
58#[derive(Clone, Debug)]
59pub struct ConnectionSupervisor {
60 inner: Arc<SupervisorInner>,
61}
62
63impl ConnectionSupervisor {
64 /// Creates a connection supervisor backed by the services the config's
65 /// `[services]` profile selects: the full liminal channel/conversation stack
66 /// (the default) or the capability-scoped worker front door. Profile
67 /// enforcement is [`build_connection_services`](super::services::build_connection_services)'s,
68 /// so this constructor can never build full services for a worker-front-door
69 /// config.
70 ///
71 /// # Errors
72 /// Returns [`ServerError`] when service construction or scheduler startup fails.
73 pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
74 Self::from_config_via(config, &ProductionSubsystems)
75 }
76
77 /// [`Self::from_config`] with the §9 D2 subsystem factory injected.
78 ///
79 /// The factory is the only route to every scheduler-owning subsystem the
80 /// services construction builds, so a recording factory observes exactly what
81 /// was constructed; the connection scheduler itself (built below for BOTH
82 /// profiles) is the census baseline, not a census entry.
83 fn from_config_via(
84 config: &ServerConfig,
85 subsystems: &dyn SubsystemFactory,
86 ) -> Result<Self, ServerError> {
87 let services = build_connection_services_via(config, subsystems)?;
88 // The configured token (if any) is carried opaquely as bytes for a
89 // constant-time comparison against the handshake's `auth_token`. Absent
90 // `[auth]` leaves it `None`, so the connection stays open-access.
91 let auth_token = config
92 .auth
93 .as_ref()
94 .map(|auth| auth.token.clone().into_bytes());
95 SupervisorInner::new(services, None, auth_token, config.limits, None).map(|inner| Self {
96 inner: Arc::new(inner),
97 })
98 }
99
100 /// Creates a connection supervisor with no configured channels.
101 ///
102 /// # Errors
103 /// Returns [`ServerError`] when scheduler startup fails.
104 pub fn new() -> Result<Self, ServerError> {
105 Self::with_services(Arc::new(LiminalConnectionServices::empty()?))
106 }
107
108 /// Creates a connection supervisor using an explicit service adapter.
109 ///
110 /// # Errors
111 /// Returns [`ServerError`] when scheduler startup fails.
112 pub fn with_services(services: Arc<dyn ConnectionServices>) -> Result<Self, ServerError> {
113 SupervisorInner::new(services, None, None, LimitsConfig::default(), None).map(|inner| {
114 Self {
115 inner: Arc::new(inner),
116 }
117 })
118 }
119
120 /// Creates a connection supervisor with an explicit service adapter and the
121 /// configured connection auth token.
122 ///
123 /// This is the production constructor for callers that build services
124 /// themselves (the runtime needs the shared channel cluster before the
125 /// supervisor takes ownership) and therefore cannot use
126 /// [`Self::from_config`]: without it the configured `[auth]` token would be
127 /// silently dropped and the server would run open-access.
128 ///
129 /// # Errors
130 /// Returns [`ServerError`] when scheduler startup fails.
131 pub fn with_services_and_auth(
132 services: Arc<dyn ConnectionServices>,
133 auth_token: Option<Vec<u8>>,
134 ) -> Result<Self, ServerError> {
135 Self::with_services_auth_and_limits(services, auth_token, LimitsConfig::default())
136 }
137
138 /// Creates a connection supervisor with explicit services, authentication,
139 /// and operational limits.
140 ///
141 /// Production runtime construction uses this form so the durable
142 /// incarnation stream's complete-reference bound is the same signed
143 /// `max_connections` bound enforced by connection admission.
144 ///
145 /// # Errors
146 /// Returns [`ServerError`] when incarnation startup or scheduler startup fails.
147 pub fn with_services_auth_and_limits(
148 services: Arc<dyn ConnectionServices>,
149 auth_token: Option<Vec<u8>>,
150 limits: LimitsConfig,
151 ) -> Result<Self, ServerError> {
152 Self::with_services_auth_limits_and_fatal_shutdown(services, auth_token, limits, None)
153 }
154
155 /// Production composition with the process-wide shutdown activation that a
156 /// post-Open participant fatal must join.
157 pub(crate) fn with_fatal_shutdown(
158 services: Arc<dyn ConnectionServices>,
159 auth_token: Option<Vec<u8>>,
160 limits: LimitsConfig,
161 fatal_shutdown: ShutdownHandle,
162 ) -> Result<Self, ServerError> {
163 Self::with_services_auth_limits_and_fatal_shutdown(
164 services,
165 auth_token,
166 limits,
167 Some(fatal_shutdown),
168 )
169 }
170
171 fn with_services_auth_limits_and_fatal_shutdown(
172 services: Arc<dyn ConnectionServices>,
173 auth_token: Option<Vec<u8>>,
174 limits: LimitsConfig,
175 fatal_shutdown: Option<ShutdownHandle>,
176 ) -> Result<Self, ServerError> {
177 SupervisorInner::new(services, None, auth_token, limits, fatal_shutdown).map(|inner| Self {
178 inner: Arc::new(inner),
179 })
180 }
181
182 /// Creates a connection supervisor with an explicit service adapter and a
183 /// connection-keyed worker-registration notifier.
184 ///
185 /// The `notifier` is invoked when a worker registers on a connection and when
186 /// such a connection closes. Supervisors built via [`Self::with_services`],
187 /// [`Self::from_config`], or [`Self::new`] carry no notifier, so liminal still
188 /// runs standalone; a `WorkerRegister` frame is then accepted without any
189 /// application callback.
190 ///
191 /// # Errors
192 /// Returns [`ServerError`] when scheduler startup fails.
193 pub fn with_services_and_notifier(
194 services: Arc<dyn ConnectionServices>,
195 notifier: Arc<dyn ConnectionNotifier>,
196 ) -> Result<Self, ServerError> {
197 SupervisorInner::new(
198 services,
199 Some(notifier),
200 None,
201 LimitsConfig::default(),
202 None,
203 )
204 .map(|inner| Self {
205 inner: Arc::new(inner),
206 })
207 }
208
209 /// Spawns one supervised beamr process that owns `stream`.
210 ///
211 /// # Errors
212 /// Returns [`ServerError`] when stream configuration or beamr spawn fails.
213 pub fn spawn_connection(&self, stream: TcpStream) -> Result<ConnectionHandle, ServerError> {
214 self.inner.spawn_connection(stream)
215 }
216
217 /// Returns the underlying beamr scheduler.
218 #[must_use]
219 pub fn scheduler(&self) -> Arc<Scheduler> {
220 Arc::clone(&self.inner.scheduler)
221 }
222
223 /// Reaps connection processes that have exited outside the normal handler path.
224 #[must_use]
225 pub fn reap_crashed_connections(&self) -> usize {
226 self.inner.runtime.reap_crashed(&self.inner.scheduler)
227 }
228
229 /// Returns true when `pid` is still tracked by the supervisor.
230 #[must_use]
231 pub fn is_tracked(&self, pid: u64) -> bool {
232 self.inner.runtime.contains(pid)
233 }
234
235 /// Returns the number of tracked live connections.
236 #[must_use]
237 pub fn active_connection_count(&self) -> usize {
238 self.inner.runtime.active_count()
239 }
240
241 /// Parks until every tracked connection has been removed or `deadline`
242 /// elapses, returning `true` when the drain completed and `false` when the
243 /// single admitted deadline won.
244 ///
245 /// The TOLD drain-completion replacement (W4 leg 3, §4.3): the waiter is woken
246 /// by the one `remove()` funnel every connection exit reaches — never by a
247 /// reap/count timer. Both the graceful drain and the force-close settle call
248 /// this same waiter, each with its own one-shot deadline; there is no second
249 /// settle poll loop.
250 #[must_use]
251 pub(crate) fn wait_for_connections_drained(&self, deadline: Instant) -> bool {
252 self.inner
253 .runtime
254 .wait_for_active_connections_drained(deadline)
255 }
256
257 /// FIX A-ii shutdown flush barrier: parks until every active connection has
258 /// fanned out its accepted publishes to its socket, or `deadline` elapses.
259 /// Called in `run_shutdown_sequence` BEFORE the shutdown Disconnect broadcast
260 /// so an accepted-but-unfanned-out publish can no longer be overtaken by it.
261 #[must_use]
262 pub(crate) fn wait_for_delivery_quiesced(&self, deadline: Instant) -> bool {
263 self.inner.runtime.wait_for_delivery_quiesced(deadline)
264 }
265
266 /// Returns the first latched post-Open participant fatal, if any.
267 ///
268 /// The production runtime reads this after its existing shutdown handle wakes,
269 /// then returns the typed fatal after the ordinary drain and durable flush.
270 pub(crate) fn participant_service_fatal(
271 &self,
272 ) -> Result<Option<ParticipantServiceFatal>, ServerError> {
273 self.inner.runtime.participant_service_fatal()
274 }
275
276 /// Returns the beamr process ids of the currently tracked live connections.
277 ///
278 /// Useful for addressing a specific connection — e.g. as the `pid` argument to
279 /// [`push_to_connection`](Self::push_to_connection) when the caller knows there
280 /// is a single connected client.
281 #[must_use]
282 pub fn active_connection_pids(&self) -> Vec<u64> {
283 self.inner
284 .runtime
285 .active_connections()
286 .into_iter()
287 .map(|connection| connection.pid)
288 .collect()
289 }
290
291 /// Broadcasts a best-effort shutdown notification to active connections.
292 ///
293 /// Connections with no active subscriptions ignore the notification. Failures
294 /// to enqueue the control message are logged and skipped; they are not retried.
295 pub fn notify_shutdown_subscribers(&self) {
296 self.inner
297 .broadcast_control(&ConnectionControl::NotifyShutdown);
298 }
299
300 /// Sends a force-close control message to every tracked connection process.
301 ///
302 /// Each live process attempts one shutdown notification before closing its
303 /// stream and exiting normally. Enqueue failures are logged and skipped.
304 pub fn force_close_active_connections(&self) {
305 for connection in self.inner.runtime.active_connections() {
306 tracing::warn!(
307 connection_pid = connection.pid,
308 peer_addr = ?connection.peer_addr,
309 "forcefully closing connection after drain timeout"
310 );
311 if !self
312 .inner
313 .enqueue_control(connection.pid, ConnectionControl::ForceClose)
314 {
315 tracing::warn!(
316 connection_pid = connection.pid,
317 peer_addr = ?connection.peer_addr,
318 "failed to request forceful connection close; process is not live"
319 );
320 }
321 }
322 }
323
324 /// Pushes an opaque payload to a specific connected client over that client's
325 /// existing connection and returns an awaiter for the client's correlated reply.
326 ///
327 /// This is the server-initiated leg (server-to-client), the inverse of every
328 /// other request frame. It allocates a correlation id, registers a one-shot
329 /// reply slot keyed by that id, and enqueues a [`ConnectionControl::Push`] for
330 /// the connection process owning `pid`; that process writes a [`Frame::Push`]
331 /// out on its socket. When the client answers with a `PushReply` carrying the
332 /// same correlation id, the connection process resolves the awaiter's slot. The
333 /// returned [`PushReplyAwaiter`] blocks (bounded) for that reply.
334 ///
335 /// The reply's lifetime belongs to the push, not to any one
336 /// [`PushReplyAwaiter::receive`] call: this no-deadline push reserves a slot
337 /// that is reclaimed only by (a) the reply being consumed or (b) the
338 /// connection closing. An elapsed `receive` poll is a benign re-arm, never a
339 /// failure and never a cancellation. The §5
340 /// `max_pending_pushes_per_connection` cap bounds abandonment; use
341 /// [`push_to_connection_with_deadline`](Self::push_to_connection_with_deadline)
342 /// when the reply must have an explicit expiry.
343 ///
344 /// # Errors
345 /// Returns [`ServerError`] when the correlation id cannot be allocated, the
346 /// reply slot cannot be registered, or the control message cannot be enqueued
347 /// for the (possibly already-gone or concurrently-closing) connection
348 /// process. PUBLICATION INVARIANT: an `Err` guarantees no `Push` control was
349 /// published — the client never sees a `Push` frame for a failed call.
350 /// Conversely `Ok` promises ADMISSION, not delivery: the awaiter's outcome
351 /// is the delivery truth (a push admitted just as its connection closes
352 /// resolves to the truthful disconnected outcome, never to a lost reply).
353 pub fn push_to_connection(
354 &self,
355 pid: u64,
356 payload: Vec<u8>,
357 ) -> Result<PushReplyAwaiter, ServerError> {
358 self.push_with_deadline(pid, payload, None)
359 }
360
361 /// Like [`push_to_connection`](Self::push_to_connection) but attaches an
362 /// explicit reply deadline to the reserved slot: `deadline` is a DURATION
363 /// FROM NOW bounding the reply's lifetime — a property of THIS push rather
364 /// than of any [`PushReplyAwaiter::receive`] wait quantum.
365 ///
366 /// Deadline expiry is evaluated HOST-SIDE and LAZILY — at the next `receive`
367 /// touch, and at connection close at the latest. It never wakes the connection
368 /// process, adds no timer thread, and runs no periodic sweeper: a push that is
369 /// abandoned and never polled resolves at the next host-side touch (connection
370 /// close). At expiry the slot resolves to [`ServerError::PushReplyExpired`],
371 /// is removed, and its §5 `max_pending_pushes_per_connection` cap admission is
372 /// released. An elapsed `receive` poll BEFORE the deadline is still a benign
373 /// re-arm. A `receive` call in flight when the deadline falls due returns
374 /// the terminal expiry PROMPTLY — it waits the earlier of its quantum and
375 /// the deadline, so a large quantum can never extend the reply's lifetime
376 /// and the terminal outcome is quantum-independent.
377 ///
378 /// The deadline is evaluated at OBSERVATION POINTS, not enforced against the
379 /// wall clock: a reply that arrives before expiry is observed is delivered
380 /// normally, even if it arrives after the deadline instant. The deadline
381 /// bounds waiting and slot occupancy; it is not a delivery-freshness
382 /// guarantee. (This is deliberate — a reply is checked for at every
383 /// observation point before the deadline is, so an answer in hand always
384 /// beats an expiry.)
385 ///
386 /// # Errors
387 /// Returns [`ServerError`] when `deadline` is not representable on the
388 /// monotonic clock (an extreme duration is refused, never a panic), the
389 /// correlation id cannot be allocated, the reply slot cannot be registered,
390 /// or the control message cannot be enqueued for the (possibly already-gone
391 /// or concurrently-closing) connection process. PUBLICATION INVARIANT: an
392 /// `Err` guarantees no `Push` control was published — the client never sees
393 /// a `Push` frame for a failed call. Conversely `Ok` promises ADMISSION,
394 /// not delivery: the awaiter's outcome is the delivery truth.
395 pub fn push_to_connection_with_deadline(
396 &self,
397 pid: u64,
398 payload: Vec<u8>,
399 deadline: Duration,
400 ) -> Result<PushReplyAwaiter, ServerError> {
401 self.push_with_deadline(pid, payload, Some(deadline))
402 }
403
404 /// Shared body for the no-deadline and explicit-deadline push paths. With
405 /// `deadline == None` this is byte-for-byte the historical
406 /// `push_to_connection` behaviour (no per-slot deadline); with `Some`, the
407 /// slot carries an absolute expiry evaluated lazily at `receive`.
408 fn push_with_deadline(
409 &self,
410 pid: u64,
411 payload: Vec<u8>,
412 deadline: Option<Duration>,
413 ) -> Result<PushReplyAwaiter, ServerError> {
414 // S5: an extreme `Duration` must surface as this fallible API's typed
415 // error, not an `Instant` addition panic. Checked BEFORE any slot is
416 // registered so a refused deadline leaves nothing to roll back.
417 let deadline_at = match deadline {
418 None => None,
419 Some(window) => {
420 Some(
421 Instant::now()
422 .checked_add(window)
423 .ok_or_else(|| ServerError::ListenerAccept {
424 message: format!(
425 "cannot push to connection process {pid}: reply deadline of {window:?} overflows the monotonic clock"
426 ),
427 })?,
428 )
429 }
430 };
431 let correlation_id = self.inner.runtime.next_push_correlation_id();
432 let receiver = self
433 .inner
434 .runtime
435 .register_push(pid, correlation_id, deadline_at)?;
436 // S3+S7 close-vs-register wall, ordered INSERT -> CONFIRM -> PUBLISH.
437 // The confirmation runs BEFORE the control is enqueued, which yields the
438 // PUBLICATION INVARIANT: an `Err` from this method guarantees no `Push`
439 // control was published — the client never sees a Push for a failed
440 // call. (Confirming after the enqueue was S7's non-linearizable race: a
441 // close could sweep, the published Push could already be answered and
442 // resolved, and the failed confirmation then returned `Err` for a push
443 // the client had received.) A close landing AFTER a successful confirm
444 // linearizes after push admission: the enqueue either fails (process
445 // gone — rollback below, `Err` truthful, nothing delivered) or succeeds
446 // with the slot already swept, and the awaiter then reads the truthful
447 // DISCONNECTED while a late client reply is the pinned harmless no-op.
448 // The exactly-one-side-observes argument lives at
449 // `confirm_push_registration`.
450 if !self
451 .inner
452 .runtime
453 .confirm_push_registration(pid, correlation_id)
454 {
455 return Err(ServerError::ListenerAccept {
456 message: format!(
457 "cannot push to connection process {pid}: the connection closed during push registration"
458 ),
459 });
460 }
461 let control = ConnectionControl::Push {
462 correlation_id,
463 payload,
464 };
465 if self.inner.enqueue_control(pid, control) {
466 Ok(PushReplyAwaiter {
467 correlation_id,
468 receiver,
469 deadline: deadline_at,
470 runtime: Arc::downgrade(&self.inner.runtime),
471 })
472 } else {
473 // The process is gone AND the control provably never reached a
474 // consumer: `enqueue_control` returns false only when its failed-wake
475 // rollback REMOVED the queued control (S8 — an entry a drain already
476 // consumed counts as published and returns true, with the slot
477 // lifecycle carrying the delivery truth). Dropping the now-unreachable
478 // reply slot here therefore keeps the publication invariant exact on
479 // every `Err` path.
480 self.inner.runtime.cancel_push(correlation_id);
481 Err(ServerError::ListenerAccept {
482 message: format!("cannot push to connection process {pid}: process is not live"),
483 })
484 }
485 }
486
487 /// Flushes durable channel state through the configured liminal services.
488 ///
489 /// # Errors
490 /// Returns [`ServerError::ShutdownFlush`] when the underlying service flush fails.
491 pub fn flush_durable_state(&self) -> Result<(), ServerError> {
492 self.inner.runtime.services().flush_durable_state()
493 }
494
495 /// LP-WS-TRANSPORT R1.3 sibling-transport spawn seam (ADDITIVE ONLY).
496 ///
497 /// Admits, allocates a durable connection incarnation for, spawns, and
498 /// registers a connection process whose handler is built by `build_factory`
499 /// over this supervisor's shared [`ConnectionRuntime`]. The WebSocket
500 /// sibling acceptor uses this so its connections share the ONE §5
501 /// `max_connections` admission bound, the one incarnation authority, the
502 /// one registry (controls, pushes, crash reap, drain, forced close), and the
503 /// one `apply_frame` seam with TCP connections. The TCP accept path above
504 /// (`spawn_connection`) is byte-for-byte untouched and never calls this.
505 ///
506 /// `fd_guard` is a host-held duplicate of the connection's underlying
507 /// socket, exactly like the TCP path's: it keeps the fd alive until the
508 /// single record-removal path has synchronously deregistered readiness. It
509 /// is `None` for a transport that owns no descriptor.
510 ///
511 /// `mount` is the admitting door's own name for itself (design §10), which
512 /// is why this seam takes it rather than deriving it: the caller IS the
513 /// door, and no other party — least of all the client — has any input.
514 ///
515 /// This method exists because Rust module privacy makes the runtime,
516 /// admission counter, incarnation authority, and registry unreachable from
517 /// the sibling `websocket` module family; it is the narrow additive seam
518 /// that shares them without generalizing any TCP hot path.
519 ///
520 /// # Errors
521 /// Returns [`ServerError`] when admission is refused
522 /// ([`ServerError::ConnectionLimitReached`]), incarnation allocation fails,
523 /// or beamr spawn/registration fails.
524 pub(super) fn spawn_transport_connection(
525 &self,
526 peer_addr: Option<SocketAddr>,
527 fd_guard: Option<TcpStream>,
528 mount: MountKind,
529 build_factory: &dyn Fn(
530 Arc<ConnectionRuntime>,
531 Option<ConnectionIncarnation>,
532 ) -> NativeHandlerFactory,
533 ) -> Result<ConnectionHandle, ServerError> {
534 self.inner
535 .spawn_transport_connection(peer_addr, fd_guard, mount, build_factory)
536 }
537
538 /// Admits one in-process connection over `server_end` (design §8 step 3).
539 ///
540 /// This replaces exactly the listener's `accept()` + `spawn_connection`
541 /// pair, and NOTHING else about admission. It runs the same
542 /// `try_reserve_admission` against the same §5 slot pool — an in-process
543 /// connect at capacity is refused with the same typed
544 /// [`ServerError::ConnectionLimitReached`] a socket connect is — allocates a
545 /// real durable [`ConnectionIncarnation`] from the same authority, so
546 /// participant binding, resume, and fate records work identically, and
547 /// registers the same record. Only two fields differ, and both are honest
548 /// descriptions rather than semantics: no fd guard (there is no descriptor
549 /// to keep alive) and `peer_addr: None` (there is no socket to have an
550 /// address). Nothing on this path reads a socket fact.
551 ///
552 /// The connection's wake is installed by the process itself on its first
553 /// serviced slice, which is the earliest point at which its pid and host
554 /// record — the two things the wake names — both exist.
555 ///
556 /// `pub(crate)` rather than `pub`: the embedding handle that grants
557 /// loopback connections lives in this crate, and widening the surface
558 /// further would hand an outside caller a way to reach the runtime that
559 /// module privacy currently denies it.
560 ///
561 /// # Errors
562 /// Returns [`ServerError`] when admission is refused
563 /// ([`ServerError::ConnectionLimitReached`]), incarnation allocation fails,
564 /// or beamr spawn/registration fails.
565 pub(crate) fn spawn_loopback_connection(
566 &self,
567 server_end: LoopbackServerEnd,
568 ) -> Result<ConnectionHandle, ServerError> {
569 // The same interior-mutability handoff the socket path uses: the native
570 // handler factory is `Fn + Send + Sync`, so the duplex end cannot be
571 // moved into it and the FIRST handler build takes it out exactly once.
572 let holder = Arc::new(Mutex::new(Some(server_end)));
573 let build = move |runtime: Arc<ConnectionRuntime>,
574 incarnation: Option<ConnectionIncarnation>|
575 -> NativeHandlerFactory {
576 let holder = Arc::clone(&holder);
577 Box::new(move || {
578 Box::new(LoopbackConnectionProcess::from_loopback_holder(
579 Arc::clone(&runtime),
580 &holder,
581 incarnation,
582 ))
583 })
584 };
585 self.inner
586 .spawn_transport_connection(None, None, MountKind::Loopback, &build)
587 }
588
589 /// Stops the beamr scheduler used by connection processes.
590 pub fn shutdown(&self) {
591 // Remove every host record while the readiness owner is still live. The
592 // removal path ACKs deregistration and only then releases each fd guard;
593 // scheduler shutdown subsequently drops the process-owned handles.
594 for connection in self.inner.runtime.active_connections() {
595 self.inner.runtime.finish(connection.pid);
596 }
597 self.inner.scheduler.shutdown();
598 }
599
600 /// R7 test instrument: slices serviced by connection `pid` since spawn.
601 #[cfg(test)]
602 pub(crate) fn slice_count(&self, pid: u64) -> u64 {
603 self.inner.runtime.slice_count(pid)
604 }
605
606 /// The mount the admitting door stamped on `pid`'s registry record, or
607 /// `None` when no record is tracked (design §10).
608 #[cfg(test)]
609 pub(crate) fn connection_mount(&self, pid: u64) -> Option<MountKind> {
610 self.inner.runtime.connection_mount(pid)
611 }
612
613 /// Whether `pid`'s registry record holds an fd guard, or `None` when no
614 /// record is tracked. A loopback record must answer `Some(false)`: there is
615 /// no descriptor for the guard to keep alive.
616 #[cfg(test)]
617 pub(crate) fn connection_has_fd_guard(&self, pid: u64) -> Option<bool> {
618 self.inner.runtime.connection_has_fd_guard(pid)
619 }
620
621 /// Installs a one-use readiness marker for the next serviced slice of `pid`.
622 #[cfg(test)]
623 pub(crate) fn observe_next_slice(&self, pid: u64) -> Receiver<u64> {
624 self.inner.runtime.observe_next_slice(pid)
625 }
626
627 /// Installs a one-use readiness marker for the next genuine scheduler park of
628 /// `pid`. The delivered value is the process's slice count at the final probe
629 /// that selected `Wait`.
630 #[cfg(test)]
631 pub(crate) fn observe_next_park(&self, pid: u64) -> Receiver<u64> {
632 self.inner.runtime.observe_next_park(pid)
633 }
634
635 /// Returns a marker for the current park when `pid` is already settled, or
636 /// the next park when a coalesced readiness event has started another slice.
637 #[cfg(test)]
638 pub(crate) fn observe_settled_park(&self, pid: u64) -> Receiver<u64> {
639 self.inner.runtime.observe_settled_park(pid)
640 }
641
642 /// Queues an explicit outbound capacity for the next TCP process constructed.
643 #[cfg(test)]
644 pub(crate) fn queue_next_outbound_capacity(&self, capacity: usize) {
645 self.inner.runtime.queue_next_outbound_capacity(capacity);
646 }
647
648 #[cfg(test)]
649 pub(crate) fn install_participant_holdback_pause(&self, pid: u64) -> Receiver<()> {
650 self.inner.runtime.install_participant_holdback_pause(pid)
651 }
652
653 #[cfg(test)]
654 pub(crate) fn resume_test_process(&self, pid: u64) -> bool {
655 self.inner.runtime.ready_waker(pid).is_some_and(|waker| {
656 waker.fire();
657 true
658 })
659 }
660
661 /// Reserved push reply slots outstanding (test observability for the public
662 /// push paths — lets e2e tests assert slot reclamation and cap accounting).
663 #[cfg(test)]
664 pub(super) fn pending_push_count(&self) -> usize {
665 self.inner.runtime.pending_push_count()
666 }
667
668 /// R6 test seam: a [`ReadyWaker`](super::wake::ReadyWaker) for `pid` — the same
669 /// handle a subscription-inbox or reply-availability notifier fires.
670 #[cfg(test)]
671 pub(super) fn ready_waker(&self, pid: u64) -> Option<super::wake::ReadyWaker> {
672 self.inner.runtime.ready_waker(pid)
673 }
674
675 /// Registered readiness tokens held in host records (test observability).
676 #[cfg(test)]
677 pub(super) fn readiness_registration_count(&self) -> usize {
678 self.inner.runtime.readiness_registration_count()
679 }
680
681 /// Kernel fd registered for `pid` (test observability for fd-reuse races).
682 #[cfg(test)]
683 pub(super) fn readiness_fd(&self, pid: u64) -> Option<RawFd> {
684 self.inner.runtime.readiness_fd(pid)
685 }
686
687 /// The readiness token registered for `pid` (test observability). Lets the
688 /// fd-reuse successor oracle capture a stale token before reclamation and
689 /// replay its deregister afterwards, proving it is a keyed no-op.
690 #[cfg(test)]
691 pub(super) fn readiness_token(&self, pid: u64) -> Option<ReadinessToken> {
692 self.inner.runtime.readiness_token(pid)
693 }
694
695 /// Installs the pid-specific reclamation gate (oracle 26) and returns its
696 /// `(reached, release, done)` endpoints.
697 #[cfg(test)]
698 pub(super) fn install_reclaim_barrier(
699 &self,
700 pid: u64,
701 ) -> (Arc<Barrier>, Arc<Barrier>, Arc<Barrier>) {
702 self.inner.runtime.install_reclaim_barrier(pid)
703 }
704
705 /// A weak handle to the connection runtime (test observability): lets a
706 /// lifetime test assert the runtime — and transitively the durable store's
707 /// writer lock — is released synchronously at supervisor drop rather than
708 /// held by the detached reclaim reactor.
709 #[cfg(test)]
710 pub(super) fn runtime_weak(&self) -> Weak<ConnectionRuntime> {
711 Arc::downgrade(&self.inner.runtime)
712 }
713
714 /// Installs a one-use observation for the process-owned stream at `fd` being
715 /// dropped. External scheduler termination removes the process-table entry
716 /// before an executing native handler is destroyed, so table absence is not
717 /// sufficient evidence that the descriptor is reusable.
718 #[cfg(test)]
719 pub(super) fn observe_process_stream_drop(&self, fd: RawFd) -> Receiver<()> {
720 self.inner.runtime.observe_process_stream_drop(fd)
721 }
722
723 /// Installs a one-use arm-to-probe barrier and returns its test endpoints.
724 #[cfg(test)]
725 pub(super) fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
726 self.inner.runtime.install_pre_wait_barrier()
727 }
728
729 /// Barrier-staged final probes that observed newly arrived work.
730 #[cfg(test)]
731 pub(super) fn pre_wait_probe_hits(&self) -> u64 {
732 self.inner.runtime.pre_wait_probe_hits()
733 }
734
735 /// Installs the one-use drain-park gate (oracles 18, 19) and returns its
736 /// `(armed, release)` endpoints.
737 #[cfg(test)]
738 pub(super) fn install_drain_park_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
739 self.inner.runtime.install_drain_park_barrier()
740 }
741
742 /// Drain waiter wakes that observed a real connection removal (oracle 12).
743 #[cfg(test)]
744 pub(super) fn drain_exit_wakes(&self) -> u64 {
745 self.inner.runtime.drain_exit_wakes()
746 }
747
748 /// Drain waiter deadline expirations (oracles 12, 16).
749 #[cfg(test)]
750 pub(super) fn drain_deadline_hits(&self) -> u64 {
751 self.inner.runtime.drain_deadline_hits()
752 }
753}
754
755/// Handle for one supervised connection process.
756#[derive(Clone, Debug)]
757pub struct ConnectionHandle {
758 pid: u64,
759 peer_addr: Option<SocketAddr>,
760 connection_incarnation: Option<ConnectionIncarnation>,
761 supervisor: Arc<SupervisorInner>,
762}
763
764impl ConnectionHandle {
765 /// Returns the beamr process id for this connection.
766 #[must_use]
767 pub const fn pid(&self) -> u64 {
768 self.pid
769 }
770
771 /// Returns the peer address if it was available from the accepted stream.
772 #[must_use]
773 pub const fn peer_addr(&self) -> Option<SocketAddr> {
774 self.peer_addr
775 }
776
777 /// Returns the durable participant connection incarnation, when this
778 /// supervisor has a complete participant service installed.
779 ///
780 /// `None` identifies a services adapter that does not advertise participant
781 /// lifecycle semantics.
782 #[must_use]
783 pub const fn connection_incarnation(&self) -> Option<ConnectionIncarnation> {
784 self.connection_incarnation
785 }
786
787 /// Returns whether the beamr process is still live.
788 #[must_use]
789 pub fn is_live(&self) -> bool {
790 self.supervisor
791 .scheduler
792 .process_table()
793 .get(self.pid)
794 .is_some()
795 }
796
797 /// Requests an error exit for tests and supervisor control paths.
798 ///
799 /// # Errors
800 /// Returns [`ServerError`] when the process is no longer live.
801 pub fn request_crash(&self) -> Result<(), ServerError> {
802 if self
803 .supervisor
804 .scheduler
805 .enqueue_atom_message(self.pid, Atom::ERROR)
806 {
807 Ok(())
808 } else {
809 Err(ServerError::ListenerAccept {
810 message: format!("connection process {} is not live", self.pid),
811 })
812 }
813 }
814}
815
816/// Awaits the correlated reply to a single server-initiated push.
817///
818/// Returned by [`ConnectionSupervisor::push_to_connection`]. The reply slot is
819/// resolved when the originating connection process receives a `PushReply` frame
820/// carrying the same correlation id, so [`PushReplyAwaiter::receive`] blocks
821/// (bounded) for that one correlated answer.
822#[derive(Debug)]
823pub struct PushReplyAwaiter {
824 correlation_id: u64,
825 receiver: Receiver<Vec<u8>>,
826 /// This push's absolute reply deadline, mirrored from its slot. `None` (the
827 /// default push) selects the no-deadline receive path, which NEVER touches
828 /// the runtime — byte-compatible with 0.2.3, no shared-lock exposure.
829 /// `Some` lets `receive` wait `min(caller quantum, time until deadline)` and
830 /// resolve expiry promptly, so the caller's quantum can never select a
831 /// deadlined push's terminal outcome.
832 deadline: Option<Instant>,
833 /// Weak handle to the owning runtime, used ONLY by the explicit-deadline
834 /// path to resolve expiry host-side at [`receive`](Self::receive). A
835 /// no-deadline push never upgrades it. `Weak` so the awaiter never keeps the
836 /// runtime alive; if it is already gone, the slot (and its sender) is gone
837 /// with it — the connection side is torn down.
838 runtime: Weak<ConnectionRuntime>,
839}
840
841impl PushReplyAwaiter {
842 /// Returns the correlation id this awaiter is matched on.
843 #[must_use]
844 pub const fn correlation_id(&self) -> u64 {
845 self.correlation_id
846 }
847
848 /// Blocks up to `timeout` for the client's correlated reply payload.
849 ///
850 /// `timeout` is a WAIT QUANTUM ONLY — a MAXIMUM wait, not a promise to
851 /// block: an elapsed poll is a benign re-arm, never a failure; the reply's
852 /// lifetime belongs to the push. A caller may re-invoke `receive`
853 /// indefinitely after a [`ServerError::PushReplyTimeout`]: the reserved slot
854 /// is untouched and a later reply is still delivered byte-exact. The poll
855 /// quantum never changes the protocol outcome — for a deadlined push the
856 /// call waits no longer than the EARLIER of the caller's quantum and the
857 /// push's deadline, so the terminal expiry is returned promptly once due,
858 /// never held until the quantum ends and never deferred past it.
859 ///
860 /// A push with no explicit deadline never touches shared supervisor state
861 /// here: the elapsed quantum returns straight from the channel wait
862 /// (behaviour-compatible with 0.2.3 — no registry lock, no contention, no
863 /// poison exposure on the unchanged API).
864 ///
865 /// # Errors
866 /// Returns [`ServerError::PushReplyTimeout`] when no reply arrived within this
867 /// `timeout` quantum and the push's deadline (if any) is not yet due (a
868 /// benign re-arm — call again to keep waiting);
869 /// [`ServerError::PushReplyExpired`] when the push carried an explicit reply
870 /// deadline (via
871 /// [`push_to_connection_with_deadline`](ConnectionSupervisor::push_to_connection_with_deadline))
872 /// and that deadline is due (terminal: the slot is removed and its §5 cap
873 /// admission released; returned as soon as the deadline passes, even
874 /// mid-quantum — but evaluated at observation points, not against the wall
875 /// clock: a reply already delivered when this call observes the slot wins
876 /// over expiry, even if it arrived after the deadline instant); or
877 /// [`ServerError::PushReplyDisconnected`] when the connection process
878 /// dropped the reply slot (the connection closed — the prompt worker-death
879 /// signal). The variants are distinct so callers classify by type, not
880 /// message.
881 pub fn receive(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
882 self.deadline.map_or_else(
883 || self.receive_no_deadline(timeout),
884 |deadline| self.receive_deadlined(timeout, deadline),
885 )
886 }
887
888 /// The default-push receive: exactly the 0.2.3 shape. One bounded channel
889 /// wait; an elapsed quantum is a benign timeout straight from the channel —
890 /// no runtime upgrade, no registry lock, EVER (unrelated registry work can
891 /// never stretch this call past its quantum, and registry poison cannot
892 /// reach it).
893 fn receive_no_deadline(&self, timeout: Duration) -> Result<Vec<u8>, ServerError> {
894 match self.receiver.recv_timeout(timeout) {
895 Ok(payload) => Ok(payload),
896 Err(RecvTimeoutError::Timeout) => Err(ServerError::PushReplyTimeout {
897 correlation_id: self.correlation_id,
898 }),
899 Err(RecvTimeoutError::Disconnected) => Err(ServerError::PushReplyDisconnected {
900 correlation_id: self.correlation_id,
901 }),
902 }
903 }
904
905 /// The deadlined receive: waits `min(caller quantum, time until deadline)`
906 /// and re-evaluates reply-first-then-expiry on every wake, so the caller's
907 /// quantum can never select the terminal outcome (S1). Order per iteration:
908 ///
909 /// 1. Deliver a reply already in hand — an answer that is here must never be
910 /// reported as a timeout OR an expiry (the observation-point rule).
911 /// 2. If the deadline is due, resolve expiry atomically against the registry
912 /// (`expire_slot`) and return the terminal outcome promptly — even when
913 /// the caller's quantum has time left (the quantum is a max wait).
914 /// 3. Otherwise wait for the earlier of quantum-remaining and deadline; a
915 /// wake re-runs 1-2, and an exhausted quantum before the deadline is the
916 /// benign `PushReplyTimeout` re-arm with the slot untouched.
917 fn receive_deadlined(
918 &self,
919 timeout: Duration,
920 deadline: Instant,
921 ) -> Result<Vec<u8>, ServerError> {
922 let started = Instant::now();
923 loop {
924 if let Some(result) = self.try_take_reply() {
925 return result;
926 }
927 let now = Instant::now();
928 if now >= deadline {
929 return self.expire_slot();
930 }
931 let quantum_left = timeout.saturating_sub(now.duration_since(started));
932 if quantum_left.is_zero() {
933 return Err(ServerError::PushReplyTimeout {
934 correlation_id: self.correlation_id,
935 });
936 }
937 match self
938 .receiver
939 .recv_timeout(quantum_left.min(deadline.duration_since(now)))
940 {
941 Ok(payload) => return Ok(payload),
942 Err(RecvTimeoutError::Disconnected) => {
943 return Err(ServerError::PushReplyDisconnected {
944 correlation_id: self.correlation_id,
945 });
946 }
947 // Re-loop: deliver a reply that raced the wake, expire a
948 // now-due deadline, or report the exhausted quantum benignly.
949 Err(RecvTimeoutError::Timeout) => {}
950 }
951 }
952 }
953
954 /// Resolves a due deadline against the registry's atomic removal transition.
955 fn expire_slot(&self) -> Result<Vec<u8>, ServerError> {
956 let timeout_error = || ServerError::PushReplyTimeout {
957 correlation_id: self.correlation_id,
958 };
959 let Some(runtime) = self.runtime.upgrade() else {
960 // The runtime is gone, and the slot map (with every sender) with it:
961 // the connection side is torn down. Re-check the channel so the
962 // dropped sender reads as the established DISCONNECTED outcome — a
963 // dead runtime must not be misreported as a benign healthy-but-slow
964 // timeout (S4).
965 return self
966 .try_take_reply()
967 .unwrap_or(Err(ServerError::PushReplyDisconnected {
968 correlation_id: self.correlation_id,
969 }));
970 };
971 match runtime.expire_push_if_due(self.correlation_id) {
972 PushSlotDisposition::Expired => Err(ServerError::PushReplyExpired {
973 correlation_id: self.correlation_id,
974 }),
975 // Unreachable by construction (this is only called with the deadline
976 // due, and the registry re-reads a monotonic clock); honest benign
977 // fallback rather than a panic.
978 PushSlotDisposition::Live => Err(timeout_error()),
979 // Another path (a concurrent `resolve_push`, or connection close)
980 // removed the slot under the registry lock while we waited on it. Its
981 // send, if any, happens under that same lock, so re-check the channel:
982 // a delivered reply is present now; a dropped sender is disconnected.
983 PushSlotDisposition::Absent => self
984 .try_take_reply()
985 .unwrap_or_else(|| Err(timeout_error())),
986 }
987 }
988
989 /// Non-blocking check for a reply already sitting in the channel. `Some` with
990 /// the payload or a disconnected error; `None` when the channel is still empty
991 /// (no reply yet — the caller re-arms).
992 fn try_take_reply(&self) -> Option<Result<Vec<u8>, ServerError>> {
993 match self.receiver.try_recv() {
994 Ok(payload) => Some(Ok(payload)),
995 Err(TryRecvError::Disconnected) => Some(Err(ServerError::PushReplyDisconnected {
996 correlation_id: self.correlation_id,
997 })),
998 Err(TryRecvError::Empty) => None,
999 }
1000 }
1001}
1002
1003/// The kernel-parked exit-event reactor (W4 leg 1 reclamation carve-out, §4.1).
1004/// It is the single TOLD source that reclaims connection host records for
1005/// processes that exit WITHOUT a final handler slice, replacing the retired
1006/// per-accept `reap_crashed` scan for that class.
1007///
1008/// It blocks on beamr's sole exit-event subscription — never polling, never
1009/// timed — and on each delivered [`ExitEvent::Exited`] it (1) drains the
1010/// retained additive outcome so beamr's exactly-once outcome store stays bounded
1011/// (we are the sole subscriber and therefore the sole drainer) and (2) reclaims
1012/// the pid through [`ConnectionRuntime::reclaim_terminated`], which funnels into
1013/// `remove()`. On the bounded queue's [`ExitEvent::Lagged`] overflow marker it
1014/// runs exactly one reconciliation pass over the tracked records (beamr's
1015/// documented recovery), driven by that one TELL — not a timer.
1016///
1017/// It holds WEAK handles to both the scheduler and the runtime and upgrades them
1018/// per event, so it never keeps either alive past supervisor drop. It returns
1019/// when the subscription disconnects (scheduler and publisher dropped) OR when a
1020/// per-event upgrade fails — both observed at an event delivery, never sampled,
1021/// so there is no stop flag (LAW-1). The runtime and its durable store therefore
1022/// release synchronously at supervisor drop rather than after the reactor exits.
1023fn run_reclaim_reactor(
1024 subscription: &ExitEventSubscription,
1025 scheduler: &Weak<Scheduler>,
1026 runtime: &Weak<ConnectionRuntime>,
1027) {
1028 loop {
1029 match subscription.recv() {
1030 Ok(ExitEvent::Exited { pid, reason }) => {
1031 let Some(runtime) = runtime.upgrade() else {
1032 return;
1033 };
1034 runtime.deliver_reclamation(scheduler, pid, reason);
1035 }
1036 Ok(ExitEvent::Lagged) => {
1037 let (Some(runtime), Some(scheduler)) = (runtime.upgrade(), scheduler.upgrade())
1038 else {
1039 return;
1040 };
1041 runtime.reap_crashed(&scheduler);
1042 }
1043 Err(_) => return,
1044 }
1045 }
1046}
1047
1048pub(super) struct SupervisorInner {
1049 scheduler: Arc<Scheduler>,
1050 runtime: Arc<ConnectionRuntime>,
1051 incarnations: Option<Arc<ConnectionIncarnationAuthority>>,
1052}
1053
1054impl std::fmt::Debug for SupervisorInner {
1055 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1056 formatter
1057 .debug_struct("SupervisorInner")
1058 .field("runtime", &self.runtime)
1059 .finish_non_exhaustive()
1060 }
1061}
1062
1063impl SupervisorInner {
1064 fn new(
1065 services: Arc<dyn ConnectionServices>,
1066 notifier: Option<Arc<dyn ConnectionNotifier>>,
1067 auth_token: Option<Vec<u8>>,
1068 limits: LimitsConfig,
1069 fatal_shutdown: Option<ShutdownHandle>,
1070 ) -> Result<Self, ServerError> {
1071 let installed_services = ConnectionServiceInstallation::capture(services);
1072 let incarnations = installed_services
1073 .participant_service
1074 .as_ref()
1075 .map(
1076 |service| -> Result<Arc<ConnectionIncarnationAuthority>, ServerError> {
1077 ConnectionIncarnationAuthority::startup(
1078 service.durable_store(),
1079 limits.max_connections,
1080 service.publication_conversation_limit(),
1081 service,
1082 )
1083 .map(Arc::new)
1084 },
1085 )
1086 .transpose()?;
1087 let atoms = AtomTable::with_common_atoms();
1088 let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
1089 let registry = Arc::new(ModuleRegistry::new());
1090
1091 let scheduler = Scheduler::with_services(
1092 SchedulerConfig {
1093 thread_count: Some(CONNECTION_SCHEDULER_THREADS),
1094 ..SchedulerConfig::default()
1095 },
1096 SchedulerServices::from_config().owned_readiness(),
1097 registry,
1098 )
1099 .map_err(|message| ServerError::ListenerAccept {
1100 message: format!("failed to start connection scheduler: {message}"),
1101 })?;
1102 let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
1103 let scheduler = Arc::new(scheduler);
1104 // The runtime captures a WEAK handle to the connection scheduler so
1105 // notifier wakes (R3/R1(vi)) can be fired from another actor's slice
1106 // without a strong scheduler↔process↔runtime cycle that would leak the
1107 // whole connection scheduler.
1108 let runtime = Arc::new(ConnectionRuntime::new(
1109 ConnectionRuntimeInstallation {
1110 services: installed_services,
1111 incarnations: incarnations.clone(),
1112 fatal_shutdown,
1113 },
1114 control_atom,
1115 ready_atom,
1116 Arc::downgrade(&scheduler),
1117 notifier,
1118 auth_token,
1119 limits,
1120 ));
1121 // W4 leg 1 reclamation carve-out (§4.1): the kernel-parked exit-event
1122 // reactor is the TOLD source that reclaims a connection host record whose
1123 // process exited WITHOUT a final handler slice (external/panic
1124 // termination). It blocks on beamr's single exit-event subscription —
1125 // never a poll — and routes every reclamation through the same `remove()`
1126 // funnel as an ordinary exit. Detached on purpose: it exits when the
1127 // scheduler (and so its event publisher) drops, so there is no stop flag
1128 // to sample (LAW-1).
1129 match scheduler.subscribe_exit_events() {
1130 Some(subscription) => {
1131 let reactor_scheduler = Arc::downgrade(&scheduler);
1132 // WEAK, symmetric with the scheduler handle: the reactor must not
1133 // keep the runtime (and its durable store's writer lock) alive past
1134 // supervisor drop. It upgrades per event and exits on a failed
1135 // upgrade, so the runtime is released synchronously at drop.
1136 let reactor_runtime = Arc::downgrade(&runtime);
1137 thread::Builder::new()
1138 .name("liminal-connection-reclaim".to_owned())
1139 .spawn(move || {
1140 run_reclaim_reactor(&subscription, &reactor_scheduler, &reactor_runtime);
1141 })
1142 .map_err(|error| ServerError::ListenerAccept {
1143 message: format!("failed to start connection reclamation reactor: {error}"),
1144 })?;
1145 }
1146 None => {
1147 tracing::error!(
1148 "connection scheduler exit-event subscription unavailable; \
1149 external-termination reclamation has no TOLD exit source (the \
1150 shutdown-drain scan that once backstopped it was retired by W4 leg 3)"
1151 );
1152 }
1153 }
1154 Ok(Self {
1155 scheduler,
1156 runtime,
1157 incarnations,
1158 })
1159 }
1160
1161 fn spawn_connection(
1162 self: &Arc<Self>,
1163 stream: TcpStream,
1164 ) -> Result<ConnectionHandle, ServerError> {
1165 // §5 `max_connections`: ATOMIC admission reservation acquired BEFORE any
1166 // process construction (review round 1 item 7 — a signed bound must not
1167 // be exceedable by concurrent callers; check-then-spawn across an
1168 // unlocked window was). The CAS reservation is released on every failure
1169 // path below and converts into the connection record at `register`;
1170 // thereafter the single record-removal path (`remove`) releases it. An
1171 // over-cap accept therefore costs nothing and the bound holds under any
1172 // concurrency.
1173 self.runtime.try_reserve_admission()?;
1174 let reservation = AdmissionReservation {
1175 runtime: &self.runtime,
1176 armed: true,
1177 };
1178 stream
1179 .set_nonblocking(true)
1180 .map_err(|error| ServerError::ListenerAccept {
1181 message: format!("failed to configure connection stream: {error}"),
1182 })?;
1183 let peer_addr = stream.peer_addr().ok();
1184 // The host-held duplicate keeps the fd alive until the single record-removal
1185 // path has synchronously deregistered readiness. External process death can
1186 // therefore never let fd reuse overtake host-side deregistration.
1187 let fd_guard = stream
1188 .try_clone()
1189 .map_err(|error| ServerError::ListenerAccept {
1190 message: format!("failed to retain connection fd for teardown: {error}"),
1191 })?;
1192 let connection_incarnation = self.allocate_connection_incarnation()?;
1193 let holder = Arc::new(Mutex::new(Some(stream)));
1194 let runtime = Arc::clone(&self.runtime);
1195 let process_holder = Arc::clone(&holder);
1196 let factory: NativeHandlerFactory = Box::new(move || {
1197 Box::new(ConnectionProcess::from_holder(
1198 Arc::clone(&runtime),
1199 peer_addr,
1200 &process_holder,
1201 connection_incarnation,
1202 ))
1203 });
1204 let pid =
1205 self.scheduler
1206 .spawn_native(factory)
1207 .map_err(|error| ServerError::ListenerAccept {
1208 message: format!("failed to spawn connection process: {error}"),
1209 })?;
1210 if let Err(error) = self.runtime.register_connection(
1211 pid,
1212 peer_addr,
1213 connection_incarnation,
1214 MountKind::Tcp,
1215 Some(fd_guard),
1216 ) {
1217 // Registration failure leaves no host record to reap. Terminate the
1218 // just-spawned process explicitly so neither its stream nor admission
1219 // reservation can escape this failed spawn.
1220 self.scheduler.terminate_process(pid, ExitReason::Error);
1221 return Err(error);
1222 }
1223 // The reservation is now owned by the registered record: `remove` (the
1224 // single record-removal path — finish/mark_crashed/reap all funnel
1225 // through it) releases the admission when the record goes away.
1226 reservation.convert();
1227 Ok(ConnectionHandle {
1228 pid,
1229 peer_addr,
1230 connection_incarnation,
1231 supervisor: Arc::clone(self),
1232 })
1233 }
1234
1235 /// LP-WS-TRANSPORT R1.3: the sibling-transport spawn body. Mirrors
1236 /// [`Self::spawn_connection`]'s admission → incarnation → spawn → register →
1237 /// convert sequence exactly (same reservation guard, same failure rollback,
1238 /// same single record-removal ownership), differing only in that the caller
1239 /// supplies the native handler factory and the host-held fd guard instead of
1240 /// a raw `TcpStream`. Purely additive; the TCP path never calls this.
1241 fn spawn_transport_connection(
1242 self: &Arc<Self>,
1243 peer_addr: Option<SocketAddr>,
1244 fd_guard: Option<TcpStream>,
1245 mount: MountKind,
1246 build_factory: &dyn Fn(
1247 Arc<ConnectionRuntime>,
1248 Option<ConnectionIncarnation>,
1249 ) -> NativeHandlerFactory,
1250 ) -> Result<ConnectionHandle, ServerError> {
1251 self.runtime.try_reserve_admission()?;
1252 let reservation = AdmissionReservation {
1253 runtime: &self.runtime,
1254 armed: true,
1255 };
1256 let connection_incarnation = self.allocate_connection_incarnation()?;
1257 let factory = build_factory(Arc::clone(&self.runtime), connection_incarnation);
1258 let pid =
1259 self.scheduler
1260 .spawn_native(factory)
1261 .map_err(|error| ServerError::ListenerAccept {
1262 message: format!("failed to spawn connection process: {error}"),
1263 })?;
1264 if let Err(error) = self.runtime.register_connection(
1265 pid,
1266 peer_addr,
1267 connection_incarnation,
1268 mount,
1269 fd_guard,
1270 ) {
1271 self.scheduler.terminate_process(pid, ExitReason::Error);
1272 return Err(error);
1273 }
1274 reservation.convert();
1275 Ok(ConnectionHandle {
1276 pid,
1277 peer_addr,
1278 connection_incarnation,
1279 supervisor: Arc::clone(self),
1280 })
1281 }
1282
1283 fn allocate_connection_incarnation(
1284 &self,
1285 ) -> Result<Option<ConnectionIncarnation>, ServerError> {
1286 let Some(authority) = self.incarnations.as_ref() else {
1287 return Ok(None);
1288 };
1289 // Production-era uniqueness invariant: every published incarnation is
1290 // unique against ALL durable references — binding epochs committed
1291 // into conversation logs included — by allocator-log monotonicity
1292 // alone, not by the completeness of the reference set below.
1293 //
1294 // 1. Startup replays the durable allocator stream and STRICTLY
1295 // increments the server incarnation, fsyncing the Startup event
1296 // before any listener becomes ready
1297 // (`IncarnationStream::startup`); a server value is never wrapped
1298 // or reused, so no two process lifetimes share one.
1299 // 2. Within a lifetime, allocations are serialized under this
1300 // authority's mutex, candidates start strictly above the durable
1301 // `last_examined_connection_ordinal`
1302 // (`allocate_connection_incarnation`), and every allocation's
1303 // event is appended and flushed BEFORE its pair is published
1304 // (`StartedIncarnationStream::allocate`), so ordinals never
1305 // repeat within a lifetime and replay restores a head at or
1306 // above every published ordinal.
1307 // 3. A durable reference can only name a pair this allocator
1308 // previously PUBLISHED (binding epochs are committed only after
1309 // their connection was admitted), and the same store's flush
1310 // barrier orders the allocator event before any conversation-log
1311 // entry that references it.
1312 //
1313 // The live-connection reference set below is therefore defense in
1314 // depth — a bounded collision skip against a rolled-back or divergent
1315 // allocator stream — never the uniqueness foundation, and never a raw
1316 // caller-supplied matrix.
1317 let references = self.runtime.complete_active_incarnation_references()?;
1318 authority.allocate(&references).map(Some)
1319 }
1320
1321 fn broadcast_control(&self, control: &ConnectionControl) {
1322 for connection in self.runtime.active_connections() {
1323 if !self.enqueue_control(connection.pid, control.clone()) {
1324 tracing::debug!(
1325 connection_pid = connection.pid,
1326 peer_addr = ?connection.peer_addr,
1327 ?control,
1328 "connection control message skipped because process is not live"
1329 );
1330 }
1331 }
1332 }
1333
1334 /// Queues `control` for `pid` and wakes the process. Returns whether the
1335 /// control was PUBLISHED (left in the queue with a successful wake, or
1336 /// already consumed by a drain) — `false` guarantees no consumer ever saw
1337 /// it.
1338 ///
1339 /// S8: a failed wake does NOT prove the queued control was never consumed.
1340 /// The insert releases the queue lock before the wake attempt, and a
1341 /// process already executing a control drain (each control atom drains ALL
1342 /// queued controls for the pid) can pop the just-inserted entry in that
1343 /// window, then exit before the wake check. Publication is therefore
1344 /// disambiguated BY OBSERVATION on the failed-wake path: `remove_control`
1345 /// finding and removing the entry proves no consumer saw it (truly
1346 /// unpublished — `false`); finding nothing proves a drain consumed it
1347 /// (`pop_control` is the only other remover of queue entries, and the
1348 /// removal key embeds the push's runtime-unique correlation id, so it can
1349 /// never match a different entry) — the control was published and the
1350 /// caller's slot lifecycle carries the delivery truth (`true`).
1351 fn enqueue_control(&self, pid: u64, control: ConnectionControl) -> bool {
1352 // Keep a key for the failure-path removal before the control is moved into
1353 // the queue, so a non-`Copy` (push) control can still be located and pulled
1354 // back out if the scheduler wakeup fails.
1355 let removal_key = control.clone();
1356 if self.runtime.push_control(pid, control).is_err() {
1357 return false;
1358 }
1359 // Deterministic test seam in the insert->wake window (S8 staging).
1360 #[cfg(test)]
1361 self.runtime.run_pre_wake_barrier();
1362 if self
1363 .scheduler
1364 .enqueue_atom_message(pid, self.runtime.control_atom())
1365 {
1366 true
1367 } else {
1368 // Failed wake: the entry's fate is the publication verdict. Removed
1369 // here => nobody consumed it => unpublished. Already gone => a
1370 // drain consumed it before the wake check => published. (A poisoned
1371 // queue lock reads as not-removed => published — the safe
1372 // direction: the slot lifecycle then reports the truthful outcome,
1373 // whereas claiming "unpublished" could be a lie.)
1374 !self.runtime.remove_control(pid, &removal_key)
1375 }
1376 }
1377}
1378
1379/// RAII guard for one §5 `max_connections` admission reservation.
1380///
1381/// Acquired (via [`ConnectionRuntime::try_reserve_admission`]) before any process
1382/// construction in `spawn_connection`; every early-return failure path releases
1383/// it through `Drop`, and a successful `register` converts it into the
1384/// connection record (whose removal releases the admission instead). RAII means
1385/// no failure path — present or future — can leak a reservation.
1386struct AdmissionReservation<'a> {
1387 runtime: &'a ConnectionRuntime,
1388 armed: bool,
1389}
1390
1391impl AdmissionReservation<'_> {
1392 /// Converts the reservation into record ownership: `Drop` no longer releases
1393 /// it, because the registered record's removal will.
1394 fn convert(mut self) {
1395 self.armed = false;
1396 }
1397}
1398
1399impl Drop for AdmissionReservation<'_> {
1400 fn drop(&mut self) {
1401 if self.armed {
1402 self.runtime.release_admission();
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Eq)]
1408pub(super) enum ConnectionControl {
1409 NotifyShutdown,
1410 ForceClose,
1411 /// Server-initiated push of an opaque payload, correlated by `correlation_id`,
1412 /// to be written out as a [`Frame::Push`] by the receiving connection process.
1413 Push {
1414 correlation_id: u64,
1415 payload: Vec<u8>,
1416 },
1417}
1418
1419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1420pub struct ActiveConnection {
1421 pid: u64,
1422 peer_addr: Option<SocketAddr>,
1423}
1424
1425#[cfg(test)]
1426#[derive(Debug, Clone)]
1427struct PreWaitBarrier {
1428 armed: Arc<Barrier>,
1429 release: Arc<Barrier>,
1430}
1431
1432/// Pid-specific, one-use deterministic gate on the reclamation delivery path
1433/// (oracle 26). The exit-event reactor stages it only for the targeted pid, so
1434/// unrelated exits still reclaim immediately; for the target it rendezvouses
1435/// `reached` (proving the pid is dead but its record still tracked — the S8
1436/// reclamation window), then `release` (the harness lets the reclaim proceed),
1437/// then `done` (the `remove()` funnel completed). It changes no production
1438/// semantics — the whole type and its call sites are `#[cfg(test)]`.
1439#[cfg(test)]
1440#[derive(Debug, Clone)]
1441struct ReclaimBarrier {
1442 pid: u64,
1443 reached: Arc<Barrier>,
1444 release: Arc<Barrier>,
1445 done: Arc<Barrier>,
1446}
1447
1448#[derive(Debug)]
1449struct ConnectionServiceInstallation {
1450 services: Arc<dyn ConnectionServices>,
1451 participant_service: Option<InstalledParticipantService>,
1452}
1453
1454impl ConnectionServiceInstallation {
1455 /// Captures the service adapter's capability posture exactly once, before
1456 /// participant incarnation startup or connection process construction.
1457 fn capture(services: Arc<dyn ConnectionServices>) -> Self {
1458 let participant_service = services.participant_service();
1459 Self {
1460 services,
1461 participant_service,
1462 }
1463 }
1464}
1465
1466#[derive(Debug)]
1467struct ConnectionRuntimeInstallation {
1468 services: ConnectionServiceInstallation,
1469 incarnations: Option<Arc<ConnectionIncarnationAuthority>>,
1470 fatal_shutdown: Option<ShutdownHandle>,
1471}
1472
1473#[derive(Debug)]
1474pub(super) struct ConnectionRuntime {
1475 services: Arc<dyn ConnectionServices>,
1476 /// Complete participant handler/store bundle captured at supervisor startup.
1477 /// `Some` is paired with an incarnation authority on `SupervisorInner`.
1478 participant_service: Option<InstalledParticipantService>,
1479 /// Same started authority used for allocation, shared for terminal Open/Complete.
1480 incarnations: Option<Arc<ConnectionIncarnationAuthority>>,
1481 /// Existing runtime shutdown activation notified by the first post-Open fatal.
1482 /// Test-only/runtime-less constructors intentionally carry `None`.
1483 fatal_shutdown: Option<ShutdownHandle>,
1484 records: Mutex<HashMap<u64, ConnectionRecord>>,
1485 controls: Mutex<Vec<QueuedConnectionControl>>,
1486 control_atom: Atom,
1487 /// R6 single `READY` wake atom for this connection scheduler. Fired by every
1488 /// wake source's notifier (R3/R1(vi)); coalescing and duplicates are harmless.
1489 ready_atom: Atom,
1490 /// Weak handle to the connection scheduler, used to build [`ReadyWaker`]s a
1491 /// notifier fires from another actor's slice. Weak so it never keeps the
1492 /// scheduler alive (the scheduler owns the processes that own this runtime).
1493 scheduler: Weak<Scheduler>,
1494 /// W4 leg 3 (§4.3) TOLD drain-completion primitive. Reuses the
1495 /// [`ShutdownHandle`] `Condvar` shape (`shutdown.rs` reuse candidate (c)): a
1496 /// monotonic connection-removal generation guarded by [`Self::drain_removed`]'s
1497 /// mutex, bumped once whenever [`Self::remove`] actually drops a record — the
1498 /// single removal funnel every exit route (in-slice `mark_crashed`/`finish`,
1499 /// the reclaim reactor, and the reconciliation scan) reaches. The
1500 /// shutdown-sequence drain/settle waiter parks on the `Condvar` and wakes only
1501 /// on a delivered exit (a generation bump + `notify_all`) or the one admitted
1502 /// deadline it passes to `wait_timeout`. No periodic reap or count scan.
1503 drain_generation: Mutex<u64>,
1504 /// Woken on every connection-record removal; the drain/settle waiter parks
1505 /// here. Paired with [`Self::drain_generation`] under the same mutex so an
1506 /// exit delivered between the waiter's arm-before-observe snapshot and its
1507 /// park cannot be lost (oracle 18).
1508 drain_removed: Condvar,
1509 /// FIX A-ii shutdown flush barrier: a delivery-quiescence generation of the
1510 /// exact TOLD `drain_generation` shape. Bumped whenever a connection parks
1511 /// with every accepted publish already fanned out to its socket — but only
1512 /// while [`Self::settle_armed`] is set (the flush barrier is waiting) — so
1513 /// normal operation pays nothing. Guards [`Self::settle_changed`]'s mutex.
1514 settle_generation: Mutex<u64>,
1515 /// Woken when a connection reaches delivery quiescence during shutdown; the
1516 /// flush barrier parks here, paired with [`Self::settle_generation`] for the
1517 /// same arm-before-observe safety as the drain waiter.
1518 settle_changed: Condvar,
1519 /// Set only while the flush barrier is actively waiting, so a park bumps the
1520 /// settle generation and wakes the barrier ONLY when someone is listening.
1521 settle_armed: AtomicBool,
1522 /// Test-only count of drain waiter wakes that observed a real removal
1523 /// (generation advanced across the park). A quiet drain records zero — it
1524 /// wakes only for the single deadline (oracle 12).
1525 #[cfg(test)]
1526 drain_exit_wakes: AtomicU64,
1527 /// Test-only count of drain waiter deadline expirations. A quiet drain that
1528 /// times out records exactly one — one arming, one delivery, no helper tick
1529 /// (oracles 12, 16).
1530 #[cfg(test)]
1531 drain_deadline_hits: AtomicU64,
1532 /// Test-only one-use gate in the drain waiter's observe->park window, so a
1533 /// harness can deliver an exit strictly after the completion observation and
1534 /// before the park to pin the arm-before-observe barrier (oracles 18, 19).
1535 #[cfg(test)]
1536 drain_park_barrier: Mutex<Option<PreWaitBarrier>>,
1537 /// R7 (§1.2(6)) test-only per-connection slice counter, keyed by pid. Bumped
1538 /// once at the head of every serviced slice. The park-flip's permanent rule-1
1539 /// assertion (a parked connection's counter must not advance without an event)
1540 /// reads this; the instrument lands now with a test proving it counts slices.
1541 #[cfg(test)]
1542 slice_counts: Mutex<HashMap<u64, u64>>,
1543 /// One-use readiness markers for the next serviced slice of a process.
1544 #[cfg(test)]
1545 slice_observers: Mutex<HashMap<u64, Sender<u64>>>,
1546 /// One-use readiness markers emitted only after the real final probe selects
1547 /// `Wait`, immediately before the native process returns to the scheduler.
1548 #[cfg(test)]
1549 park_observers: Mutex<HashMap<u64, Sender<u64>>>,
1550 /// Most recent slice count whose real final probe selected `Wait`.
1551 #[cfg(test)]
1552 park_counts: Mutex<HashMap<u64, u64>>,
1553 /// Explicit capacities consumed in TCP process construction order.
1554 #[cfg(test)]
1555 next_outbound_capacities: Mutex<VecDeque<usize>>,
1556 #[cfg(test)]
1557 participant_holdback_pauses: Mutex<HashMap<u64, Sender<()>>>,
1558 /// Deterministic test gate placed after arm and before the final probe.
1559 #[cfg(test)]
1560 pre_wait_barrier: Mutex<Option<PreWaitBarrier>>,
1561 /// Deterministic test gate in `enqueue_control`'s insert->wake window (S8).
1562 #[cfg(test)]
1563 pre_wake_barrier: Mutex<Option<PreWaitBarrier>>,
1564 /// Pid-specific one-use gate held on the reclamation delivery path so a test
1565 /// can pin the dead-but-tracked S8 window deterministically (oracle 26).
1566 #[cfg(test)]
1567 reclaim_barrier: Mutex<Option<ReclaimBarrier>>,
1568 /// Barrier-staged slices where the final probe found newly arrived work.
1569 #[cfg(test)]
1570 pre_wait_probe_hits: AtomicU64,
1571 /// One-use observers for process-owned streams reaching their actual drop
1572 /// boundary after external scheduler termination.
1573 #[cfg(test)]
1574 process_stream_drop_observers: Mutex<HashMap<RawFd, Sender<()>>>,
1575 /// One-shot reply slots for in-flight server pushes, keyed by correlation id.
1576 /// The supervisor registers a slot in `push_to_connection`; the connection
1577 /// process resolves it when the matching `PushReply` frame arrives. Each slot
1578 /// records the owning connection pid so the close path can drop a connection's
1579 /// outstanding slots and wake their awaiters with a prompt disconnected error.
1580 push_replies: Mutex<HashMap<u64, PendingPush>>,
1581 /// Monotonic source of push correlation ids. Server-allocated, so it never
1582 /// collides with a client-chosen id on this connection.
1583 next_push_id: AtomicU64,
1584 /// §5 `max_connections` admission counter. Incremented atomically (CAS
1585 /// against the limit) BEFORE a connection process is constructed and
1586 /// decremented on every spawn-failure path and on final record removal, so
1587 /// the signed bound holds under concurrent spawns — admission is never
1588 /// derived from the records-map length across an unlocked window.
1589 admissions: AtomicU64,
1590 /// Optional application hook invoked on worker registration and on the close
1591 /// of a connection that had registered. `None` keeps liminal standalone: a
1592 /// `WorkerRegister` is accepted with no callback.
1593 notifier: Option<Arc<dyn ConnectionNotifier>>,
1594 /// Configured connection auth token (the `[auth]` section's token as opaque
1595 /// bytes). `Some` gates the `Connect` handshake — the frame's `auth_token` must
1596 /// match under a constant-time comparison; `None` leaves the server open-access,
1597 /// byte-identical to the pre-auth behaviour.
1598 auth_token: Option<Vec<u8>>,
1599 /// Operational caps (§5). Enforced with typed refusals at admission:
1600 /// per-connection subscription, conversation, push, and pending-reply counts,
1601 /// plus the shared inbox byte budget. Non-config constructors carry the signed
1602 /// defaults ([`LimitsConfig::default`]).
1603 limits: LimitsConfig,
1604}
1605
1606impl ConnectionRuntime {
1607 fn new(
1608 installation: ConnectionRuntimeInstallation,
1609 control_atom: Atom,
1610 ready_atom: Atom,
1611 scheduler: Weak<Scheduler>,
1612 notifier: Option<Arc<dyn ConnectionNotifier>>,
1613 auth_token: Option<Vec<u8>>,
1614 limits: LimitsConfig,
1615 ) -> Self {
1616 let ConnectionRuntimeInstallation {
1617 services:
1618 ConnectionServiceInstallation {
1619 services,
1620 participant_service,
1621 },
1622 incarnations,
1623 fatal_shutdown,
1624 } = installation;
1625 Self {
1626 services,
1627 participant_service,
1628 incarnations,
1629 fatal_shutdown,
1630 records: Mutex::new(HashMap::new()),
1631 controls: Mutex::new(Vec::new()),
1632 control_atom,
1633 ready_atom,
1634 scheduler,
1635 drain_generation: Mutex::new(0),
1636 drain_removed: Condvar::new(),
1637 settle_generation: Mutex::new(0),
1638 settle_changed: Condvar::new(),
1639 settle_armed: AtomicBool::new(false),
1640 #[cfg(test)]
1641 drain_exit_wakes: AtomicU64::new(0),
1642 #[cfg(test)]
1643 drain_deadline_hits: AtomicU64::new(0),
1644 #[cfg(test)]
1645 drain_park_barrier: Mutex::new(None),
1646 #[cfg(test)]
1647 slice_counts: Mutex::new(HashMap::new()),
1648 #[cfg(test)]
1649 slice_observers: Mutex::new(HashMap::new()),
1650 #[cfg(test)]
1651 park_observers: Mutex::new(HashMap::new()),
1652 #[cfg(test)]
1653 park_counts: Mutex::new(HashMap::new()),
1654 #[cfg(test)]
1655 next_outbound_capacities: Mutex::new(VecDeque::new()),
1656 #[cfg(test)]
1657 participant_holdback_pauses: Mutex::new(HashMap::new()),
1658 #[cfg(test)]
1659 pre_wait_barrier: Mutex::new(None),
1660 #[cfg(test)]
1661 pre_wake_barrier: Mutex::new(None),
1662 #[cfg(test)]
1663 reclaim_barrier: Mutex::new(None),
1664 #[cfg(test)]
1665 pre_wait_probe_hits: AtomicU64::new(0),
1666 #[cfg(test)]
1667 process_stream_drop_observers: Mutex::new(HashMap::new()),
1668 push_replies: Mutex::new(HashMap::new()),
1669 next_push_id: AtomicU64::new(1),
1670 admissions: AtomicU64::new(0),
1671 notifier,
1672 auth_token,
1673 limits,
1674 }
1675 }
1676
1677 /// Atomically reserves one §5 `max_connections` admission slot: a CAS loop
1678 /// against the configured limit, so N concurrent callers racing for the last
1679 /// slot admit EXACTLY one — the bound cannot be transiently exceeded.
1680 ///
1681 /// # Errors
1682 /// Returns [`ServerError::ConnectionLimitReached`] when every slot is taken.
1683 fn try_reserve_admission(&self) -> Result<(), ServerError> {
1684 self.ensure_participant_service_live()?;
1685 let limit = self.limits.max_connections as u64;
1686 let mut current = self.admissions.load(Ordering::Acquire);
1687 loop {
1688 if current >= limit {
1689 return Err(ServerError::ConnectionLimitReached {
1690 limit: self.limits.max_connections,
1691 });
1692 }
1693 match self.admissions.compare_exchange_weak(
1694 current,
1695 current + 1,
1696 Ordering::AcqRel,
1697 Ordering::Acquire,
1698 ) {
1699 Ok(_) => return Ok(()),
1700 Err(observed) => current = observed,
1701 }
1702 }
1703 }
1704
1705 /// Releases one admission slot. Called by the spawn failure paths (via the
1706 /// [`AdmissionReservation`] guard) and by [`Self::remove`] when a registered
1707 /// record is removed — exactly one release per reservation. Saturating so a
1708 /// spurious release can never wrap the counter.
1709 fn release_admission(&self) {
1710 let mut current = self.admissions.load(Ordering::Acquire);
1711 loop {
1712 let next = current.saturating_sub(1);
1713 match self.admissions.compare_exchange_weak(
1714 current,
1715 next,
1716 Ordering::AcqRel,
1717 Ordering::Acquire,
1718 ) {
1719 Ok(_) => return,
1720 Err(observed) => current = observed,
1721 }
1722 }
1723 }
1724
1725 /// The operational caps (§5) this runtime enforces.
1726 pub(super) const fn limits(&self) -> &LimitsConfig {
1727 &self.limits
1728 }
1729
1730 /// The connection's single R6 `READY` wake atom.
1731 pub(super) const fn ready_atom(&self) -> Atom {
1732 self.ready_atom
1733 }
1734
1735 /// Builds a [`ReadyWaker`] targeting `pid` on the connection scheduler, if the
1736 /// scheduler is still live. `None` when the scheduler is gone (teardown) or in
1737 /// scheduler-free unit tests — a notifier with no waker simply never wakes,
1738 /// which under the busy loop is redundant anyway (the every-slice pump still
1739 /// services the source). This is the seam every wake source installs its
1740 /// notifier through (R3/R1(vi)).
1741 pub(super) fn ready_waker(&self, pid: u64) -> Option<super::wake::ReadyWaker> {
1742 let scheduler = self.scheduler.upgrade()?;
1743 let ready_pending = self
1744 .records
1745 .lock()
1746 .ok()?
1747 .get(&pid)
1748 .map(|record| Arc::clone(&record.ready_pending))?;
1749 Some(super::wake::ReadyWaker::new(
1750 &scheduler,
1751 pid,
1752 self.ready_atom,
1753 ready_pending,
1754 ))
1755 }
1756
1757 /// Acknowledges READY edges whose mailbox atoms were drained before this slice.
1758 pub(super) fn acknowledge_ready(&self, pid: u64) {
1759 if let Ok(records) = self.records.lock()
1760 && let Some(record) = records.get(&pid)
1761 {
1762 record.ready_pending.store(false, Ordering::Release);
1763 }
1764 }
1765
1766 /// Reports a READY edge queued while the current process snapshot is executing.
1767 pub(super) fn ready_pending(&self, pid: u64) -> bool {
1768 self.records
1769 .lock()
1770 .ok()
1771 .and_then(|records| {
1772 records
1773 .get(&pid)
1774 .map(|record| record.ready_pending.load(Ordering::Acquire))
1775 })
1776 .unwrap_or(false)
1777 }
1778
1779 /// FIX A-ii: marks `pid` as executing a slice — not parked, so not yet
1780 /// delivery-quiescent. Reuses the registry lock the slice already takes for
1781 /// `is_registered`; it never touches the barrier condvar.
1782 pub(super) fn mark_running(&self, pid: u64) {
1783 if let Ok(records) = self.records.lock()
1784 && let Some(record) = records.get(&pid)
1785 {
1786 record.parked.store(false, Ordering::Release);
1787 }
1788 }
1789
1790 /// FIX A-ii: marks `pid` as parked with every accepted publish already fanned
1791 /// out to its socket, and — only while the shutdown flush barrier is armed —
1792 /// bumps the settle generation and wakes it. The bump/notify is skipped
1793 /// entirely in normal operation, so a park off the shutdown path is just one
1794 /// flag store.
1795 pub(super) fn mark_parked(&self, pid: u64) {
1796 if let Ok(records) = self.records.lock()
1797 && let Some(record) = records.get(&pid)
1798 {
1799 record.parked.store(true, Ordering::Release);
1800 }
1801 if self.settle_armed.load(Ordering::Acquire) {
1802 self.signal_settle_changed();
1803 }
1804 }
1805
1806 /// Bumps the delivery-quiescence generation under its mutex, then wakes the
1807 /// flush barrier — the same lock-then-notify discipline as
1808 /// [`Self::signal_connection_removed`], so a park published before the notify
1809 /// can never be missed by a waiter holding the mutex across its re-check.
1810 fn signal_settle_changed(&self) {
1811 {
1812 let mut generation = recover_lock(&self.settle_generation);
1813 *generation = generation.wrapping_add(1);
1814 }
1815 self.settle_changed.notify_all();
1816 }
1817
1818 /// Reads the current delivery-quiescence generation under its mutex.
1819 fn settle_generation_snapshot(&self) -> u64 {
1820 *recover_lock(&self.settle_generation)
1821 }
1822
1823 /// True when every tracked connection is parked with no pending READY edge —
1824 /// i.e. every accepted publish has been pumped to its subscriber's outbound
1825 /// and no fan-out wake is still in flight. An empty registry is trivially
1826 /// quiescent.
1827 fn all_connections_delivery_quiesced(&self) -> bool {
1828 let Ok(records) = self.records.lock() else {
1829 return false;
1830 };
1831 records.values().all(|record| {
1832 record.parked.load(Ordering::Acquire) && !record.ready_pending.load(Ordering::Acquire)
1833 })
1834 }
1835
1836 /// FIX A-ii: wakes every tracked connection once so it drains its socket and
1837 /// pumps its subscriptions. This is what makes the flush barrier robust to the
1838 /// readiness gap: a publisher whose fire-and-forget publish bytes have arrived
1839 /// but whose readiness wake has not yet rescheduled it still looks "parked",
1840 /// so without this it could be sampled as quiescent before it admits and fans
1841 /// out those publishes. Firing sets each connection's `ready_pending` edge, so
1842 /// the quiescence check below cannot pass until every woken connection has run
1843 /// its slice (reading and admitting any buffered publish, whose admission then
1844 /// fires its subscribers in turn) and re-parked.
1845 fn wake_all_connections_for_flush(&self) {
1846 let pids: Vec<u64> = self
1847 .records
1848 .lock()
1849 .map(|records| records.keys().copied().collect())
1850 .unwrap_or_default();
1851 for pid in pids {
1852 if let Some(waker) = self.ready_waker(pid) {
1853 waker.fire();
1854 }
1855 }
1856 }
1857
1858 /// FIX A-ii: parks until every tracked connection has fanned out its accepted
1859 /// publishes (delivery quiescence) or `deadline` elapses, returning `true` on
1860 /// quiescence and `false` when the single admitted deadline won. The TOLD
1861 /// shape mirrors [`Self::wait_for_active_connections_drained`]: arm the
1862 /// barrier, then snapshot-before-observe so a park delivered between the
1863 /// observation and the wait bumps a generation the wait detects. It samples
1864 /// nothing on a timer — it wakes only on a delivered park (generation bump) or
1865 /// the one deadline.
1866 pub(super) fn wait_for_delivery_quiesced(&self, deadline: Instant) -> bool {
1867 self.settle_armed.store(true, Ordering::Release);
1868 // Force every connection to run once so a publisher whose buffered publish
1869 // bytes have not yet triggered a readiness wake still drains and admits
1870 // them (and fires its subscribers) before the quiescence check can pass.
1871 self.wake_all_connections_for_flush();
1872 let quiesced = loop {
1873 let snapshot = self.settle_generation_snapshot();
1874 if self.all_connections_delivery_quiesced() {
1875 break true;
1876 }
1877 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
1878 break false;
1879 };
1880 let outcome = self
1881 .settle_changed
1882 .wait_timeout_while(
1883 recover_lock(&self.settle_generation),
1884 remaining,
1885 |current| *current == snapshot,
1886 )
1887 .unwrap_or_else(PoisonError::into_inner);
1888 drop(outcome);
1889 };
1890 self.settle_armed.store(false, Ordering::Release);
1891 quiesced
1892 }
1893
1894 /// R7: records one serviced slice for `pid`. Bumped at the head of every
1895 /// slice; the park-flip's quiescence assertion reads [`Self::slice_count`].
1896 #[cfg(test)]
1897 pub(super) fn record_slice(&self, pid: u64) {
1898 let count = if let Ok(mut counts) = self.slice_counts.lock() {
1899 let count = counts.entry(pid).or_insert(0);
1900 *count += 1;
1901 *count
1902 } else {
1903 return;
1904 };
1905 if let Ok(mut observers) = self.slice_observers.lock()
1906 && let Some(observer) = observers.remove(&pid)
1907 {
1908 let _ = observer.send(count);
1909 }
1910 }
1911
1912 #[cfg(test)]
1913 fn observe_next_slice(&self, pid: u64) -> Receiver<u64> {
1914 let (sender, receiver) = channel();
1915 if let Ok(mut observers) = self.slice_observers.lock() {
1916 observers.insert(pid, sender);
1917 }
1918 receiver
1919 }
1920
1921 #[cfg(test)]
1922 fn observe_next_park(&self, pid: u64) -> Receiver<u64> {
1923 let (sender, receiver) = channel();
1924 if let Ok(mut observers) = self.park_observers.lock() {
1925 observers.insert(pid, sender);
1926 }
1927 receiver
1928 }
1929
1930 #[cfg(test)]
1931 fn observe_settled_park(&self, pid: u64) -> Receiver<u64> {
1932 let (sender, receiver) = channel();
1933 let Ok(counts) = self.slice_counts.lock() else {
1934 return receiver;
1935 };
1936 let current = counts.get(&pid).copied().unwrap_or(0);
1937 let Ok(parks) = self.park_counts.lock() else {
1938 return receiver;
1939 };
1940 if current > 0 && parks.get(&pid).copied() == Some(current) {
1941 let _ = sender.send(current);
1942 } else if let Ok(mut observers) = self.park_observers.lock() {
1943 observers.insert(pid, sender);
1944 }
1945 drop(parks);
1946 drop(counts);
1947 receiver
1948 }
1949
1950 #[cfg(test)]
1951 pub(super) fn record_park(&self, pid: u64) {
1952 let count = self.slice_count(pid);
1953 if let Ok(mut parks) = self.park_counts.lock() {
1954 parks.insert(pid, count);
1955 }
1956 if let Ok(mut observers) = self.park_observers.lock()
1957 && let Some(observer) = observers.remove(&pid)
1958 {
1959 let _ = observer.send(count);
1960 }
1961 }
1962
1963 #[cfg(test)]
1964 fn queue_next_outbound_capacity(&self, capacity: usize) {
1965 if let Ok(mut capacities) = self.next_outbound_capacities.lock() {
1966 capacities.push_back(capacity);
1967 }
1968 }
1969
1970 #[cfg(test)]
1971 pub(super) fn take_next_outbound_capacity(&self) -> Option<usize> {
1972 self.next_outbound_capacities
1973 .lock()
1974 .ok()
1975 .and_then(|mut capacities| capacities.pop_front())
1976 }
1977
1978 #[cfg(test)]
1979 fn install_participant_holdback_pause(&self, pid: u64) -> Receiver<()> {
1980 let (sender, receiver) = channel();
1981 if let Ok(mut pauses) = self.participant_holdback_pauses.lock() {
1982 pauses.insert(pid, sender);
1983 }
1984 receiver
1985 }
1986
1987 #[cfg(test)]
1988 pub(super) fn pause_participant_holdback(&self, pid: u64) -> bool {
1989 self.participant_holdback_pauses
1990 .lock()
1991 .ok()
1992 .and_then(|mut pauses| pauses.remove(&pid))
1993 .is_some_and(|sender| {
1994 let _ = sender.send(());
1995 true
1996 })
1997 }
1998
1999 /// R7: slices serviced by connection `pid` since spawn (test instrument).
2000 #[cfg(test)]
2001 pub(super) fn slice_count(&self, pid: u64) -> u64 {
2002 self.slice_counts
2003 .lock()
2004 .map_or(0, |counts| counts.get(&pid).copied().unwrap_or(0))
2005 }
2006
2007 #[cfg(test)]
2008 fn install_pre_wait_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
2009 let armed = Arc::new(Barrier::new(2));
2010 let release = Arc::new(Barrier::new(2));
2011 if let Ok(mut slot) = self.pre_wait_barrier.lock() {
2012 *slot = Some(PreWaitBarrier {
2013 armed: Arc::clone(&armed),
2014 release: Arc::clone(&release),
2015 });
2016 }
2017 (armed, release)
2018 }
2019
2020 /// Installs the pid-specific reclamation gate (oracle 26) and returns its
2021 /// `(reached, release, done)` endpoints. The harness rendezvouses `reached`
2022 /// to pin the dead-but-tracked window, `release` to let the reclaim proceed,
2023 /// and `done` to observe the `remove()` funnel completing.
2024 #[cfg(test)]
2025 pub(super) fn install_reclaim_barrier(
2026 &self,
2027 pid: u64,
2028 ) -> (Arc<Barrier>, Arc<Barrier>, Arc<Barrier>) {
2029 let reached = Arc::new(Barrier::new(2));
2030 let release = Arc::new(Barrier::new(2));
2031 let done = Arc::new(Barrier::new(2));
2032 if let Ok(mut slot) = self.reclaim_barrier.lock() {
2033 *slot = Some(ReclaimBarrier {
2034 pid,
2035 reached: Arc::clone(&reached),
2036 release: Arc::clone(&release),
2037 done: Arc::clone(&done),
2038 });
2039 }
2040 (reached, release, done)
2041 }
2042
2043 /// Takes the installed reclamation gate iff it targets `pid` (one-use). Any
2044 /// other pid's delivery is ungated, so unrelated exits reclaim immediately.
2045 #[cfg(test)]
2046 fn stage_reclaim_barrier(&self, pid: u64) -> Option<ReclaimBarrier> {
2047 let mut slot = self.reclaim_barrier.lock().ok()?;
2048 if slot.as_ref().is_some_and(|barrier| barrier.pid == pid) {
2049 slot.take()
2050 } else {
2051 None
2052 }
2053 }
2054
2055 /// Installs the one-use drain-park gate (oracles 18, 19) and returns its
2056 /// `(armed, release)` endpoints. Staged in the drain waiter's observe->park
2057 /// window so a harness can deliver an exit strictly between the completion
2058 /// observation and the park. Entirely `#[cfg(test)]`; changes no production
2059 /// wait semantics.
2060 #[cfg(test)]
2061 pub(super) fn install_drain_park_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
2062 let armed = Arc::new(Barrier::new(2));
2063 let release = Arc::new(Barrier::new(2));
2064 if let Ok(mut slot) = self.drain_park_barrier.lock() {
2065 *slot = Some(PreWaitBarrier {
2066 armed: Arc::clone(&armed),
2067 release: Arc::clone(&release),
2068 });
2069 }
2070 (armed, release)
2071 }
2072
2073 /// Runs the one-use drain-park gate, if installed. One-use so only the staged
2074 /// park rendezvouses; every later park in the same waiter runs ungated.
2075 #[cfg(test)]
2076 fn run_drain_park_barrier(&self) {
2077 let barrier = self
2078 .drain_park_barrier
2079 .lock()
2080 .ok()
2081 .and_then(|mut slot| slot.take());
2082 let Some(barrier) = barrier else {
2083 return;
2084 };
2085 barrier.armed.wait();
2086 barrier.release.wait();
2087 }
2088
2089 /// Test-only count of drain waiter wakes that observed a real removal.
2090 #[cfg(test)]
2091 pub(super) fn drain_exit_wakes(&self) -> u64 {
2092 self.drain_exit_wakes.load(Ordering::SeqCst)
2093 }
2094
2095 /// Test-only count of drain waiter deadline expirations.
2096 #[cfg(test)]
2097 pub(super) fn drain_deadline_hits(&self) -> u64 {
2098 self.drain_deadline_hits.load(Ordering::SeqCst)
2099 }
2100
2101 /// Runs a one-use deterministic test gate after arm. Returns whether the gate
2102 /// was installed so only that staged probe contributes to observability.
2103 #[cfg(test)]
2104 pub(super) fn run_pre_wait_barrier(&self) -> bool {
2105 let barrier = self
2106 .pre_wait_barrier
2107 .lock()
2108 .ok()
2109 .and_then(|mut slot| slot.take());
2110 let Some(barrier) = barrier else {
2111 return false;
2112 };
2113 barrier.armed.wait();
2114 barrier.release.wait();
2115 true
2116 }
2117
2118 /// Installs a one-use barrier in `enqueue_control`'s insert->wake window
2119 /// (S8 staging: lets a test act as the control-drain consumer between the
2120 /// queue insertion and the wake attempt) and returns its test endpoints.
2121 #[cfg(test)]
2122 pub(super) fn install_pre_wake_barrier(&self) -> (Arc<Barrier>, Arc<Barrier>) {
2123 let armed = Arc::new(Barrier::new(2));
2124 let release = Arc::new(Barrier::new(2));
2125 if let Ok(mut slot) = self.pre_wake_barrier.lock() {
2126 *slot = Some(PreWaitBarrier {
2127 armed: Arc::clone(&armed),
2128 release: Arc::clone(&release),
2129 });
2130 }
2131 (armed, release)
2132 }
2133
2134 /// Runs the one-use insert->wake test gate, if installed.
2135 #[cfg(test)]
2136 pub(super) fn run_pre_wake_barrier(&self) {
2137 let barrier = self
2138 .pre_wake_barrier
2139 .lock()
2140 .ok()
2141 .and_then(|mut slot| slot.take());
2142 if let Some(barrier) = barrier {
2143 barrier.armed.wait();
2144 barrier.release.wait();
2145 }
2146 }
2147
2148 #[cfg(test)]
2149 pub(super) fn record_pre_wait_probe_hit(&self) {
2150 self.pre_wait_probe_hits.fetch_add(1, Ordering::AcqRel);
2151 }
2152
2153 #[cfg(test)]
2154 fn pre_wait_probe_hits(&self) -> u64 {
2155 self.pre_wait_probe_hits.load(Ordering::Acquire)
2156 }
2157
2158 #[cfg(test)]
2159 fn observe_process_stream_drop(&self, fd: RawFd) -> Receiver<()> {
2160 let (sender, receiver) = channel();
2161 if let Ok(mut observers) = self.process_stream_drop_observers.lock() {
2162 observers.insert(fd, sender);
2163 }
2164 receiver
2165 }
2166
2167 /// Publishes the process-owned stream's real drop boundary to a waiting test.
2168 #[cfg(test)]
2169 pub(super) fn record_process_stream_drop(&self, fd: RawFd) {
2170 let observer = self
2171 .process_stream_drop_observers
2172 .lock()
2173 .ok()
2174 .and_then(|mut observers| observers.remove(&fd));
2175 if let Some(observer) = observer {
2176 let _ = observer.send(());
2177 }
2178 }
2179
2180 /// Builds a runtime wrapping `services` for unit tests that exercise
2181 /// `apply_frame` without a live scheduler. Uses a fresh interned control atom
2182 /// and no notifier.
2183 #[cfg(test)]
2184 pub(super) fn for_tests(services: Arc<dyn ConnectionServices>) -> Self {
2185 let atoms = AtomTable::with_common_atoms();
2186 let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
2187 let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
2188 Self::new(
2189 ConnectionRuntimeInstallation {
2190 services: ConnectionServiceInstallation::capture(services),
2191 incarnations: None,
2192 fatal_shutdown: None,
2193 },
2194 control_atom,
2195 ready_atom,
2196 Weak::new(),
2197 None,
2198 None,
2199 LimitsConfig::default(),
2200 )
2201 }
2202
2203 /// Builds a runtime wrapping `services` with explicit `limits` for unit tests
2204 /// that exercise the §5 admission caps without a live scheduler.
2205 #[cfg(test)]
2206 pub(super) fn for_tests_with_limits(
2207 services: Arc<dyn ConnectionServices>,
2208 limits: LimitsConfig,
2209 ) -> Self {
2210 let atoms = AtomTable::with_common_atoms();
2211 let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
2212 let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
2213 Self::new(
2214 ConnectionRuntimeInstallation {
2215 services: ConnectionServiceInstallation::capture(services),
2216 incarnations: None,
2217 fatal_shutdown: None,
2218 },
2219 control_atom,
2220 ready_atom,
2221 Weak::new(),
2222 None,
2223 None,
2224 limits,
2225 )
2226 }
2227
2228 /// Builds a runtime wrapping `services` with a configured auth `token` for unit
2229 /// tests that exercise the `Connect` handshake enforcement without a live
2230 /// scheduler. Uses a fresh interned control atom and no notifier.
2231 #[cfg(test)]
2232 pub(super) fn for_tests_with_auth_token(
2233 services: Arc<dyn ConnectionServices>,
2234 token: Vec<u8>,
2235 ) -> Self {
2236 let atoms = AtomTable::with_common_atoms();
2237 let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
2238 let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
2239 Self::new(
2240 ConnectionRuntimeInstallation {
2241 services: ConnectionServiceInstallation::capture(services),
2242 incarnations: None,
2243 fatal_shutdown: None,
2244 },
2245 control_atom,
2246 ready_atom,
2247 Weak::new(),
2248 None,
2249 Some(token),
2250 LimitsConfig::default(),
2251 )
2252 }
2253
2254 /// Builds a runtime wrapping `services` with a `notifier` for unit tests that
2255 /// exercise `apply_frame` and the close path without a live scheduler.
2256 #[cfg(test)]
2257 pub(super) fn for_tests_with_notifier(
2258 services: Arc<dyn ConnectionServices>,
2259 notifier: Arc<dyn ConnectionNotifier>,
2260 ) -> Self {
2261 let atoms = AtomTable::with_common_atoms();
2262 let control_atom = atoms.intern(CONNECTION_SHUTDOWN_CONTROL_ATOM);
2263 let ready_atom = atoms.intern(CONNECTION_READY_ATOM);
2264 Self::new(
2265 ConnectionRuntimeInstallation {
2266 services: ConnectionServiceInstallation::capture(services),
2267 incarnations: None,
2268 fatal_shutdown: None,
2269 },
2270 control_atom,
2271 ready_atom,
2272 Weak::new(),
2273 Some(notifier),
2274 None,
2275 LimitsConfig::default(),
2276 )
2277 }
2278
2279 pub(super) fn services(&self) -> &dyn ConnectionServices {
2280 self.services.as_ref()
2281 }
2282
2283 /// Returns the complete participant service captured at supervisor startup.
2284 pub(super) const fn participant_service(&self) -> Option<&InstalledParticipantService> {
2285 self.participant_service.as_ref()
2286 }
2287
2288 fn participant_service_fatal(&self) -> Result<Option<ParticipantServiceFatal>, ServerError> {
2289 let Some(service) = self.participant_service() else {
2290 return Ok(None);
2291 };
2292 service
2293 .service_fatal()
2294 .map_err(|error| ServerError::ParticipantIncarnation {
2295 phase: "participant fatal latch inspection",
2296 message: error.to_string(),
2297 })
2298 }
2299
2300 fn activate_fatal_shutdown(&self) {
2301 if let Some(shutdown) = self.fatal_shutdown.as_ref() {
2302 shutdown.initiate();
2303 }
2304 }
2305
2306 fn ensure_participant_service_live(&self) -> Result<(), ServerError> {
2307 let Some(fatal) = self.participant_service_fatal()? else {
2308 return Ok(());
2309 };
2310 self.activate_fatal_shutdown();
2311 Err(ServerError::ParticipantServiceFatal { fatal })
2312 }
2313
2314 fn latch_connection_fate_intent_incomplete(
2315 &self,
2316 open_sequence: u64,
2317 conversation_id: u64,
2318 ) -> Result<ParticipantServiceFatal, ServerError> {
2319 let Some(service) = self.participant_service() else {
2320 return Err(ServerError::ParticipantIncarnation {
2321 phase: "connection-fate fatal latch",
2322 message: "a durable Open lacks its installed participant service".to_owned(),
2323 });
2324 };
2325 let fatal = service
2326 .latch_connection_fate_intent_incomplete(open_sequence, conversation_id)
2327 .map_err(|error| ServerError::ParticipantIncarnation {
2328 phase: "connection-fate fatal latch",
2329 message: error.to_string(),
2330 })?;
2331 self.activate_fatal_shutdown();
2332 Ok(fatal)
2333 }
2334
2335 fn complete_connection_fate_fatal(
2336 &self,
2337 open_sequence: u64,
2338 conversations: &[u64],
2339 phase: &'static str,
2340 error: &impl std::fmt::Display,
2341 ) -> ServerError {
2342 tracing::error!(open_sequence, phase, %error, "durable connection-fate intent is incomplete");
2343 let Some(&conversation_id) = conversations.first() else {
2344 return ServerError::ParticipantIncarnation {
2345 phase: "connection-fate fatal target",
2346 message: "a durable Open has no tracked conversation target".to_owned(),
2347 };
2348 };
2349 match self.latch_connection_fate_intent_incomplete(open_sequence, conversation_id) {
2350 Ok(fatal) => ServerError::ParticipantServiceFatal { fatal },
2351 Err(latch_error) => latch_error,
2352 }
2353 }
2354
2355 /// Runs one typed terminal fold after classification and before teardown.
2356 pub(super) fn complete_connection_fate(
2357 &self,
2358 connection_incarnation: Option<ConnectionIncarnation>,
2359 class: ConnectionFateClass,
2360 conversations: &[u64],
2361 ) -> Result<(), ServerError> {
2362 if conversations.is_empty() {
2363 return Ok(());
2364 }
2365 self.ensure_participant_service_live()?;
2366 let (Some(connection_incarnation), Some(service), Some(authority)) = (
2367 connection_incarnation,
2368 self.participant_service(),
2369 self.incarnations.as_ref(),
2370 ) else {
2371 return Err(ServerError::ParticipantIncarnation {
2372 phase: "connection-fate authority composition",
2373 message: "tracked participant conversations lack a complete service/incarnation authority"
2374 .to_owned(),
2375 });
2376 };
2377 let intent =
2378 authority.open_connection_fate(connection_incarnation, class, conversations)?;
2379 if let Err(error) = service.handle_connection_fate(intent.work_item()) {
2380 return Err(self.complete_connection_fate_fatal(
2381 intent.open_sequence,
2382 conversations,
2383 "handler",
2384 &error,
2385 ));
2386 }
2387 if let Err(error) = authority.complete_connection_fate(intent.open_sequence) {
2388 return Err(self.complete_connection_fate_fatal(
2389 intent.open_sequence,
2390 conversations,
2391 "Complete",
2392 &error,
2393 ));
2394 }
2395 Ok(())
2396 }
2397
2398 /// Resolves the bound-only protocol-error gate from participant authority.
2399 pub(super) fn connection_has_bound_participant(
2400 &self,
2401 connection_incarnation: Option<ConnectionIncarnation>,
2402 conversations: &[u64],
2403 ) -> Result<bool, ServerError> {
2404 if conversations.is_empty() {
2405 return Ok(false);
2406 }
2407 self.ensure_participant_service_live()?;
2408 let (Some(connection_incarnation), Some(service)) =
2409 (connection_incarnation, self.participant_service())
2410 else {
2411 return Err(ServerError::ParticipantIncarnation {
2412 phase: "bound participant classification",
2413 message:
2414 "tracked participant conversations lack a complete service/incarnation pair"
2415 .to_owned(),
2416 });
2417 };
2418 service
2419 .connection_has_bound_participant(connection_incarnation, conversations)
2420 .map_err(|error| ServerError::ParticipantIncarnation {
2421 phase: "bound participant classification",
2422 message: error.to_string(),
2423 })
2424 }
2425
2426 /// Returns the configured connection auth token as opaque bytes, or `None` when
2427 /// no `[auth]` section was configured (open access).
2428 pub(super) fn auth_token(&self) -> Option<&[u8]> {
2429 self.auth_token.as_deref()
2430 }
2431
2432 /// Returns the configured connection-keyed notifier, if any.
2433 pub(super) fn notifier(&self) -> Option<&Arc<dyn ConnectionNotifier>> {
2434 self.notifier.as_ref()
2435 }
2436
2437 /// Offers a channel publish to the notifier's observability-drain tap, returning
2438 /// `true` when the application consumed it (so the connection process skips the
2439 /// normal fan-out). `false` when no notifier is installed (liminal standalone) or
2440 /// the notifier did not recognise the channel, so the caller can invoke it
2441 /// unconditionally and fall through to the normal publish path.
2442 pub(super) fn notifier_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
2443 self.notifier
2444 .as_ref()
2445 .is_some_and(|notifier| notifier.on_channel_publish(pid, channel, payload))
2446 }
2447
2448 /// Stores `registration` on the connection record for `pid`, so the close
2449 /// path can later fire `on_worker_unregistered` for exactly the connections
2450 /// that registered. A missing record (the connection already closed) is a
2451 /// no-op.
2452 ///
2453 /// # Errors
2454 /// Returns [`ServerError`] when the connection registry mutex is poisoned.
2455 pub(super) fn set_registration(
2456 &self,
2457 pid: u64,
2458 registration: WorkerRegistration,
2459 ) -> Result<(), ServerError> {
2460 if let Some(record) = lock(&self.records, "connection registry")?.get_mut(&pid) {
2461 record.registration = Some(registration);
2462 }
2463 Ok(())
2464 }
2465
2466 /// Allocates the next monotonic push correlation id.
2467 fn next_push_correlation_id(&self) -> u64 {
2468 self.next_push_id.fetch_add(1, Ordering::Relaxed)
2469 }
2470
2471 /// Registers a one-shot reply slot for `correlation_id`, owned by connection
2472 /// `pid`, and returns its receiver. `deadline` is the slot's optional absolute
2473 /// reply expiry (`None` = the default no-deadline shape). The connection
2474 /// process resolves the slot via [`resolve_push`]; the close path drops the
2475 /// connection's outstanding slots via [`cancel_pushes_for_connection`]; an
2476 /// explicit deadline resolves it via [`expire_push_if_due`].
2477 ///
2478 /// # Errors
2479 /// Returns [`ServerError`] when the correlation registry mutex is poisoned.
2480 fn register_push(
2481 &self,
2482 pid: u64,
2483 correlation_id: u64,
2484 deadline: Option<Instant>,
2485 ) -> Result<Receiver<Vec<u8>>, ServerError> {
2486 let (sender, receiver) = channel();
2487 let limit = self.limits.max_pending_pushes_per_connection;
2488 {
2489 let mut slots = lock(&self.push_replies, "push correlation registry")?;
2490 // §5 `max_pending_pushes_per_connection`: refuse a new in-flight push
2491 // once this connection already holds the cap. Counted per owning pid so
2492 // one connection cannot exhaust the shared registry; slots free on
2493 // reply, deadline expiry, or connection close. The count-and-insert
2494 // stays under the one lock so the cap is enforced atomically.
2495 let outstanding = slots.values().filter(|pending| pending.pid == pid).count();
2496 if outstanding >= limit {
2497 return Err(ServerError::ConnectionCapReached {
2498 operation: "server push".to_owned(),
2499 cap: "max_pending_pushes_per_connection",
2500 limit,
2501 });
2502 }
2503 slots.insert(
2504 correlation_id,
2505 PendingPush {
2506 pid,
2507 sender,
2508 deadline,
2509 },
2510 );
2511 }
2512 Ok(receiver)
2513 }
2514
2515 /// Host-side, lazy evaluation of a push's reply deadline, called from an
2516 /// elapsed [`PushReplyAwaiter::receive`] quantum. This NEVER wakes the
2517 /// connection process and runs no timer — it inspects supervisor-owned state
2518 /// under the registry lock only.
2519 ///
2520 /// A slot with an explicit deadline that has passed is removed here (dropping
2521 /// its `Sender` and releasing its §5 `max_pending_pushes_per_connection` cap
2522 /// admission, since the cap is the per-pid slot count) and reported
2523 /// [`PushSlotDisposition::Expired`]. A slot with no deadline, or a deadline
2524 /// still in the future, is left UNTOUCHED and reported
2525 /// [`PushSlotDisposition::Live`] — the elapsed quantum is a benign re-arm. A
2526 /// missing slot is [`PushSlotDisposition::Absent`].
2527 fn expire_push_if_due(&self, correlation_id: u64) -> PushSlotDisposition {
2528 // S4: a poisoned registry must NOT read as slot absence — the slot (and
2529 // its cap admission) may still be in the map. Reclamation recovers the
2530 // guard: removal-only operations are sound on a recovered map (a panic
2531 // in another critical section cannot leave the HashMap itself in a
2532 // partial state; only our bookkeeping invariants could be stale, and
2533 // removal restores them). Admission (`register_push`) stays fail-closed.
2534 let mut slots = recover_lock(&self.push_replies);
2535 let Some(pending) = slots.get(&correlation_id) else {
2536 return PushSlotDisposition::Absent;
2537 };
2538 // Copy the deadline out so the immutable borrow of `slots` ends before the
2539 // conditional `remove` below takes a mutable one.
2540 let deadline = pending.deadline;
2541 match deadline {
2542 Some(at) if Instant::now() >= at => {
2543 slots.remove(&correlation_id);
2544 PushSlotDisposition::Expired
2545 }
2546 _ => PushSlotDisposition::Live,
2547 }
2548 }
2549
2550 /// Drops a registered reply slot without resolving it, used on the
2551 /// push-enqueue failure path (the control could not be delivered to a
2552 /// now-gone process, so the just-reserved slot is unreachable). Dropping the
2553 /// slot's `Sender` wakes a still-waiting awaiter with a disconnected error.
2554 ///
2555 /// Returns whether THIS call removed the slot. Removal under the registry
2556 /// mutex is the atomic resolved-vs-cancelled transition: `false` means
2557 /// another path won — [`resolve_push`](Self::resolve_push) already sent the
2558 /// reply (its send happens under the same lock, so the payload is already in
2559 /// the channel when this returns), or the connection's close path dropped
2560 /// the slot (sender gone, channel disconnected).
2561 pub(super) fn cancel_push(&self, correlation_id: u64) -> bool {
2562 // S4: reclamation recovers a poisoned guard — a rollback that silently
2563 // skipped its removal would strand the slot and its cap admission.
2564 recover_lock(&self.push_replies)
2565 .remove(&correlation_id)
2566 .is_some()
2567 }
2568
2569 /// Drops every reply slot owned by connection `pid`, waking each awaiter with a
2570 /// disconnected error (the dropped `Sender` disconnects the awaiter's
2571 /// `Receiver`). Called from the close path so a connection that exits with
2572 /// in-flight pushes signals worker death immediately instead of leaving each
2573 /// awaiter to block the full push-reply timeout. A slot that [`resolve_push`]
2574 /// already removed is gone, so it is untouched here; an unknown pid is a no-op.
2575 fn cancel_pushes_for_connection(&self, pid: u64) {
2576 // S4: the close sweep is the reclamation of last resort ("connection
2577 // close at the latest") — it must complete on a poisoned map too.
2578 recover_lock(&self.push_replies).retain(|_correlation_id, pending| pending.pid != pid);
2579 }
2580
2581 /// S3 second half (shape (b), check-after-insert): pre-publication
2582 /// confirmation that the connection record for `pid` still exists, run in
2583 /// the INSERT -> CONFIRM -> PUBLISH order (S7 — confirming after the
2584 /// enqueue let a close-swept-then-answered push report `Err` for a Push the
2585 /// client had received). `true` leaves the slot in place and the caller may
2586 /// publish; `false` means a concurrent close already removed the record —
2587 /// this call then removes the caller's own just-inserted slot (rolling back
2588 /// its cap admission) so nothing is stranded, and the caller returns
2589 /// WITHOUT publishing: an `Err` from the push methods guarantees no `Push`
2590 /// control was published.
2591 ///
2592 /// Why exactly one side always observes the slot: `remove` (the single
2593 /// record-removal path) removes the host record BEFORE sweeping the pid's
2594 /// push slots, and this check reads the record AFTER inserting the slot and
2595 /// BEFORE the control is published. Both records accesses are serialized by
2596 /// the `records` mutex, so either (i) this read precedes the record removal
2597 /// — then the slot insert precedes the sweep (insert < read < removal <
2598 /// sweep in the happens-before order) and the SWEEP observes and removes
2599 /// the slot: if the control was published in the meantime the awaiter reads
2600 /// the truthful disconnected outcome and a late client reply is a harmless
2601 /// no-op; or (ii) this read follows the record removal — then THIS call
2602 /// observes the absence, rolls the slot back itself, and nothing was
2603 /// published. When both observe (a sweep and a rollback can both run in
2604 /// case (ii) if the insert also preceded the sweep), removal is idempotent
2605 /// and the cap is derived from map membership, so nothing double-releases.
2606 ///
2607 /// Lock discipline: `records` and `push_replies` are NEVER held together —
2608 /// here (`records` read, released, then `push_replies` on rollback), in
2609 /// `remove` (`records` removal, released, then the sweep), and everywhere
2610 /// else in this file the two mutexes are taken strictly sequentially, so no
2611 /// lock-order inversion is possible. This adds ZERO work to the connection
2612 /// slice path: the re-check runs on the push caller's thread only.
2613 pub(super) fn confirm_push_registration(&self, pid: u64, correlation_id: u64) -> bool {
2614 if self.is_registered(pid) {
2615 return true;
2616 }
2617 self.cancel_push(correlation_id);
2618 false
2619 }
2620
2621 /// Number of reserved push reply slots outstanding. A benign wait-quantum
2622 /// timeout must NOT change this (the slot survives); an explicit-deadline
2623 /// expiry, a consumed reply, and connection close each release exactly one.
2624 #[cfg(test)]
2625 pub(super) fn pending_push_count(&self) -> usize {
2626 recover_lock(&self.push_replies).len()
2627 }
2628
2629 /// Reserved push reply slots owned by connection `pid` — the exact quantity
2630 /// the §5 `max_pending_pushes_per_connection` cap counts (test instrument).
2631 #[cfg(test)]
2632 pub(super) fn pending_push_count_for(&self, pid: u64) -> usize {
2633 recover_lock(&self.push_replies)
2634 .values()
2635 .filter(|pending| pending.pid == pid)
2636 .count()
2637 }
2638
2639 /// Resolves the reply slot for `correlation_id` with the client's reply
2640 /// payload, waking the [`PushReplyAwaiter`]. Called by the connection process
2641 /// when a correlated `PushReply` frame arrives. A missing slot — already
2642 /// resolved, expired at its explicit deadline, dropped by connection close, or
2643 /// an unknown id — is a harmless no-op: a late `PushReply` for a slot that is
2644 /// gone is discarded here, never delivered and never a panic or desync.
2645 pub(super) fn resolve_push(&self, correlation_id: u64, payload: Vec<u8>) {
2646 // S4: delivery-plus-removal recovers a poisoned guard — dropping a real
2647 // reply (and stranding its slot) because an unrelated critical section
2648 // panicked would kill reclamation and exact cap accounting.
2649 let mut slots = recover_lock(&self.push_replies);
2650 if let Some(pending) = slots.remove(&correlation_id) {
2651 // The send stays under the registry lock so removal and delivery are
2652 // one atomic step: a timed-out awaiter that observes the slot gone
2653 // (its `cancel_push` returned false) is then GUARANTEED to find the
2654 // payload already in the channel — without this ordering the awaiter
2655 // could see the removal, find the channel still empty, and report a
2656 // timeout for a reply that was about to land. The send itself never
2657 // blocks (unbounded channel), and a receiver dropped after an
2658 // abandoned wait makes it a benign discard.
2659 pending.sender.send(payload).ok();
2660 }
2661 }
2662
2663 pub(super) const fn control_atom(&self) -> Atom {
2664 self.control_atom
2665 }
2666
2667 /// Sole registration path for a connection: the spawn thread inserts the
2668 /// record synchronously, before `spawn_connection` returns the handle, so
2669 /// `is_tracked`/`active_connection_count` reflect the connection
2670 /// immediately. The connection handler never writes the registry (it only
2671 /// reads via `mark_crashed`/`finish`), so there is a single writer here and
2672 /// no register/ensure-register race.
2673 ///
2674 /// Ordering note: `spawn_native` only enqueues the process, so its first
2675 /// slice may run on another worker thread before this insert lands. If that
2676 /// first slice exits immediately (e.g. a missing-stream crash) its
2677 /// `mark_crashed`/`finish` removes nothing and this insert then leaves a
2678 /// record for an already-dead pid. W4 leg 1 retires the per-accept
2679 /// `reap_crashed` scan that used to self-heal that orphan continuously;
2680 /// instead [`Self::reconcile_register_orphan`] closes the race with a SINGLE
2681 /// point check on the registration event itself — never a loop.
2682 /// `fd_guard` is `None` for a transport that owns no descriptor. The guard
2683 /// exists to keep an fd alive until readiness deregistration has been
2684 /// acknowledged; a loopback connection registers no readiness and holds no
2685 /// descriptor, so there is nothing to guard and the slot is honestly empty
2686 /// rather than filled with a placeholder.
2687 fn register_connection(
2688 &self,
2689 pid: u64,
2690 peer_addr: Option<SocketAddr>,
2691 connection_incarnation: Option<ConnectionIncarnation>,
2692 mount: MountKind,
2693 fd_guard: Option<TcpStream>,
2694 ) -> Result<(), ServerError> {
2695 self.register_record(pid, peer_addr, connection_incarnation, mount, fd_guard)?;
2696 self.reconcile_register_orphan(pid);
2697 Ok(())
2698 }
2699
2700 /// Closes the register-orphan race (see [`Self::register_with_fd`]) with one
2701 /// point check driven by the registration event — not a periodic scan. If
2702 /// the just-registered pid is already absent from the scheduler process
2703 /// table, its first slice has run and exited, so the record this
2704 /// registration inserted is an orphan the retiring reap scan used to sweep;
2705 /// reclaim it immediately through the ordinary `remove()` funnel. A pid still
2706 /// present is live and needs nothing here: a later external termination rides
2707 /// the exit-event reactor and an ordinary exit its own final slice.
2708 fn reconcile_register_orphan(&self, pid: u64) {
2709 let Some(scheduler) = self.scheduler.upgrade() else {
2710 return;
2711 };
2712 // The process-table lookup returns a sharded guard; bind only the
2713 // presence bool so the guard is released before `reclaim_terminated`
2714 // takes the connection registry lock (no cross-lock hold).
2715 let already_exited = scheduler.process_table().get(pid).is_none();
2716 if !already_exited {
2717 return;
2718 }
2719 let reason = scheduler
2720 .peek_exit_reason(pid)
2721 .unwrap_or(ExitReason::Normal);
2722 self.reclaim_terminated(pid, reason);
2723 }
2724
2725 /// TOLD reclamation of a connection whose process exited WITHOUT running a
2726 /// final handler slice — external/panic termination, where
2727 /// [`ConnectionProcess::Drop`] runs but no `mark_crashed`/`finish` does, and
2728 /// the register-orphan race above. Delivered the instant beamr publishes the
2729 /// process's [`ExitEvent`] (via [`run_reclaim_reactor`]) or at the
2730 /// registration point check, and routed through the SAME [`Self::remove`]
2731 /// funnel as every other teardown — no third funnel, no periodic scan.
2732 /// Idempotent: a record already removed in-slice, by the orphan reconcile, or
2733 /// by a duplicate delivery is a no-op here (remove returns `None`), so the §5
2734 /// admission gauge is released exactly once.
2735 fn reclaim_terminated(&self, pid: u64, reason: ExitReason) {
2736 let Some(record) = self.remove(pid) else {
2737 return;
2738 };
2739 self.fire_unregistered(pid, &record);
2740 tracing::warn!(
2741 connection_pid = pid,
2742 peer_addr = ?record.peer_addr,
2743 mount = ?record.mount,
2744 reason = ?reason,
2745 "connection process exited without a final slice; host record reclaimed by delivery"
2746 );
2747 }
2748
2749 /// One exit-event delivery: drain beamr's retained outcome (sole drainer,
2750 /// bounding its store) then reclaim through [`Self::reclaim_terminated`]. The
2751 /// only non-production element is the `#[cfg(test)]` reclamation gate, which
2752 /// is staged for at most one targeted pid and compiled out entirely in
2753 /// production — the delivery semantics are identical with or without it.
2754 fn deliver_reclamation(&self, scheduler: &Weak<Scheduler>, pid: u64, reason: ExitReason) {
2755 if let Some(scheduler) = scheduler.upgrade() {
2756 // The reason is already in-hand from the event; the drained outcome
2757 // is discarded, its purpose being only to bound beamr's store.
2758 drop(scheduler.take_exit_outcome(pid));
2759 }
2760 #[cfg(test)]
2761 let staged = self.stage_reclaim_barrier(pid);
2762 #[cfg(test)]
2763 if let Some(barrier) = staged.as_ref() {
2764 // Rendezvous: the pid is now dead but its record is still tracked —
2765 // the S8 reclamation window (oracle 26). Then wait for the harness to
2766 // release the reclaim.
2767 barrier.reached.wait();
2768 barrier.release.wait();
2769 }
2770 self.reclaim_terminated(pid, reason);
2771 #[cfg(test)]
2772 if let Some(barrier) = staged.as_ref() {
2773 // Signal the funnel completed so the harness can observe the record
2774 // gone without sampling.
2775 barrier.done.wait();
2776 }
2777 }
2778
2779 #[cfg(test)]
2780 fn register(&self, pid: u64, peer_addr: Option<SocketAddr>) -> Result<(), ServerError> {
2781 self.register_record(pid, peer_addr, None, MountKind::Tcp, None)
2782 }
2783
2784 fn register_record(
2785 &self,
2786 pid: u64,
2787 peer_addr: Option<SocketAddr>,
2788 connection_incarnation: Option<ConnectionIncarnation>,
2789 mount: MountKind,
2790 fd_guard: Option<TcpStream>,
2791 ) -> Result<(), ServerError> {
2792 match lock(&self.records, "connection registry")?.entry(pid) {
2793 // ENFORCED (not comment-only): pids are fresh per spawn and a record
2794 // is reclaimed through the single `remove` funnel before its pid can
2795 // recycle, so an occupied slot here is a supervision defect. Refuse
2796 // the whole registration rather than silently replacing — a replace
2797 // would drop the displaced record's `fd_guard` outside the teardown
2798 // funnel (orphaning a live connection's stream) and increment the
2799 // `liminal_connections_active` gauge a second time with no paired
2800 // decrement. The prior record stays intact; both spawn paths roll the
2801 // fresh process back on this error. Same idiom as
2802 // `StreamTable::insert` (entry-vacant → typed error) and the
2803 // fail-closed duplicate-conversation refusal in `apply.rs`.
2804 Entry::Occupied(_) => return Err(ServerError::ConnectionPidCollision { pid }),
2805 Entry::Vacant(entry) => {
2806 entry.insert(ConnectionRecord {
2807 peer_addr,
2808 mount,
2809 connection_incarnation,
2810 registration: None,
2811 readiness: None,
2812 ready_pending: Arc::new(AtomicBool::new(false)),
2813 parked: AtomicBool::new(false),
2814 fd_guard,
2815 });
2816 }
2817 }
2818 // Single-writer vacant-only insert (see doc above and the refusal arm)
2819 // pairs EXACTLY one gauge increment with the decrement in `remove`,
2820 // keeping `liminal_connections_active` equal to the live record count on
2821 // every teardown route; a refused duplicate increments nothing.
2822 crate::metrics::connection_spawned();
2823 Ok(())
2824 }
2825
2826 pub(super) fn mark_crashed(&self, pid: u64, reason: ExitReason, peer_addr: Option<SocketAddr>) {
2827 let removed = self.remove(pid);
2828 if let Some(record) = removed.as_ref() {
2829 self.fire_unregistered(pid, record);
2830 }
2831 let removed_peer_addr = removed
2832 .as_ref()
2833 .and_then(|record| record.peer_addr)
2834 .or(peer_addr);
2835 // The mount names the door this connection came through — the one fact
2836 // that tells a reader whether a crashed connection was a socket peer or
2837 // a co-resident caller, which `peer_addr: None` alone cannot.
2838 let mount = removed.as_ref().map(|record| record.mount);
2839 tracing::warn!(
2840 connection_pid = pid,
2841 peer_addr = ?removed_peer_addr,
2842 mount = ?mount,
2843 reason = ?reason,
2844 "connection process crashed"
2845 );
2846 }
2847
2848 /// Whether the spawn thread has installed the host record. A first native
2849 /// slice can win the enqueue-vs-record race and must remain runnable until it
2850 /// has somewhere host-reachable to publish its readiness token.
2851 pub(super) fn is_registered(&self, pid: u64) -> bool {
2852 self.contains(pid)
2853 }
2854
2855 /// Removes a token minted in-slice when publishing it to the host record fails.
2856 pub(super) fn deregister_unpublished_readiness(&self, token: ReadinessToken) {
2857 if let Some(scheduler) = self.scheduler.upgrade() {
2858 scheduler.readiness_deregister(token);
2859 }
2860 }
2861
2862 /// Cancels deadline timers detached by reply completion or connection close.
2863 pub(super) fn cancel_deadline_timers(&self, timers: Vec<TimerRef>) {
2864 let Some(scheduler) = self.scheduler.upgrade() else {
2865 return;
2866 };
2867 if let Ok(mut wheel) = scheduler.timers().lock() {
2868 for timer in timers {
2869 wheel.cancel(timer);
2870 }
2871 }
2872 }
2873
2874 pub(super) fn finish(&self, pid: u64) {
2875 if let Some(removed) = self.remove(pid) {
2876 self.fire_unregistered(pid, &removed);
2877 }
2878 }
2879
2880 /// Records the one readiness token minted for this connection. A live
2881 /// connection never re-registers: later parked slices rearm this identity.
2882 pub(super) fn set_readiness_token_once(
2883 &self,
2884 pid: u64,
2885 token: ReadinessToken,
2886 fd: RawFd,
2887 ) -> Result<(), ServerError> {
2888 let mut records = lock(&self.records, "connection registry")?;
2889 let record = records
2890 .get_mut(&pid)
2891 .ok_or_else(|| ServerError::ListenerAccept {
2892 message: format!("connection {pid} has no host record for readiness registration"),
2893 })?;
2894 if record.readiness.is_some() {
2895 return Err(ServerError::ListenerAccept {
2896 message: format!("connection {pid} attempted to replace its readiness token"),
2897 });
2898 }
2899 record.readiness = Some(ReadinessRegistration { token, fd });
2900 drop(records);
2901 Ok(())
2902 }
2903
2904 /// Invokes `on_worker_unregistered` for a removed connection record that
2905 /// carried a worker registration. A record with no registration (a plain
2906 /// connection, or a worker connection that never registered) is a no-op, so
2907 /// only connections that actually registered deregister.
2908 fn fire_unregistered(&self, pid: u64, record: &ConnectionRecord) {
2909 if record.registration.is_some() {
2910 if let Some(notifier) = self.notifier.as_ref() {
2911 notifier.on_worker_unregistered(pid);
2912 }
2913 }
2914 }
2915
2916 fn reap_crashed(&self, scheduler: &Scheduler) -> usize {
2917 let pids = match self.records.lock() {
2918 Ok(records) => records.keys().copied().collect::<Vec<_>>(),
2919 Err(error) => {
2920 tracing::warn!(%error, "connection registry unavailable during crash reap");
2921 return 0;
2922 }
2923 };
2924 let mut reaped = 0;
2925 for pid in pids {
2926 if scheduler.process_table().get(pid).is_none() {
2927 let removed = self.remove(pid);
2928 if let Some(record) = removed.as_ref() {
2929 self.fire_unregistered(pid, record);
2930 }
2931 let peer_addr = removed.and_then(|record| record.peer_addr);
2932 // This process exited without ever reaching `mark_crashed`/`finish`
2933 // (e.g. the beamr scheduler terminated it externally). W4 leg 1
2934 // retired this scan from the per-accept listener loop, and W4 leg 3
2935 // retired the shutdown-drain reconciliation that also drove it: the
2936 // reclaimer of these exits is now the TOLD exit-event reactor
2937 // ([`run_reclaim_reactor`]), which also composes drain completion
2938 // through the one `remove()` funnel. This scan now survives only as
2939 // that reactor's exit-event overflow (`Lagged`) recovery, driven a
2940 // bounded number of times, never periodically. beamr 0.15.4 exposes
2941 // a public, non-blocking
2942 // `peek_exit_reason` (and `take_exit_outcome`), so the real reason
2943 // IS recoverable here rather than logged as an opaque literal.
2944 let reason = scheduler.peek_exit_reason(pid);
2945 tracing::warn!(
2946 connection_pid = pid,
2947 ?peer_addr,
2948 ?reason,
2949 "connection process exited without a final slice; reclaimed by reconciliation"
2950 );
2951 reaped += 1;
2952 }
2953 }
2954 reaped
2955 }
2956
2957 fn contains(&self, pid: u64) -> bool {
2958 self.records
2959 .lock()
2960 .is_ok_and(|records| records.contains_key(&pid))
2961 }
2962
2963 #[cfg(test)]
2964 fn readiness_registration_count(&self) -> usize {
2965 self.records.lock().map_or(0, |records| {
2966 records
2967 .values()
2968 .filter(|record| record.readiness.is_some())
2969 .count()
2970 })
2971 }
2972
2973 #[cfg(test)]
2974 fn readiness_fd(&self, pid: u64) -> Option<RawFd> {
2975 self.records
2976 .lock()
2977 .ok()?
2978 .get(&pid)
2979 .and_then(|record| record.readiness.map(|registration| registration.fd))
2980 }
2981
2982 #[cfg(test)]
2983 fn readiness_token(&self, pid: u64) -> Option<ReadinessToken> {
2984 self.records
2985 .lock()
2986 .ok()?
2987 .get(&pid)
2988 .and_then(|record| record.readiness.map(|registration| registration.token))
2989 }
2990
2991 fn active_connections(&self) -> Vec<ActiveConnection> {
2992 self.records.lock().map_or_else(
2993 |_| Vec::new(),
2994 |records| {
2995 records
2996 .iter()
2997 .map(|(&pid, record)| ActiveConnection {
2998 pid,
2999 peer_addr: record.peer_addr,
3000 })
3001 .collect()
3002 },
3003 )
3004 }
3005
3006 /// Reads the complete active-connection incarnation set under the
3007 /// registry lock — the bounded defense-in-depth collision-skip input to
3008 /// incarnation allocation (uniqueness itself comes from allocator-log
3009 /// monotonicity; see `allocate_connection_incarnation`). Poisoning fails
3010 /// admission closed: treating an unreadable registry as empty would
3011 /// silently drop the defense layer.
3012 fn complete_active_incarnation_references(
3013 &self,
3014 ) -> Result<Vec<ConnectionIncarnation>, ServerError> {
3015 Ok(
3016 lock(&self.records, "connection incarnation reference registry")?
3017 .values()
3018 .filter_map(|record| record.connection_incarnation)
3019 .collect(),
3020 )
3021 }
3022
3023 fn push_control(&self, pid: u64, control: ConnectionControl) -> Result<(), ServerError> {
3024 lock(&self.controls, "connection control queue")?
3025 .push(QueuedConnectionControl { pid, control });
3026 Ok(())
3027 }
3028
3029 pub(super) fn pop_control(&self, pid: u64) -> Option<ConnectionControl> {
3030 let mut controls = self.controls.lock().ok()?;
3031 let index = controls.iter().position(|queued| queued.pid == pid)?;
3032 Some(controls.remove(index).control)
3033 }
3034
3035 /// Non-consuming final-probe query for controls enqueued after mailbox drain.
3036 pub(super) fn has_control(&self, pid: u64) -> bool {
3037 self.controls
3038 .lock()
3039 .is_ok_and(|controls| controls.iter().any(|queued| queued.pid == pid))
3040 }
3041
3042 /// Pulls a queued-but-unconsumed control back out of the queue. Returns
3043 /// whether THIS call removed it — `false` means the entry already left the
3044 /// queue, and since [`Self::pop_control`] is the only other remover, a
3045 /// consumer drain took it (S8's publication disambiguator). Matching is
3046 /// `pid` + full control equality; a `Push` control embeds its
3047 /// runtime-unique correlation id, so this can never remove a different
3048 /// push's entry and misreport.
3049 fn remove_control(&self, pid: u64, control: &ConnectionControl) -> bool {
3050 let Ok(mut controls) = self.controls.lock() else {
3051 return false;
3052 };
3053 let Some(index) = controls
3054 .iter()
3055 .position(|queued| queued.pid == pid && &queued.control == control)
3056 else {
3057 return false;
3058 };
3059 controls.remove(index);
3060 true
3061 }
3062
3063 fn active_count(&self) -> usize {
3064 self.records.lock().map_or(0, |records| records.len())
3065 }
3066
3067 /// Test read of the record's stamped mount (design §10).
3068 #[cfg(test)]
3069 fn connection_mount(&self, pid: u64) -> Option<MountKind> {
3070 self.records
3071 .lock()
3072 .ok()?
3073 .get(&pid)
3074 .map(|record| record.mount)
3075 }
3076
3077 /// Test read of whether the record holds an fd guard.
3078 #[cfg(test)]
3079 fn connection_has_fd_guard(&self, pid: u64) -> Option<bool> {
3080 self.records
3081 .lock()
3082 .ok()?
3083 .get(&pid)
3084 .map(|record| record.fd_guard.is_some())
3085 }
3086
3087 /// Removes the connection record for `pid` and, in the same close step, drops
3088 /// every push reply slot that connection still owns so each waiting
3089 /// [`PushReplyAwaiter`] wakes immediately with a disconnected error. This runs
3090 /// on every close route — `finish`, `mark_crashed`, and `reap_crashed` all
3091 /// remove through here — and fires regardless of whether the connection ever
3092 /// registered a worker, so a plain push target is covered too.
3093 ///
3094 /// ORDER MATTERS (S3/S7): the record is removed BEFORE the push sweep. A
3095 /// push registering concurrently runs INSERT -> CONFIRM -> PUBLISH
3096 /// (`confirm_push_registration` reads the record after inserting its slot
3097 /// and before publishing its control), so with this ordering exactly one
3098 /// side always observes a racing slot: a confirm that ran before this
3099 /// removal implies the slot was inserted before the sweep below (which then
3100 /// reaps it — a control published after that confirm is answered into a
3101 /// swept slot, read as the truthful disconnected outcome); a confirm after
3102 /// this removal sees the absence, rolls the slot back itself, and never
3103 /// publishes. Sweeping first (the original order) left a window — sweep,
3104 /// then insert+confirm, then record removal — where NEITHER side observed
3105 /// the slot and it leaked past connection close. The two locks are taken
3106 /// strictly sequentially (never nested), so no lock-order inversion.
3107 fn remove(&self, pid: u64) -> Option<ConnectionRecord> {
3108 let mut removed = self
3109 .records
3110 .lock()
3111 .ok()
3112 .and_then(|mut records| records.remove(&pid));
3113 self.cancel_pushes_for_connection(pid);
3114 if let Some(registration) = removed.as_mut().and_then(|record| record.readiness.take()) {
3115 if let Some(scheduler) = self.scheduler.upgrade() {
3116 // This call is ACK'd: it returns only after the poll owner has
3117 // removed the registration. `fd_guard` is still live here.
3118 scheduler.readiness_deregister(registration.token);
3119 tracing::debug!(
3120 registered_fd = registration.fd,
3121 "connection readiness deregistration acknowledged"
3122 );
3123 }
3124 }
3125 // Decrement only when a record was actually present so a double-remove
3126 // (e.g. `finish` after `reap_crashed`) cannot drive the gauge negative.
3127 // The §5 admission slot is released on the same guard: the reservation
3128 // acquired in `spawn_connection` converted into this record at
3129 // `register`, so its removal is exactly one release per reservation.
3130 if removed.is_some() {
3131 crate::metrics::connection_closed();
3132 self.release_admission();
3133 }
3134 if let Some(record) = removed.as_mut() {
3135 // Explicit after-deregister drop documents and enforces the fd wall.
3136 drop(record.fd_guard.take());
3137 }
3138 // TOLD drain-completion tell (W4 leg 3, §4.3): the record map above already
3139 // reflects this removal, so bump the removal generation and wake the
3140 // drain/settle waiter AFTER the observed state is updated. Ordering the
3141 // state update before the generation bump — paired with the waiter arming
3142 // its snapshot before observing `active_count` — is the arm-before-observe
3143 // barrier that makes a concurrently delivered exit un-losable (oracle 18).
3144 // Only a real removal tells, so a double-remove drives no spurious wake.
3145 if removed.is_some() {
3146 self.signal_connection_removed();
3147 }
3148 removed
3149 }
3150
3151 /// Bumps the drain-completion generation and wakes the drain/settle waiter.
3152 /// Called from [`Self::remove`] on every route that actually drops a record.
3153 fn signal_connection_removed(&self) {
3154 // Bump the generation under the lock, release it, THEN notify. A waiter
3155 // holds this lock continuously from its generation re-check through the
3156 // atomic release inside `wait_timeout`, so it can never miss a bump
3157 // published before the notify — the Condvar lost-wakeup contract holds
3158 // without notifying under the guard.
3159 {
3160 let mut generation = recover_lock(&self.drain_generation);
3161 *generation = generation.wrapping_add(1);
3162 }
3163 self.drain_removed.notify_all();
3164 }
3165
3166 /// Parks the calling thread until every tracked connection has been removed
3167 /// or `deadline` elapses, returning `true` when the drain completed and
3168 /// `false` when the single admitted deadline won. This is the TOLD
3169 /// replacement (W4 leg 3, §4.3) for the retired reap/count/sleep drain loop:
3170 /// it never samples completion on a timer. Completion is observed only on a
3171 /// delivered connection-removal wake (composed from the one `remove()` funnel,
3172 /// so a Died/Detached/crash exit and an orderly close decrement through the
3173 /// same path — oracle 15) or the one deadline. Force-close settle reuses this
3174 /// same waiter with its own deadline rather than a second poll loop (oracle
3175 /// 14).
3176 pub(super) fn wait_for_active_connections_drained(&self, deadline: Instant) -> bool {
3177 loop {
3178 // ARM before OBSERVE: snapshot the removal generation first, so an exit
3179 // delivered after the completion observation below and before the park
3180 // bumps a generation the park detects — it is never lost (oracle 18).
3181 let snapshot = self.drain_generation_snapshot();
3182 // OBSERVE completion first: a last exit that reaches zero wins a tie
3183 // with a simultaneously elapsed deadline (oracle 19).
3184 if self.active_count() == 0 {
3185 return true;
3186 }
3187 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
3188 #[cfg(test)]
3189 self.drain_deadline_hits.fetch_add(1, Ordering::SeqCst);
3190 return false;
3191 };
3192 #[cfg(test)]
3193 self.run_drain_park_barrier();
3194 self.park_until_removed_or(snapshot, remaining);
3195 }
3196 }
3197
3198 /// Reads the current removal generation under its mutex.
3199 fn drain_generation_snapshot(&self) -> u64 {
3200 *recover_lock(&self.drain_generation)
3201 }
3202
3203 /// Parks on the removal `Condvar` for at most `timeout`, but only while no
3204 /// removal has bumped the generation since `snapshot` — the arm-before-observe
3205 /// barrier. `wait_timeout_while` evaluates the predicate under the lock BEFORE
3206 /// waiting, so a generation already advanced (an exit landed between the
3207 /// observation and here) returns immediately without sleeping; spurious wakes
3208 /// re-wait inside the call and never return early.
3209 fn park_until_removed_or(&self, snapshot: u64, timeout: Duration) {
3210 let outcome = self
3211 .drain_removed
3212 .wait_timeout_while(recover_lock(&self.drain_generation), timeout, |current| {
3213 *current == snapshot
3214 })
3215 .unwrap_or_else(PoisonError::into_inner);
3216 // A non-timed-out return means the predicate went false — a real removal
3217 // bumped the generation and woke this park (as opposed to the deadline).
3218 #[cfg(test)]
3219 if !outcome.1.timed_out() {
3220 self.drain_exit_wakes.fetch_add(1, Ordering::SeqCst);
3221 }
3222 // Release the guard promptly; the caller re-loops unlocked.
3223 drop(outcome);
3224 }
3225}
3226
3227/// One in-flight server-push reply slot, associating the awaiter's reply `sender`
3228/// with the `pid` of the connection that owns the push. The pid lets the close
3229/// path drop exactly that connection's slots; the correlation id (the map key)
3230/// still drives [`ConnectionRuntime::resolve_push`] and
3231/// [`ConnectionRuntime::cancel_push`].
3232#[derive(Debug)]
3233struct PendingPush {
3234 pid: u64,
3235 sender: Sender<Vec<u8>>,
3236 /// Absolute reply deadline for this push, when one was requested via
3237 /// [`ConnectionSupervisor::push_to_connection_with_deadline`]. `None` is the
3238 /// default 0.2.3 shape: the slot has no per-slot deadline and is reclaimed
3239 /// only by reply-consumed or connection-close. `Some` is evaluated host-side
3240 /// and lazily in [`ConnectionRuntime::expire_push_if_due`].
3241 deadline: Option<Instant>,
3242}
3243
3244/// Host-side disposition of a reply slot at an elapsed `receive` quantum.
3245enum PushSlotDisposition {
3246 /// The slot carried an explicit deadline that has passed; this call removed
3247 /// it (releasing its §5 cap admission).
3248 Expired,
3249 /// The slot is present with no deadline, or a deadline still in the future:
3250 /// the elapsed quantum is a benign re-arm and the slot is untouched.
3251 Live,
3252 /// No slot for this correlation id — a concurrent resolve or connection close
3253 /// already removed it.
3254 Absent,
3255}
3256
3257#[derive(Debug)]
3258struct ConnectionRecord {
3259 peer_addr: Option<SocketAddr>,
3260 /// Which door admitted this connection (design §10). Written once, by the
3261 /// spawn path, from the server's own knowledge of which path it is; never
3262 /// read from, or influenced by, anything the client sends.
3263 mount: MountKind,
3264 /// Durable pair allocated and flushed before the process was spawned.
3265 connection_incarnation: Option<ConnectionIncarnation>,
3266 /// Worker registration declared on this connection, set by `set_registration`
3267 /// when a `WorkerRegister` frame is accepted. `Some` marks a connection whose
3268 /// close must fire `on_worker_unregistered`.
3269 registration: Option<WorkerRegistration>,
3270 /// Host-reachable identity for ACK'd deregistration after external death.
3271 readiness: Option<ReadinessRegistration>,
3272 /// Shared edge set before READY enters beamr's process-table pending queue;
3273 /// the executing process reads it at its final probe.
3274 ready_pending: Arc<AtomicBool>,
3275 /// FIX A-ii: `true` while this connection is parked (its last slice returned
3276 /// `Wait`). A parked connection with no `ready_pending` edge has fanned out
3277 /// every accepted publish to its socket, so the shutdown flush barrier reads
3278 /// this with `ready_pending` to know delivery has quiesced before it lets the
3279 /// shutdown Disconnect be broadcast.
3280 parked: AtomicBool,
3281 /// Keeps the fd alive until deregistration has been acknowledged, preventing
3282 /// stale registration delivery to a subsequently reused descriptor number.
3283 fd_guard: Option<TcpStream>,
3284}
3285
3286#[derive(Debug, Clone, Copy)]
3287struct ReadinessRegistration {
3288 token: ReadinessToken,
3289 fd: RawFd,
3290}
3291
3292#[derive(Debug, Clone, PartialEq, Eq)]
3293struct QueuedConnectionControl {
3294 pid: u64,
3295 control: ConnectionControl,
3296}
3297
3298fn lock<'a, T>(mutex: &'a Mutex<T>, context: &str) -> Result<MutexGuard<'a, T>, ServerError> {
3299 mutex.lock().map_err(|error| ServerError::ListenerAccept {
3300 message: format!("{context} unavailable: {error}"),
3301 })
3302}
3303
3304/// Locks `mutex`, RECOVERING a poisoned guard instead of failing (S4). For
3305/// lifecycle-cleanup paths only (reply delivery, expiry, cancellation, the
3306/// close sweep): removal-style operations are sound on a recovered map, and a
3307/// cleanup that silently skipped its removal would strand slots and their §5
3308/// cap admissions forever. Admission paths keep the fail-closed [`lock`].
3309fn recover_lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
3310 mutex
3311 .lock()
3312 .unwrap_or_else(std::sync::PoisonError::into_inner)
3313}