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