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