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