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