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