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