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