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