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