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