Skip to main content

subc_daemon/
forwarding.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    error::Error,
4    fmt,
5    sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard},
6    time::Duration,
7};
8
9use subc_control::{ClientControlResponse, RouteCloseReason};
10use subc_protocol::{
11    manifest::Concurrency,
12    session::{LiveRoot, ModuleControlResponse, ModuleControlResponseToModule},
13    ErrorBody, Flags, FrameType, Principal, Priority,
14};
15use tokio::sync::{mpsc, oneshot, Semaphore};
16use tokio::time::Instant;
17use tracing::{debug, info, warn};
18
19use crate::{
20    control::{RouteBindBreakers, RouteBindConcurrency},
21    observability::DaemonCounters,
22    registry::ConnectionId,
23    router::FrameSink,
24    Frame, ProjectRootId,
25};
26
27/// Default per-channel request-credit window for modules that schedule internally.
28const DEFAULT_MODULE_MANAGED_WINDOW: usize = 32;
29
30/// High per-channel cap for stateless modules; this is an OOM guard, not scheduling policy.
31const STATELESS_PARALLEL_WINDOW: usize = 1024;
32
33/// A stopped probe cycle cannot retain its last unanswered correlation forever.
34/// Active endpoints replace the tombstone on their next serial health probe;
35/// this backstop covers endpoints that stop probing altogether.
36const HEALTH_PROBE_TOMBSTONE_TTL: Duration = Duration::from_secs(5 * 60);
37
38/// Module connection identity used in forwarding keys.
39///
40/// The generation is bumped every time a module connection is registered so a future restart cannot
41/// accidentally reuse a stale `(connection_id, route_channel)` binding.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct ModuleEndpointId {
44    pub connection_id: ConnectionId,
45    pub generation: u64,
46}
47
48/// Client-local route key. A route channel is unique only within one client connection.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub(crate) struct ClientRouteKey {
51    pub connection_id: ConnectionId,
52    pub channel: u16,
53}
54
55/// Module-local route key. A route channel is unique only within one live module endpoint.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub(crate) struct ModuleRouteKey {
58    pub endpoint: ModuleEndpointId,
59    pub channel: u16,
60}
61
62#[derive(Debug)]
63pub(crate) struct RouteBinding {
64    pub client_connection_id: ConnectionId,
65    pub client_sink: FrameSink,
66    pub client_negotiated_ver: u8,
67    pub client_channel: u16,
68    pub client_epoch: u32,
69    pub module_id: String,
70    pub module_endpoint: ModuleEndpointId,
71    pub module_sink: FrameSink,
72    pub module_negotiated_ver: u8,
73    pub module_channel: u16,
74    pub module_epoch: u32,
75    pub principal: Principal,
76    pub project_root: Option<ProjectRootId>,
77    pub bound_at: Instant,
78    pub flow: Arc<ChannelFlow>,
79}
80
81#[derive(Debug, Clone)]
82pub(crate) enum DataRoute {
83    Client(DataRouteState),
84    Module(DataRouteState),
85}
86
87#[derive(Debug, Clone)]
88pub(crate) enum DataRouteState {
89    Bound(Arc<RouteBinding>),
90    Reserved,
91    EpochMismatch,
92    Absent,
93}
94
95/// Which kind of peer a route GOODBYE is being delivered to. This decides what
96/// happens when the GOODBYE cannot be enqueued (egress full/closed):
97/// - `Client`: escalate to closing that client connection (a socket close is a
98///   stronger teardown signal, and a full client egress means it is the slow
99///   client we would drop anyway).
100/// - `Module`: best-effort DROP, never close. A client-disconnect notifies the
101///   SHARED module that one client's route is gone; closing the module on its
102///   egress backpressure would tear down every co-tenant client (the exact
103///   cross-tenant blast radius this never-close rule exists to prevent — observed when a
104///   flooding dead client filled BOTH its own and the module's egress, so its
105///   route-gone GOODBYE to the module failed and closed the shared connection).
106///   subc has already removed the route from its forwarding state and drops
107///   stale module frames for the released channel (see router.rs), so subc's
108///   own routing is correct. The residual: under SUSTAINED module-egress
109///   backpressure a module-targeted route-gone notification can be lost, which a
110///   module using it for client-refcounting (e.g. AFT's session accounting)
111///   would miss. This is INTENTIONALLY ACCEPTED, not a gap. A consuming module
112///   must bound stale bindings with its own idle-activity reaper (last-touched
113///   TTL) independent of route-gone signals — AFT does exactly this, so a lost
114///   GOODBYE degrades to "the
115///   binding stays warm until its idle TTL" (bounded wasted resources), never an
116///   unbounded leak; disk-durable replay is unaffected. A dedicated reliable
117///   module control lane was evaluated and deliberately NOT
118///   built: it would add starvation-avoidance machinery to the thin core for a
119///   bounded warm-resource window that is not a correctness issue. never-close
120///   is the invariant that matters here.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub(crate) enum GoodbyeTargetKind {
123    Client,
124    Module,
125}
126
127#[derive(Debug, Clone)]
128pub(crate) struct GoodbyeTarget {
129    pub connection_id: ConnectionId,
130    pub sink: FrameSink,
131    pub negotiated_ver: u8,
132    pub channel: u16,
133    pub epoch: u32,
134    pub kind: GoodbyeTargetKind,
135    /// The receiving module when this is a module-targeted relay. Client targets
136    /// have no module owner, while this identifier attributes a dropped relay.
137    pub module_id: Option<String>,
138}
139
140impl GoodbyeTarget {
141    /// True only when an undeliverable GOODBYE should escalate to closing the
142    /// target connection. Never escalate for module recipients.
143    pub(crate) fn close_on_delivery_failure(&self) -> bool {
144        matches!(self.kind, GoodbyeTargetKind::Client)
145    }
146}
147
148/// One route currently served by a module endpoint.
149///
150/// `goodbye_target` is deliberately retained alongside the census projection so
151/// a draining caller can address exactly the same route set that this read
152/// reports, without a second forwarding-table pass.
153#[derive(Debug, Clone)]
154pub(crate) struct EndpointRoute {
155    pub goodbye_target: GoodbyeTarget,
156    pub principal: Principal,
157    pub bound_at: Instant,
158    pub draining: bool,
159    /// WHY the endpoint is draining, when it is. Carried per-route so the
160    /// census can answer "closing because of what" without a second lookup;
161    /// `None` exactly when `draining` is false (one source: the drain map).
162    pub drain_reason: Option<RouteCloseReason>,
163}
164
165#[derive(Debug)]
166pub(crate) struct PendingRouteBindRelay {
167    pub endpoint: ModuleEndpointId,
168    pub module_sink: FrameSink,
169    pub negotiated_ver: u8,
170    pub client_channel: u16,
171    pub client_epoch: u32,
172    pub module_channel: u16,
173    pub module_epoch: u32,
174    pub corr: u64,
175    pub receiver: oneshot::Receiver<RouteBindRelayOutcome>,
176}
177
178#[derive(Debug, Clone)]
179pub(crate) struct ModuleDrainTarget {
180    pub endpoint: ModuleEndpointId,
181    pub sink: FrameSink,
182    pub negotiated_ver: u8,
183    pub abandoned_bindings: Vec<GoodbyeTarget>,
184    pub excluded_subscriptions: u32,
185}
186
187#[derive(Debug, Clone)]
188pub(crate) enum RouteBindRelayOutcome {
189    Accepted,
190    Rejected(ErrorBody),
191    ModuleGone(String),
192}
193
194/// What [`ForwardingTable::cutover_candidate`] did.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub(crate) struct ForwardingCutover {
197    /// The endpoint now in the active slot (the former candidate).
198    pub promoted: ModuleEndpointId,
199    /// The endpoint demoted out of the active slot, to be drained by endpoint.
200    /// `None` when the incumbent's connection was already gone.
201    pub incumbent: Option<ModuleEndpointId>,
202}
203
204#[derive(Debug, Clone)]
205pub(crate) struct PendingRelayCompletion {
206    pub settled: bool,
207    pub abandoned: Option<GoodbyeTarget>,
208}
209
210#[derive(Debug)]
211pub(crate) struct PendingModuleControlRpc {
212    pub endpoint: ModuleEndpointId,
213    pub module_sink: FrameSink,
214    pub negotiated_ver: u8,
215    pub corr: u64,
216    pub receiver: oneshot::Receiver<ModuleControlRpcOutcome>,
217}
218
219#[derive(Debug, Clone)]
220pub(crate) enum ModuleControlRpcOutcome {
221    Response(ModuleControlResponse),
222    Rejected(ErrorBody),
223    ModuleGone(String),
224    MalformedResponse(String),
225    UnexpectedOp { expected: String, actual: String },
226    DeadlineElapsed,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub(crate) enum ModuleControlRpcCompletion {
231    Unknown,
232    Settled,
233    LateHealthAnswer {
234        module_id: String,
235        latency: Duration,
236    },
237}
238
239#[derive(Debug)]
240struct PendingModuleControlRpcEntry {
241    expected_op: String,
242    deadline: Instant,
243    health_probe_started_at: Option<Instant>,
244    sender: oneshot::Sender<ModuleControlRpcOutcome>,
245}
246
247#[derive(Debug)]
248struct HealthProbeTombstone {
249    expected_op: String,
250    module_id: String,
251    probe_started_at: Instant,
252    expires_at: Instant,
253}
254
255#[derive(Debug, Clone)]
256struct RouteReservation {
257    client_key: ClientRouteKey,
258    module_key: ModuleRouteKey,
259    client_epoch: u32,
260    module_epoch: u32,
261    project_root: Option<ProjectRootId>,
262}
263
264#[derive(Debug)]
265struct PendingRouteBindRelayEntry {
266    reservation: RouteReservation,
267    client_sink: FrameSink,
268    client_negotiated_ver: u8,
269    client_permit: mpsc::OwnedPermit<crate::router::OutboundFrame>,
270    route_open_frame: Frame,
271    principal: Principal,
272    deadline: Instant,
273    relay_enqueued: bool,
274    sender: oneshot::Sender<RouteBindRelayOutcome>,
275}
276
277#[derive(Debug, Clone)]
278pub(crate) enum RouteRelease {
279    Removed(GoodbyeTarget),
280    Stale,
281    Absent,
282}
283
284#[derive(Debug, Clone)]
285pub(crate) enum RoutePollSnapshot {
286    Bound {
287        module_id: String,
288        status: Option<String>,
289    },
290    Absent,
291}
292
293#[derive(Debug, Clone)]
294struct ModuleConnection {
295    endpoint: ModuleEndpointId,
296    sink: FrameSink,
297    negotiated_ver: u8,
298    concurrency: Concurrency,
299}
300
301#[derive(Debug, Default)]
302struct ForwardingInner {
303    daemon_draining: bool,
304    /// The ACTIVE slot: the one endpoint per module id that routing resolves.
305    /// Every by-id lookup (relay reservation, drain-by-id, liveness, census,
306    /// live roots, module-control RPCs) reads this map and nothing else.
307    modules_by_id: HashMap<String, ModuleConnection>,
308    /// The CANDIDATE slot of a blue/green swap: a second process registered
309    /// under an id that already has an active endpoint. It has a full endpoint
310    /// identity (so its own connection can be looked up and torn down) but no
311    /// by-id lookup sees it, so nothing is routed to it until `cutover_candidate`
312    /// promotes it.
313    candidates_by_id: HashMap<String, ModuleConnection>,
314    /// Former active endpoints demoted by `cutover_candidate`, until their
315    /// connection is removed. Membership is what distinguishes an endpoint that
316    /// was SUPERSEDED by a promotion (still alive, still carrying bound routes,
317    /// about to be drained) from one that is merely STALE (replaced some other
318    /// way, which is treated as a fault on the acking connection).
319    superseded_endpoints: HashMap<ModuleEndpointId, ModuleConnection>,
320    endpoint_by_connection: HashMap<ConnectionId, ModuleEndpointId>,
321    module_id_by_endpoint: HashMap<ModuleEndpointId, String>,
322    /// Endpoints mid-drain, keyed to the reason the drain was begun with. The
323    /// value serves the census ("draining because restart"); membership alone
324    /// still answers every admission-gate check.
325    draining_endpoints: HashMap<ModuleEndpointId, RouteCloseReason>,
326    closing_connections: HashSet<ConnectionId>,
327    next_generation: u64,
328    reserved_client: HashMap<ClientRouteKey, ModuleRouteKey>,
329    reserved_module: HashMap<ModuleRouteKey, ClientRouteKey>,
330    next_client_channel: HashMap<ConnectionId, u16>,
331    next_module_channel: HashMap<ModuleEndpointId, u16>,
332    client_slot_epochs: HashMap<ClientRouteKey, u32>,
333    module_slot_epochs: HashMap<ModuleRouteKey, u32>,
334    last_published_epoch: HashMap<ClientRouteKey, u32>,
335    client_to_module: HashMap<ClientRouteKey, Arc<RouteBinding>>,
336    module_to_client: HashMap<ModuleRouteKey, Arc<RouteBinding>>,
337    status: HashMap<(ClientRouteKey, u32), String>,
338    pending_relays: HashMap<(ModuleEndpointId, u64), PendingRouteBindRelayEntry>,
339    next_control_corr: HashMap<ModuleEndpointId, u64>,
340    pending_control_rpcs: HashMap<(ModuleEndpointId, u64), PendingModuleControlRpcEntry>,
341    health_probe_tombstones: HashMap<(ModuleEndpointId, u64), HealthProbeTombstone>,
342}
343
344#[derive(Debug, Clone)]
345pub(crate) struct CloseReason {
346    code: &'static str,
347    message: String,
348}
349
350impl CloseReason {
351    pub(crate) fn new(code: &'static str, message: impl Into<String>) -> Self {
352        Self {
353            code,
354            message: message.into(),
355        }
356    }
357}
358
359impl fmt::Display for CloseReason {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        write!(f, "{}: {}", self.code, self.message)
362    }
363}
364
365pub(crate) type ConnectionCloseReceiver = oneshot::Receiver<CloseReason>;
366
367/// Dynamic forwarding state shared by the control plane and data-plane router.
368#[derive(Debug, Default)]
369pub struct ForwardingTable {
370    inner: Arc<RwLock<ForwardingInner>>,
371    close_registry: Mutex<HashMap<ConnectionId, oneshot::Sender<CloseReason>>>,
372    counters: DaemonCounters,
373    /// Per-target-module bind-relay breaker state. It lives here, beside the
374    /// module connections it describes, because this is where a module
375    /// connection's identity is established and therefore where a stale
376    /// verdict has to be discarded.
377    route_bind_breakers: RouteBindBreakers,
378    /// Current route.bind relays keyed by target module. Admission is shared
379    /// across every client connection that points at the same endpoint.
380    route_bind_concurrency: RouteBindConcurrency,
381}
382
383impl ForwardingTable {
384    pub(crate) fn counters(&self) -> DaemonCounters {
385        self.counters.clone()
386    }
387
388    pub(crate) fn route_bind_breakers(&self) -> RouteBindBreakers {
389        self.route_bind_breakers.clone()
390    }
391
392    pub(crate) fn route_bind_concurrency(&self) -> RouteBindConcurrency {
393        self.route_bind_concurrency.clone()
394    }
395
396    pub(crate) fn register_connection_close(
397        &self,
398        connection_id: ConnectionId,
399    ) -> ConnectionCloseReceiver {
400        let (sender, receiver) = oneshot::channel();
401        let replaced = self
402            .lock_close_registry()
403            .insert(connection_id, sender)
404            .is_some();
405        if replaced {
406            warn!(
407                connection_id = connection_id.get(),
408                "replaced existing connection close registration"
409            );
410        }
411        receiver
412    }
413
414    pub(crate) fn unregister_connection_close(&self, connection_id: ConnectionId) {
415        self.lock_close_registry().remove(&connection_id);
416    }
417
418    /// Close every established connection, modules and clients alike, as the
419    /// last step of an announced daemon shutdown. A module learns the daemon is
420    /// gone by EOF on its control connection; delivering that EOF while the
421    /// daemon is still alive lets the daemon wait for the modules' own
422    /// teardowns before it ends whatever is left.
423    #[cfg(unix)]
424    pub(crate) fn close_all_connections(&self, reason: &CloseReason) -> usize {
425        let senders: Vec<_> = self.lock_close_registry().drain().collect();
426        let count = senders.len();
427        for (_, sender) in senders {
428            let _ = sender.send(reason.clone());
429        }
430        count
431    }
432
433    pub(crate) fn request_connection_close(
434        &self,
435        connection_id: ConnectionId,
436        reason: CloseReason,
437    ) {
438        let sender = self.lock_close_registry().remove(&connection_id);
439        if let Some(sender) = sender {
440            debug!(
441                connection_id = connection_id.get(),
442                close_reason = %reason,
443                "requesting connection close"
444            );
445            let _ = sender.send(reason);
446        } else {
447            debug!(
448                connection_id = connection_id.get(),
449                close_reason = %reason,
450                "connection close request ignored for inactive connection"
451            );
452        }
453    }
454
455    pub fn register_module_connection(
456        &self,
457        connection_id: ConnectionId,
458        module_id: String,
459        negotiated_ver: u8,
460        concurrency: Concurrency,
461        sink: FrameSink,
462    ) -> Result<ModuleEndpointId, ForwardingError> {
463        let mut inner = self.write_inner()?;
464        if inner.daemon_draining || inner.closing_connections.contains(&connection_id) {
465            return Err(ForwardingError::ConnectionClosing { connection_id });
466        }
467        if let Some(old_endpoint) = inner.endpoint_by_connection.remove(&connection_id) {
468            let _ = remove_module_connection_locked(&mut inner, old_endpoint);
469        }
470
471        inner.next_generation = inner.next_generation.checked_add(1).unwrap_or(1);
472        let endpoint = ModuleEndpointId {
473            connection_id,
474            generation: inner.next_generation,
475        };
476        inner.endpoint_by_connection.insert(connection_id, endpoint);
477        inner
478            .module_id_by_endpoint
479            .insert(endpoint, module_id.clone());
480        inner.next_module_channel.insert(endpoint, 1);
481        inner.next_control_corr.insert(endpoint, 1);
482        inner.modules_by_id.insert(
483            module_id.clone(),
484            ModuleConnection {
485                endpoint,
486                sink,
487                negotiated_ver,
488                concurrency,
489            },
490        );
491        drop(inner);
492
493        // A new module connection has arrived under this id, so anything the
494        // bind-relay breaker learned was learned about a process that is no
495        // longer the one behind this name. See
496        // `RouteBindBreakers::reset_for_new_module_connection`.
497        //
498        // Keyed on ARRIVAL rather than on teardown deliberately: a connection
499        // going away is not evidence about anything, and a module whose
500        // connection drops without coming back should keep its verdict until
501        // something actually registers in its place. This also covers the
502        // unclean replacements -- a module killed mid-bind, or one whose
503        // connection was closing -- because registration is the single path by
504        // which any module connection becomes usable.
505        if let Some(discarded) = self
506            .route_bind_breakers
507            .reset_for_new_module_connection(&module_id)
508        {
509            info!(
510                module_id = %module_id,
511                discarded_consecutive_timeouts = discarded,
512                "route.bind breaker state discarded: a new module connection replaced the process it described"
513            );
514        }
515        Ok(endpoint)
516    }
517
518    /// Register a blue/green swap candidate for `module_id` into the candidate
519    /// slot, alongside whatever endpoint is active for the id.
520    ///
521    /// Unlike [`Self::register_module_connection`], this never touches the
522    /// active slot and never resets the bind-relay breaker: the incumbent is
523    /// still the process serving the id, so what the breaker learned about it
524    /// is still true. The breaker is reset at cutover instead, when the process
525    /// behind the name actually changes. A second candidate for the same id is
526    /// refused.
527    pub(crate) fn register_candidate_module_connection(
528        &self,
529        connection_id: ConnectionId,
530        module_id: String,
531        negotiated_ver: u8,
532        concurrency: Concurrency,
533        sink: FrameSink,
534    ) -> Result<ModuleEndpointId, ForwardingError> {
535        let mut inner = self.write_inner()?;
536        if inner.daemon_draining || inner.closing_connections.contains(&connection_id) {
537            return Err(ForwardingError::ConnectionClosing { connection_id });
538        }
539        if inner.candidates_by_id.contains_key(&module_id) {
540            return Err(ForwardingError::CandidateSlotOccupied { module_id });
541        }
542        if let Some(old_endpoint) = inner.endpoint_by_connection.remove(&connection_id) {
543            let _ = remove_module_connection_locked(&mut inner, old_endpoint);
544        }
545
546        inner.next_generation = inner.next_generation.checked_add(1).unwrap_or(1);
547        let endpoint = ModuleEndpointId {
548            connection_id,
549            generation: inner.next_generation,
550        };
551        inner.endpoint_by_connection.insert(connection_id, endpoint);
552        inner
553            .module_id_by_endpoint
554            .insert(endpoint, module_id.clone());
555        inner.next_module_channel.insert(endpoint, 1);
556        inner.next_control_corr.insert(endpoint, 1);
557        inner.candidates_by_id.insert(
558            module_id,
559            ModuleConnection {
560                endpoint,
561                sink,
562                negotiated_ver,
563                concurrency,
564            },
565        );
566        Ok(endpoint)
567    }
568
569    /// Promote `module_id`'s candidate to the active slot, in ONE forwarding
570    /// write-lock critical section.
571    ///
572    /// That single section is the linearization point of a swap. Every relay
573    /// reservation resolves the active slot under the same lock, so each one
574    /// lands wholly before cutover (on the incumbent) or wholly after it (on the
575    /// promoted candidate), never on a mix. A relay reserved on the incumbent
576    /// before cutover can no longer commit: `commit_route_locked` requires the
577    /// reservation's endpoint to be the active one, and an ack for it arriving
578    /// later is answered by the superseded-endpoint arm of
579    /// `complete_pending_relay` instead of ending the incumbent's connection.
580    ///
581    /// Both endpoints keep their identities: nothing is re-keyed, so the
582    /// incumbent's bound routes and its pending correlation keys stay exactly
583    /// where they are until it is drained with [`Self::begin_endpoint_drain`],
584    /// using the incumbent endpoint this returns. Returns `Ok(None)` when there
585    /// is no candidate for the id.
586    pub(crate) fn cutover_candidate(
587        &self,
588        module_id: &str,
589    ) -> Result<Option<ForwardingCutover>, ForwardingError> {
590        let mut inner = self.write_inner()?;
591        if inner.daemon_draining {
592            return Err(ForwardingError::ModuleReloading {
593                module_id: module_id.to_string(),
594            });
595        }
596        let Some(candidate) = inner.candidates_by_id.remove(module_id) else {
597            return Ok(None);
598        };
599        let promoted = candidate.endpoint;
600        let incumbent = inner.modules_by_id.insert(module_id.to_string(), candidate);
601        let incumbent = incumbent.map(|incumbent| {
602            let endpoint = incumbent.endpoint;
603            inner.superseded_endpoints.insert(endpoint, incumbent);
604            endpoint
605        });
606        drop(inner);
607
608        // A different process now answers for this id; see the matching reset
609        // in `register_module_connection` for why a verdict about the old one
610        // must not carry over.
611        if let Some(discarded) = self
612            .route_bind_breakers
613            .reset_for_new_module_connection(module_id)
614        {
615            info!(
616                module_id = %module_id,
617                discarded_consecutive_timeouts = discarded,
618                "route.bind breaker state discarded: a swap candidate was promoted over the process it described"
619            );
620        }
621        Ok(Some(ForwardingCutover {
622            promoted,
623            incumbent,
624        }))
625    }
626
627    #[allow(clippy::too_many_arguments)]
628    pub(crate) async fn begin_route_bind_relay_for(
629        &self,
630        client_connection_id: ConnectionId,
631        client_sink: FrameSink,
632        client_negotiated_ver: u8,
633        client_corr: u64,
634        module_id: &str,
635        principal: Principal,
636        project_root: Option<ProjectRootId>,
637        deadline: Instant,
638    ) -> Result<PendingRouteBindRelay, ForwardingError> {
639        // Reserve egress capacity before taking the forwarding lock. The permit is
640        // held until the bind reaches one terminal state, so an accepted bind can
641        // publish its table entry and RouteOpen response in one critical section.
642        let client_permit =
643            client_sink
644                .reserve_owned()
645                .await
646                .map_err(|_| ForwardingError::ClientEgressClosed {
647                    connection_id: client_connection_id,
648                })?;
649        self.begin_route_bind_relay_inner(
650            client_connection_id,
651            client_sink,
652            client_negotiated_ver,
653            client_corr,
654            module_id,
655            principal,
656            project_root,
657            deadline,
658            client_permit,
659        )
660    }
661
662    #[cfg(test)]
663    pub(crate) fn begin_route_bind_relay_for_test(
664        &self,
665        client_connection_id: ConnectionId,
666        client_sink: FrameSink,
667        client_corr: u64,
668        module_id: &str,
669    ) -> Result<PendingRouteBindRelay, ForwardingError> {
670        let permit =
671            client_sink
672                .try_reserve_owned()
673                .map_err(|_| ForwardingError::ClientEgressClosed {
674                    connection_id: client_connection_id,
675                })?;
676        self.begin_route_bind_relay_inner(
677            client_connection_id,
678            client_sink,
679            subc_protocol::PROTOCOL_VERSION,
680            client_corr,
681            module_id,
682            Principal::Direct,
683            None,
684            Instant::now() + std::time::Duration::from_secs(60),
685            permit,
686        )
687    }
688
689    pub(crate) fn begin_module_control_rpc_for(
690        &self,
691        module_id: &str,
692        expected_op: &str,
693        deadline: Instant,
694    ) -> Result<PendingModuleControlRpc, ForwardingError> {
695        self.begin_module_control_rpc_inner(module_id, expected_op, deadline, None, false)
696    }
697
698    pub(crate) fn begin_health_probe_rpc_for(
699        &self,
700        module_id: &str,
701        expected_op: &str,
702        probe_started_at: Instant,
703        deadline: Instant,
704    ) -> Result<PendingModuleControlRpc, ForwardingError> {
705        self.begin_module_control_rpc_inner(
706            module_id,
707            expected_op,
708            deadline,
709            Some(probe_started_at),
710            false,
711        )
712    }
713
714    pub(crate) fn begin_drain_health_probe_rpc_for(
715        &self,
716        module_id: &str,
717        expected_op: &str,
718        probe_started_at: Instant,
719        deadline: Instant,
720    ) -> Result<PendingModuleControlRpc, ForwardingError> {
721        self.begin_module_control_rpc_inner(
722            module_id,
723            expected_op,
724            deadline,
725            Some(probe_started_at),
726            true,
727        )
728    }
729
730    /// A health probe addressed to one endpoint, in whichever slot it is.
731    ///
732    /// Every other control RPC resolves the id's ACTIVE endpoint, which is how
733    /// a swap candidate (not active until cutover) and a superseded incumbent
734    /// (not active after it) are unreachable by them. A swap needs to probe
735    /// exactly those two: the candidate before promoting it, and the incumbent
736    /// for its busy gauges while it drains.
737    pub(crate) fn begin_endpoint_health_probe_rpc_for(
738        &self,
739        endpoint: ModuleEndpointId,
740        expected_op: &str,
741        probe_started_at: Instant,
742        deadline: Instant,
743    ) -> Result<PendingModuleControlRpc, ForwardingError> {
744        let inner = self.write_inner()?;
745        let module = module_connection_for_endpoint_locked(&inner, endpoint)
746            .cloned()
747            .ok_or(ForwardingError::NoModuleConnection)?;
748        let module_id = inner
749            .module_id_by_endpoint
750            .get(&endpoint)
751            .cloned()
752            .unwrap_or_default();
753        // A draining incumbent is exactly what this probes for busy gauges, so
754        // draining is allowed, as it is for the by-id drain probe.
755        self.begin_control_rpc_locked(
756            inner,
757            &module_id,
758            module,
759            expected_op,
760            deadline,
761            Some(probe_started_at),
762            true,
763        )
764    }
765
766    fn begin_module_control_rpc_inner(
767        &self,
768        module_id: &str,
769        expected_op: &str,
770        deadline: Instant,
771        health_probe_started_at: Option<Instant>,
772        allow_draining: bool,
773    ) -> Result<PendingModuleControlRpc, ForwardingError> {
774        let inner = self.write_inner()?;
775        let module = inner
776            .modules_by_id
777            .get(module_id)
778            .cloned()
779            .ok_or(ForwardingError::NoModuleConnection)?;
780        self.begin_control_rpc_locked(
781            inner,
782            module_id,
783            module,
784            expected_op,
785            deadline,
786            health_probe_started_at,
787            allow_draining,
788        )
789    }
790
791    #[allow(clippy::too_many_arguments)]
792    fn begin_control_rpc_locked(
793        &self,
794        mut inner: RwLockWriteGuard<'_, ForwardingInner>,
795        module_id: &str,
796        module: ModuleConnection,
797        expected_op: &str,
798        deadline: Instant,
799        health_probe_started_at: Option<Instant>,
800        allow_draining: bool,
801    ) -> Result<PendingModuleControlRpc, ForwardingError> {
802        if !allow_draining && inner.draining_endpoints.contains_key(&module.endpoint) {
803            return Err(ForwardingError::ModuleReloading {
804                module_id: module_id.to_string(),
805            });
806        }
807        if inner
808            .closing_connections
809            .contains(&module.endpoint.connection_id)
810        {
811            return Err(ForwardingError::ConnectionClosing {
812                connection_id: module.endpoint.connection_id,
813            });
814        }
815        if health_probe_started_at.is_some() {
816            // Recurring health probes are serial per endpoint. Once the next one
817            // starts, an older answer can no longer improve the current snapshot,
818            // so retaining more than the newest unanswered probe has no value.
819            inner
820                .health_probe_tombstones
821                .retain(|(endpoint, _), _| *endpoint != module.endpoint);
822        }
823        let corr = match inner.allocate_control_corr(module.endpoint) {
824            Ok(corr) => corr,
825            Err(err) => {
826                drop(inner);
827                self.request_connection_close(
828                    module.endpoint.connection_id,
829                    CloseReason::new(
830                        "control_correlation_exhausted",
831                        "daemon-originated channel-0 correlation space exhausted",
832                    ),
833                );
834                return Err(err);
835            }
836        };
837        let (sender, receiver) = oneshot::channel();
838        inner.pending_control_rpcs.insert(
839            (module.endpoint, corr),
840            PendingModuleControlRpcEntry {
841                expected_op: expected_op.to_string(),
842                deadline,
843                health_probe_started_at,
844                sender,
845            },
846        );
847
848        Ok(PendingModuleControlRpc {
849            endpoint: module.endpoint,
850            module_sink: module.sink,
851            negotiated_ver: module.negotiated_ver,
852            corr,
853            receiver,
854        })
855    }
856
857    #[allow(clippy::too_many_arguments)]
858    fn begin_route_bind_relay_inner(
859        &self,
860        client_connection_id: ConnectionId,
861        client_sink: FrameSink,
862        client_negotiated_ver: u8,
863        client_corr: u64,
864        expected_module_id: &str,
865        principal: Principal,
866        project_root: Option<ProjectRootId>,
867        deadline: Instant,
868        client_permit: mpsc::OwnedPermit<crate::router::OutboundFrame>,
869    ) -> Result<PendingRouteBindRelay, ForwardingError> {
870        let mut inner = self.write_inner()?;
871        if inner.closing_connections.contains(&client_connection_id) {
872            return Err(ForwardingError::ConnectionClosing {
873                connection_id: client_connection_id,
874            });
875        }
876        let module = inner
877            .modules_by_id
878            .get(expected_module_id)
879            .cloned()
880            .ok_or(ForwardingError::NoModuleConnection)?;
881        if inner.draining_endpoints.contains_key(&module.endpoint) {
882            return Err(ForwardingError::ModuleReloading {
883                module_id: expected_module_id.to_string(),
884            });
885        }
886        if inner
887            .closing_connections
888            .contains(&module.endpoint.connection_id)
889        {
890            return Err(ForwardingError::ConnectionClosing {
891                connection_id: module.endpoint.connection_id,
892            });
893        }
894
895        let corr = match inner.allocate_control_corr(module.endpoint) {
896            Ok(corr) => corr,
897            Err(err) => {
898                drop(inner);
899                self.request_connection_close(
900                    module.endpoint.connection_id,
901                    CloseReason::new(
902                        "control_correlation_exhausted",
903                        "daemon-originated channel-0 correlation space exhausted",
904                    ),
905                );
906                return Err(err);
907            }
908        };
909        let (client_channel, client_epoch, module_channel, module_epoch) =
910            inner.allocate_route_slots(client_connection_id, module.endpoint)?;
911        let client_key = ClientRouteKey {
912            connection_id: client_connection_id,
913            channel: client_channel,
914        };
915        let module_key = ModuleRouteKey {
916            endpoint: module.endpoint,
917            channel: module_channel,
918        };
919        let reservation = RouteReservation {
920            client_key,
921            module_key,
922            client_epoch,
923            module_epoch,
924            project_root,
925        };
926        let response_body = serde_json::to_vec(&ClientControlResponse::RouteOpen {
927            route_channel: client_channel,
928            route_epoch: client_epoch,
929        })
930        .map_err(|err| ForwardingError::RouteOpenBuild(err.to_string()))?;
931        let route_open_frame = Frame::build_with_version(
932            client_negotiated_ver,
933            FrameType::Response,
934            Flags::new(false, Priority::Passive, false),
935            0,
936            0,
937            client_corr,
938            response_body,
939        )
940        .map_err(|err| ForwardingError::RouteOpenBuild(err.to_string()))?;
941        let (sender, receiver) = oneshot::channel();
942        inner.reserved_client.insert(client_key, module_key);
943        inner.reserved_module.insert(module_key, client_key);
944        inner.pending_relays.insert(
945            (module.endpoint, corr),
946            PendingRouteBindRelayEntry {
947                reservation,
948                client_sink,
949                client_negotiated_ver,
950                client_permit,
951                route_open_frame,
952                principal,
953                deadline,
954                relay_enqueued: false,
955                sender,
956            },
957        );
958
959        Ok(PendingRouteBindRelay {
960            endpoint: module.endpoint,
961            module_sink: module.sink,
962            negotiated_ver: module.negotiated_ver,
963            client_channel,
964            client_epoch,
965            module_channel,
966            module_epoch,
967            corr,
968            receiver,
969        })
970    }
971
972    pub(crate) fn mark_route_bind_relay_enqueued(
973        &self,
974        endpoint: ModuleEndpointId,
975        corr: u64,
976    ) -> Result<bool, ForwardingError> {
977        let mut inner = self.write_inner()?;
978        let Some(pending) = inner.pending_relays.get_mut(&(endpoint, corr)) else {
979            return Ok(false);
980        };
981        pending.relay_enqueued = true;
982        Ok(true)
983    }
984
985    pub(crate) fn release_client_route(
986        &self,
987        client_connection_id: ConnectionId,
988        client_channel: u16,
989        expected_epoch: u32,
990    ) -> Result<RouteRelease, ForwardingError> {
991        let mut inner = self.write_inner()?;
992        let release = release_client_route_locked(
993            &mut inner,
994            ClientRouteKey {
995                connection_id: client_connection_id,
996                channel: client_channel,
997            },
998            expected_epoch,
999        );
1000        self.record_route_release(&release);
1001        Ok(release)
1002    }
1003
1004    pub(crate) fn release_module_route(
1005        &self,
1006        module_connection_id: ConnectionId,
1007        module_channel: u16,
1008        expected_epoch: u32,
1009    ) -> Result<RouteRelease, ForwardingError> {
1010        let mut inner = self.write_inner()?;
1011        let Some(endpoint) = inner
1012            .endpoint_by_connection
1013            .get(&module_connection_id)
1014            .copied()
1015        else {
1016            return Ok(RouteRelease::Absent);
1017        };
1018        let release = release_module_route_locked(
1019            &mut inner,
1020            ModuleRouteKey {
1021                endpoint,
1022                channel: module_channel,
1023            },
1024            expected_epoch,
1025        );
1026        self.record_route_release(&release);
1027        Ok(release)
1028    }
1029
1030    pub(crate) fn abort_pending_relay(
1031        &self,
1032        endpoint: ModuleEndpointId,
1033        corr: u64,
1034        outcome: RouteBindRelayOutcome,
1035    ) -> Result<Option<GoodbyeTarget>, ForwardingError> {
1036        let mut inner = self.write_inner()?;
1037        let Some(pending) = inner.pending_relays.remove(&(endpoint, corr)) else {
1038            return Ok(None);
1039        };
1040        release_reserved_route_locked(
1041            &mut inner,
1042            pending.reservation.client_key,
1043            pending.reservation.module_key,
1044        );
1045        let target = pending
1046            .relay_enqueued
1047            .then(|| abandoned_route_target(&inner, &pending.reservation));
1048        let _ = pending.sender.send(outcome);
1049        Ok(target.flatten())
1050    }
1051
1052    pub(crate) fn cancel_module_control_rpc(
1053        &self,
1054        endpoint: ModuleEndpointId,
1055        corr: u64,
1056    ) -> Result<(), ForwardingError> {
1057        self.write_inner()?
1058            .pending_control_rpcs
1059            .remove(&(endpoint, corr));
1060        Ok(())
1061    }
1062
1063    pub(crate) fn tombstone_health_probe_rpc(
1064        &self,
1065        endpoint: ModuleEndpointId,
1066        corr: u64,
1067    ) -> Result<bool, ForwardingError> {
1068        let key = (endpoint, corr);
1069        let expires_at = Instant::now() + HEALTH_PROBE_TOMBSTONE_TTL;
1070        {
1071            let mut inner = self.write_inner()?;
1072            let Some(pending) = inner.pending_control_rpcs.remove(&key) else {
1073                return Ok(false);
1074            };
1075            let Some(probe_started_at) = pending.health_probe_started_at else {
1076                inner.pending_control_rpcs.insert(key, pending);
1077                return Ok(false);
1078            };
1079            let module_id = inner
1080                .module_id_by_endpoint
1081                .get(&endpoint)
1082                .cloned()
1083                .unwrap_or_else(|| "unknown".to_string());
1084            inner.health_probe_tombstones.insert(
1085                key,
1086                HealthProbeTombstone {
1087                    expected_op: pending.expected_op,
1088                    module_id,
1089                    probe_started_at,
1090                    expires_at,
1091                },
1092            );
1093        }
1094        self.schedule_health_probe_tombstone_expiration(key, expires_at);
1095        Ok(true)
1096    }
1097
1098    fn schedule_health_probe_tombstone_expiration(
1099        &self,
1100        key: (ModuleEndpointId, u64),
1101        expires_at: Instant,
1102    ) {
1103        let inner = Arc::downgrade(&self.inner);
1104        tokio::spawn(async move {
1105            tokio::time::sleep_until(expires_at).await;
1106            let Some(inner) = inner.upgrade() else {
1107                return;
1108            };
1109            let Ok(mut inner) = inner.write() else {
1110                return;
1111            };
1112            let expired = inner
1113                .health_probe_tombstones
1114                .get(&key)
1115                .is_some_and(|tombstone| tombstone.expires_at <= Instant::now());
1116            if expired {
1117                inner.health_probe_tombstones.remove(&key);
1118            }
1119        });
1120    }
1121
1122    pub(crate) fn complete_pending_relay(
1123        &self,
1124        connection_id: ConnectionId,
1125        corr: u64,
1126        outcome: RouteBindRelayOutcome,
1127    ) -> Result<PendingRelayCompletion, ForwardingError> {
1128        let mut inner = self.write_inner()?;
1129        let Some(endpoint) = inner.endpoint_by_connection.get(&connection_id).copied() else {
1130            return Ok(PendingRelayCompletion {
1131                settled: false,
1132                abandoned: None,
1133            });
1134        };
1135        let Some(pending) = inner.pending_relays.remove(&(endpoint, corr)) else {
1136            return Ok(PendingRelayCompletion {
1137                settled: false,
1138                abandoned: None,
1139            });
1140        };
1141
1142        if Instant::now() >= pending.deadline {
1143            release_reserved_route_locked(
1144                &mut inner,
1145                pending.reservation.client_key,
1146                pending.reservation.module_key,
1147            );
1148            let abandoned = matches!(outcome, RouteBindRelayOutcome::Accepted)
1149                .then(|| abandoned_route_target(&inner, &pending.reservation))
1150                .flatten();
1151            let _ = pending
1152                .sender
1153                .send(RouteBindRelayOutcome::Rejected(ErrorBody {
1154                    code: "module_timeout".to_string(),
1155                    message: "route.bind response arrived after its daemon deadline".to_string(),
1156                    detail: None,
1157                }));
1158            return Ok(PendingRelayCompletion {
1159                settled: true,
1160                abandoned,
1161            });
1162        }
1163
1164        match outcome {
1165            // Two shapes of "the client is not there to receive this route" that
1166            // must resolve identically: its egress is already closed, or it is
1167            // marked closing (its connection loop has been asked to end, but has
1168            // not drained yet, so the sink is still open).
1169            //
1170            // Only the first used to be caught here. The second fell through to
1171            // `commit_route_locked`, which refuses a closing client with
1172            // `ConnectionClosing` -- and this function is called from the MODULE
1173            // connection's frame handler, so that refusal ended the module's
1174            // connection instead of this one client's route. One dying client's
1175            // route.open then took down a connection carrying every other
1176            // client's routes to that module.
1177            //
1178            // The remedy for both is the same, which is why they share an arm:
1179            // give back the reserved handle pair, tell the waiting route.open the
1180            // route is gone, and report the module-side channel so the caller can
1181            // send a channel-scoped GOODBYE for the binding the module just
1182            // created. Nothing here touches the module connection.
1183            RouteBindRelayOutcome::Accepted
1184                if pending.client_sink.is_closed()
1185                    || inner
1186                        .closing_connections
1187                        .contains(&pending.reservation.client_key.connection_id) =>
1188            {
1189                let reason = if pending.client_sink.is_closed() {
1190                    "client egress closed before route publication"
1191                } else {
1192                    "client connection is closing before route publication"
1193                };
1194                release_reserved_route_locked(
1195                    &mut inner,
1196                    pending.reservation.client_key,
1197                    pending.reservation.module_key,
1198                );
1199                let abandoned = pending
1200                    .relay_enqueued
1201                    .then(|| abandoned_route_target(&inner, &pending.reservation))
1202                    .flatten();
1203                let _ = pending
1204                    .sender
1205                    .send(RouteBindRelayOutcome::ModuleGone(reason.to_string()));
1206                return Ok(PendingRelayCompletion {
1207                    settled: true,
1208                    abandoned,
1209                });
1210            }
1211            // The acking endpoint was the active one when this relay was
1212            // reserved, and a swap has since promoted a candidate over it. The
1213            // module did nothing wrong: it bound the route it was asked to bind,
1214            // and it is still carrying every other client's routes until it is
1215            // drained. So this is settled here, while the pending entry still
1216            // holds the client's sender and the reservation pair: release the
1217            // pair, tell the waiting route.open to retry (it will reserve on the
1218            // promoted endpoint), and hand back the module-side channel so the
1219            // caller sends one channel-scoped GOODBYE for the binding the module
1220            // just created. Nothing here touches the module connection.
1221            //
1222            // Only membership in `superseded_endpoints` takes this arm. An
1223            // endpoint that stopped being the active one for any other reason
1224            // is STALE, not superseded, and still falls through to
1225            // `commit_route_locked`, which refuses it with `StaleModuleEndpoint`
1226            // exactly as before swaps existed.
1227            RouteBindRelayOutcome::Accepted
1228                if inner.superseded_endpoints.contains_key(&endpoint) =>
1229            {
1230                release_reserved_route_locked(
1231                    &mut inner,
1232                    pending.reservation.client_key,
1233                    pending.reservation.module_key,
1234                );
1235                // Not gated on `relay_enqueued`: an ack proves the module
1236                // received the bind, whether or not the enqueue mark was set.
1237                let abandoned = abandoned_route_target(&inner, &pending.reservation);
1238                let module_id = inner
1239                    .module_id_by_endpoint
1240                    .get(&endpoint)
1241                    .cloned()
1242                    .unwrap_or_else(|| "unknown".to_string());
1243                let _ = pending
1244                    .sender
1245                    .send(RouteBindRelayOutcome::Rejected(ErrorBody::new(
1246                        "module_reloading",
1247                        format!("module_id '{module_id}' is reloading"),
1248                    )));
1249                return Ok(PendingRelayCompletion {
1250                    settled: true,
1251                    abandoned,
1252                });
1253            }
1254            RouteBindRelayOutcome::Accepted => {
1255                let abandoned = commit_route_locked(&mut inner, pending)?;
1256                return Ok(PendingRelayCompletion {
1257                    settled: true,
1258                    abandoned,
1259                });
1260            }
1261            terminal => {
1262                release_reserved_route_locked(
1263                    &mut inner,
1264                    pending.reservation.client_key,
1265                    pending.reservation.module_key,
1266                );
1267                let _ = pending.sender.send(terminal);
1268            }
1269        }
1270        Ok(PendingRelayCompletion {
1271            settled: true,
1272            abandoned: None,
1273        })
1274    }
1275
1276    pub(crate) fn pending_module_control_op(
1277        &self,
1278        connection_id: ConnectionId,
1279        corr: u64,
1280    ) -> Result<Option<String>, ForwardingError> {
1281        let inner = self.read_inner()?;
1282        let Some(endpoint) = inner.endpoint_by_connection.get(&connection_id).copied() else {
1283            return Ok(None);
1284        };
1285        let key = (endpoint, corr);
1286        Ok(inner
1287            .pending_control_rpcs
1288            .get(&key)
1289            .map(|pending| pending.expected_op.clone())
1290            .or_else(|| {
1291                inner
1292                    .health_probe_tombstones
1293                    .get(&key)
1294                    .filter(|tombstone| tombstone.expires_at > Instant::now())
1295                    .map(|tombstone| tombstone.expected_op.clone())
1296            }))
1297    }
1298
1299    pub(crate) fn complete_module_control_rpc(
1300        &self,
1301        connection_id: ConnectionId,
1302        corr: u64,
1303        actual_op: Option<&str>,
1304        outcome: ModuleControlRpcOutcome,
1305    ) -> Result<ModuleControlRpcCompletion, ForwardingError> {
1306        let now = Instant::now();
1307        let mut inner = self.write_inner()?;
1308        let Some(endpoint) = inner.endpoint_by_connection.get(&connection_id).copied() else {
1309            return Ok(ModuleControlRpcCompletion::Unknown);
1310        };
1311        let key = (endpoint, corr);
1312        if let Some(pending) = inner.pending_control_rpcs.remove(&key) {
1313            if now >= pending.deadline {
1314                let late_health_answer = pending.health_probe_started_at.map(|probe_started_at| {
1315                    ModuleControlRpcCompletion::LateHealthAnswer {
1316                        module_id: inner
1317                            .module_id_by_endpoint
1318                            .get(&endpoint)
1319                            .cloned()
1320                            .unwrap_or_else(|| "unknown".to_string()),
1321                        latency: now.saturating_duration_since(probe_started_at),
1322                    }
1323                });
1324                let _ = pending
1325                    .sender
1326                    .send(ModuleControlRpcOutcome::DeadlineElapsed);
1327                return Ok(late_health_answer.unwrap_or(ModuleControlRpcCompletion::Settled));
1328            }
1329            let outcome = match actual_op {
1330                Some(actual) if actual != pending.expected_op => {
1331                    ModuleControlRpcOutcome::UnexpectedOp {
1332                        expected: pending.expected_op,
1333                        actual: actual.to_string(),
1334                    }
1335                }
1336                _ => outcome,
1337            };
1338            let _ = pending.sender.send(outcome);
1339            return Ok(ModuleControlRpcCompletion::Settled);
1340        }
1341
1342        let Some(tombstone) = inner.health_probe_tombstones.remove(&key) else {
1343            return Ok(ModuleControlRpcCompletion::Unknown);
1344        };
1345        if tombstone.expires_at <= now {
1346            return Ok(ModuleControlRpcCompletion::Unknown);
1347        }
1348        Ok(ModuleControlRpcCompletion::LateHealthAnswer {
1349            module_id: tombstone.module_id,
1350            latency: now.saturating_duration_since(tombstone.probe_started_at),
1351        })
1352    }
1353
1354    #[cfg(test)]
1355    pub(crate) fn health_probe_tombstone_count(&self) -> Result<usize, ForwardingError> {
1356        Ok(self.read_inner()?.health_probe_tombstones.len())
1357    }
1358
1359    #[cfg(test)]
1360    pub(crate) fn closing_connection_count(&self) -> Result<usize, ForwardingError> {
1361        Ok(self.read_inner()?.closing_connections.len())
1362    }
1363
1364    /// Reserved-but-uncommitted route handles, counted on both index sides, so
1365    /// a test can assert a reservation pair was actually given back.
1366    #[cfg(test)]
1367    pub(crate) fn reserved_route_count(&self) -> Result<(usize, usize), ForwardingError> {
1368        let inner = self.read_inner()?;
1369        Ok((inner.reserved_client.len(), inner.reserved_module.len()))
1370    }
1371
1372    pub(crate) fn module_endpoint_for_connection(
1373        &self,
1374        connection_id: ConnectionId,
1375    ) -> Result<Option<ModuleEndpointId>, ForwardingError> {
1376        Ok(self
1377            .read_inner()?
1378            .endpoint_by_connection
1379            .get(&connection_id)
1380            .copied())
1381    }
1382
1383    /// Looks up the module registered on a data-plane connection so route-drop
1384    /// diagnostics name the emitter instead of only reporting a daemon total.
1385    pub(crate) fn module_id_for_connection(
1386        &self,
1387        connection_id: ConnectionId,
1388    ) -> Result<Option<String>, ForwardingError> {
1389        let inner = self.read_inner()?;
1390        Ok(inner
1391            .endpoint_by_connection
1392            .get(&connection_id)
1393            .and_then(|endpoint| inner.module_id_by_endpoint.get(endpoint))
1394            .cloned())
1395    }
1396
1397    pub(crate) fn has_live_module_connection(
1398        &self,
1399        module_id: &str,
1400    ) -> Result<bool, ForwardingError> {
1401        Ok(self.read_inner()?.modules_by_id.contains_key(module_id))
1402    }
1403
1404    pub(crate) fn lookup_data_route(
1405        &self,
1406        connection_id: ConnectionId,
1407        channel: u16,
1408        epoch: u32,
1409    ) -> Result<DataRoute, ForwardingError> {
1410        let inner = self.read_inner()?;
1411        let state = if let Some(endpoint) =
1412            inner.endpoint_by_connection.get(&connection_id).copied()
1413        {
1414            let key = ModuleRouteKey { endpoint, channel };
1415            match inner.module_to_client.get(&key) {
1416                Some(route) if route.module_epoch == epoch => {
1417                    DataRouteState::Bound(Arc::clone(route))
1418                }
1419                Some(_) => DataRouteState::EpochMismatch,
1420                None if inner.reserved_module.contains_key(&key)
1421                    && inner.module_slot_epochs.get(&key).copied() == Some(epoch) =>
1422                {
1423                    DataRouteState::Reserved
1424                }
1425                None if inner.reserved_module.contains_key(&key) => DataRouteState::EpochMismatch,
1426                None => DataRouteState::Absent,
1427            }
1428        } else {
1429            let key = ClientRouteKey {
1430                connection_id,
1431                channel,
1432            };
1433            match inner.client_to_module.get(&key) {
1434                Some(route) if route.client_epoch == epoch => {
1435                    DataRouteState::Bound(Arc::clone(route))
1436                }
1437                Some(_) => DataRouteState::EpochMismatch,
1438                None if inner.reserved_client.contains_key(&key)
1439                    && inner.client_slot_epochs.get(&key).copied() == Some(epoch) =>
1440                {
1441                    DataRouteState::Reserved
1442                }
1443                None if inner.reserved_client.contains_key(&key) => DataRouteState::EpochMismatch,
1444                None => DataRouteState::Absent,
1445            }
1446        };
1447        Ok(
1448            if inner.endpoint_by_connection.contains_key(&connection_id) {
1449                DataRoute::Module(state)
1450            } else {
1451                DataRoute::Client(state)
1452            },
1453        )
1454    }
1455
1456    #[cfg(test)]
1457    pub(crate) fn inject_client_slot_epoch(
1458        &self,
1459        connection_id: ConnectionId,
1460        channel: u16,
1461        last_epoch: u32,
1462    ) {
1463        let mut inner = self.write_inner().expect("forwarding lock");
1464        inner.client_slot_epochs.insert(
1465            ClientRouteKey {
1466                connection_id,
1467                channel,
1468            },
1469            last_epoch,
1470        );
1471        inner.next_client_channel.insert(connection_id, channel);
1472    }
1473
1474    #[cfg(test)]
1475    pub(crate) fn inject_module_slot_epoch(
1476        &self,
1477        endpoint: ModuleEndpointId,
1478        channel: u16,
1479        last_epoch: u32,
1480    ) {
1481        let mut inner = self.write_inner().expect("forwarding lock");
1482        inner
1483            .module_slot_epochs
1484            .insert(ModuleRouteKey { endpoint, channel }, last_epoch);
1485        inner.next_module_channel.insert(endpoint, channel);
1486    }
1487
1488    #[cfg(test)]
1489    pub(crate) fn inject_control_corr(&self, endpoint: ModuleEndpointId, next_corr: u64) {
1490        self.write_inner()
1491            .expect("forwarding lock")
1492            .next_control_corr
1493            .insert(endpoint, next_corr);
1494    }
1495
1496    pub(crate) fn cache_status(
1497        &self,
1498        endpoint: ModuleEndpointId,
1499        module_channel: u16,
1500        module_epoch: u32,
1501        status: String,
1502    ) -> Result<bool, ForwardingError> {
1503        let mut inner = self.write_inner()?;
1504        if !inner.module_id_by_endpoint.contains_key(&endpoint) {
1505            return Err(ForwardingError::StaleModuleEndpoint);
1506        }
1507
1508        let module_key = ModuleRouteKey {
1509            endpoint,
1510            channel: module_channel,
1511        };
1512        let handle = if let Some(route) = inner.module_to_client.get(&module_key) {
1513            (route.module_epoch == module_epoch).then_some((
1514                ClientRouteKey {
1515                    connection_id: route.client_connection_id,
1516                    channel: route.client_channel,
1517                },
1518                route.client_epoch,
1519            ))
1520        } else if let Some(client_key) = inner.reserved_module.get(&module_key).copied() {
1521            (inner.module_slot_epochs.get(&module_key).copied() == Some(module_epoch)).then_some((
1522                client_key,
1523                inner
1524                    .client_slot_epochs
1525                    .get(&client_key)
1526                    .copied()
1527                    .unwrap_or(0),
1528            ))
1529        } else {
1530            None
1531        };
1532
1533        if let Some(handle) = handle {
1534            inner.status.insert(handle, status);
1535            Ok(true)
1536        } else {
1537            debug!(
1538                module_channel,
1539                module_epoch,
1540                generation = endpoint.generation,
1541                connection_id = endpoint.connection_id.get(),
1542                "dropping stale status update for module route handle"
1543            );
1544            Ok(false)
1545        }
1546    }
1547
1548    pub(crate) fn route_poll_snapshot(
1549        &self,
1550        client_connection_id: ConnectionId,
1551        client_channel: u16,
1552        client_epoch: u32,
1553    ) -> Result<RoutePollSnapshot, ForwardingError> {
1554        let inner = self.read_inner()?;
1555        let client_key = ClientRouteKey {
1556            connection_id: client_connection_id,
1557            channel: client_channel,
1558        };
1559        let Some(route) = inner.client_to_module.get(&client_key) else {
1560            return Ok(RoutePollSnapshot::Absent);
1561        };
1562        if route.client_epoch != client_epoch
1563            || !inner
1564                .module_id_by_endpoint
1565                .contains_key(&route.module_endpoint)
1566        {
1567            return Ok(RoutePollSnapshot::Absent);
1568        }
1569        Ok(RoutePollSnapshot::Bound {
1570            module_id: route.module_id.clone(),
1571            status: inner.status.get(&(client_key, client_epoch)).cloned(),
1572        })
1573    }
1574
1575    pub fn active_binding_count(&self) -> Result<usize, ForwardingError> {
1576        Ok(self.read_inner()?.client_to_module.len())
1577    }
1578
1579    /// How many distinct client connections hold at least one committed route,
1580    /// alongside the largest number of routes any single connection holds.
1581    ///
1582    /// `connected_clients` alone cannot distinguish many clients with a route
1583    /// each from one client accumulating hundreds, and those have opposite
1584    /// causes. Reading it required an out-of-band `lsof` during a live
1585    /// investigation, and the count of connections was mistaken for a count of
1586    /// client processes — which sent two of us after cleanup paths that were
1587    /// working correctly.
1588    pub fn client_route_concentration(&self) -> Result<(usize, usize), ForwardingError> {
1589        let inner = self.read_inner()?;
1590        let mut per_connection: HashMap<ConnectionId, usize> = HashMap::new();
1591        for key in inner.client_to_module.keys() {
1592            *per_connection.entry(key.connection_id).or_insert(0) += 1;
1593        }
1594        let max = per_connection.values().copied().max().unwrap_or(0);
1595        Ok((per_connection.len(), max))
1596    }
1597
1598    pub fn has_route_channel(&self, route_channel: u16) -> Result<bool, ForwardingError> {
1599        let inner = self.read_inner()?;
1600        Ok(inner
1601            .client_to_module
1602            .keys()
1603            .any(|key| key.channel == route_channel))
1604    }
1605
1606    /// Gate every provider atomically, including registrations racing shutdown.
1607    /// No supervisor lock is held while taking the forwarding lock.
1608    #[cfg(unix)]
1609    pub(crate) fn begin_daemon_drain(&self) -> Result<Vec<String>, ForwardingError> {
1610        let mut inner = self.write_inner()?;
1611        inner.daemon_draining = true;
1612        let modules = inner
1613            .modules_by_id
1614            .iter()
1615            .map(|(id, module)| (id.clone(), module.endpoint))
1616            .collect::<Vec<_>>();
1617        for (_, endpoint) in &modules {
1618            inner
1619                .draining_endpoints
1620                .insert(*endpoint, RouteCloseReason::Restart);
1621        }
1622        // Swap candidates and superseded incumbents are not routable, but they
1623        // are live endpoints that could still be sent module-control work, so
1624        // the daemon-wide gate covers them too. The returned ids are unchanged:
1625        // they name modules for the supervisor to drain, one per id.
1626        let off_slot_endpoints = inner
1627            .candidates_by_id
1628            .values()
1629            .map(|module| module.endpoint)
1630            .chain(inner.superseded_endpoints.keys().copied())
1631            .collect::<Vec<_>>();
1632        for endpoint in off_slot_endpoints {
1633            inner
1634                .draining_endpoints
1635                .insert(endpoint, RouteCloseReason::Restart);
1636        }
1637        Ok(modules.into_iter().map(|(id, _)| id).collect())
1638    }
1639
1640    /// Begin draining whatever endpoint is ACTIVE for `module_id`.
1641    ///
1642    /// After a swap's cutover the active endpoint is the promoted candidate, so
1643    /// this must not be used to drain the incumbent; use
1644    /// [`Self::begin_endpoint_drain`] with the endpoint `cutover_candidate`
1645    /// returned.
1646    pub(crate) fn begin_module_drain(
1647        &self,
1648        module_id: &str,
1649        reason: RouteCloseReason,
1650    ) -> Result<Option<ModuleDrainTarget>, ForwardingError> {
1651        let mut inner = self.write_inner()?;
1652        let Some(module) = inner.modules_by_id.get(module_id).cloned() else {
1653            return Ok(None);
1654        };
1655        Ok(Some(begin_drain_locked(
1656            &mut inner, module_id, module, reason,
1657        )))
1658    }
1659
1660    /// Begin draining one specific endpoint, whichever slot it is in.
1661    ///
1662    /// This is how a swap drains its incumbent after cutover: the incumbent's
1663    /// endpoint is captured by `cutover_candidate`, and resolving it by module id
1664    /// instead would find the promoted candidate and leave neither process
1665    /// routable. Returns `Ok(None)` when the endpoint is no longer registered.
1666    pub(crate) fn begin_endpoint_drain(
1667        &self,
1668        endpoint: ModuleEndpointId,
1669        reason: RouteCloseReason,
1670    ) -> Result<Option<ModuleDrainTarget>, ForwardingError> {
1671        let mut inner = self.write_inner()?;
1672        let Some(module) = module_connection_for_endpoint_locked(&inner, endpoint).cloned() else {
1673            return Ok(None);
1674        };
1675        let module_id = inner
1676            .module_id_by_endpoint
1677            .get(&endpoint)
1678            .cloned()
1679            .expect("an endpoint resolved to a module connection has a module id");
1680        Ok(Some(begin_drain_locked(
1681            &mut inner, &module_id, module, reason,
1682        )))
1683    }
1684}
1685
1686/// Mark `module`'s endpoint draining and settle everything still pending on it.
1687/// Shared by the by-id and by-endpoint drain entry points, which differ only in
1688/// how they find the endpoint.
1689fn begin_drain_locked(
1690    inner: &mut ForwardingInner,
1691    module_id: &str,
1692    module: ModuleConnection,
1693    reason: RouteCloseReason,
1694) -> ModuleDrainTarget {
1695    {
1696        let endpoint = module.endpoint;
1697        inner.draining_endpoints.insert(endpoint, reason);
1698
1699        let flows = inner
1700            .client_to_module
1701            .values()
1702            .filter(|route| route.module_endpoint == endpoint)
1703            .map(|route| Arc::clone(&route.flow))
1704            .collect::<Vec<_>>();
1705        let excluded_subscriptions = flows
1706            .into_iter()
1707            .map(|flow| flow.begin_drain())
1708            .fold(0u32, u32::saturating_add);
1709
1710        let pending_keys = inner
1711            .pending_relays
1712            .keys()
1713            .filter(|(pending_endpoint, _)| *pending_endpoint == endpoint)
1714            .copied()
1715            .collect::<Vec<_>>();
1716        let mut abandoned_bindings = Vec::new();
1717        for key in pending_keys {
1718            let Some(pending) = inner.pending_relays.remove(&key) else {
1719                continue;
1720            };
1721            release_reserved_route_locked(
1722                inner,
1723                pending.reservation.client_key,
1724                pending.reservation.module_key,
1725            );
1726            if pending.relay_enqueued {
1727                if let Some(target) = abandoned_route_target(inner, &pending.reservation) {
1728                    abandoned_bindings.push(target);
1729                }
1730            }
1731            let _ = pending
1732                .sender
1733                .send(RouteBindRelayOutcome::Rejected(ErrorBody::new(
1734                    "module_reloading",
1735                    format!("module_id '{module_id}' is reloading"),
1736                )));
1737        }
1738
1739        let pending_control_keys = inner
1740            .pending_control_rpcs
1741            .keys()
1742            .filter(|(pending_endpoint, _)| *pending_endpoint == endpoint)
1743            .copied()
1744            .collect::<Vec<_>>();
1745        for key in pending_control_keys {
1746            if let Some(pending) = inner.pending_control_rpcs.remove(&key) {
1747                let _ = pending
1748                    .sender
1749                    .send(ModuleControlRpcOutcome::ModuleGone(format!(
1750                        "module '{module_id}' began draining during module-control RPC"
1751                    )));
1752            }
1753        }
1754
1755        ModuleDrainTarget {
1756            endpoint,
1757            sink: module.sink,
1758            negotiated_ver: module.negotiated_ver,
1759            abandoned_bindings,
1760            excluded_subscriptions,
1761        }
1762    }
1763}
1764
1765/// What a drain that timed out was still waiting on, for the log line that
1766/// reports the timeout. Per-request ages are not tracked.
1767#[derive(Debug, Default, PartialEq, Eq)]
1768pub(crate) struct DrainHoldouts {
1769    /// Requests still counted against the drain (subscriptions flagged as
1770    /// such are excluded from the drain and not counted here).
1771    pub(crate) requests: usize,
1772    /// Routes holding at least one of those requests.
1773    pub(crate) routes: usize,
1774    /// Every route on the endpoint, for scale.
1775    pub(crate) total_routes: usize,
1776    /// The client connections holding the most requests, largest first, at
1777    /// most three: enough to name the consumer without listing every route.
1778    pub(crate) top_connections: Vec<(u64, usize)>,
1779    /// The held requests themselves, as the module saw them, so its own log
1780    /// can say what each one was: `(module channel, corr)`, ordered by channel
1781    /// then corr, at most [`DRAIN_HELD_REQUESTS_LISTED`]. The daemon never reads
1782    /// request bodies, so it cannot name a request's method; the module can,
1783    /// from the channel and corr. A request is released only when the module
1784    /// sends a terminal frame (Response, Error or StreamEnd) with its corr on
1785    /// its route, so every pair listed here is one the module ended, if at all,
1786    /// without sending that frame.
1787    pub(crate) held: Vec<(u16, u64)>,
1788}
1789
1790/// How many held requests the drain-timeout line lists by channel and corr.
1791pub(crate) const DRAIN_HELD_REQUESTS_LISTED: usize = 32;
1792
1793impl ForwardingTable {
1794    /// Summarise the requests one endpoint's drain is still waiting on.
1795    pub(crate) fn endpoint_drain_holdouts(
1796        &self,
1797        endpoint: ModuleEndpointId,
1798    ) -> Result<DrainHoldouts, ForwardingError> {
1799        let inner = self.read_inner()?;
1800        let mut holdouts = DrainHoldouts::default();
1801        let mut by_connection: HashMap<u64, usize> = HashMap::new();
1802        for (key, route) in &inner.client_to_module {
1803            if route.module_endpoint != endpoint {
1804                continue;
1805            }
1806            holdouts.total_routes += 1;
1807            let held = route.flow.drain_in_flight();
1808            if held == 0 {
1809                continue;
1810            }
1811            holdouts.requests += held;
1812            holdouts.routes += 1;
1813            *by_connection.entry(key.connection_id.get()).or_default() += held;
1814            holdouts.held.extend(
1815                route
1816                    .flow
1817                    .drain_held_corrs()
1818                    .into_iter()
1819                    .map(|corr| (route.module_channel, corr)),
1820            );
1821        }
1822        holdouts.held.sort_unstable();
1823        holdouts.held.truncate(DRAIN_HELD_REQUESTS_LISTED);
1824        let mut connections = by_connection.into_iter().collect::<Vec<_>>();
1825        connections.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
1826        connections.truncate(3);
1827        holdouts.top_connections = connections;
1828        Ok(holdouts)
1829    }
1830
1831    pub(crate) fn endpoint_in_flight_count(
1832        &self,
1833        endpoint: ModuleEndpointId,
1834    ) -> Result<usize, ForwardingError> {
1835        let inner = self.read_inner()?;
1836        Ok(inner
1837            .client_to_module
1838            .values()
1839            .filter(|route| route.module_endpoint == endpoint)
1840            .map(|route| route.flow.drain_in_flight())
1841            .sum())
1842    }
1843
1844    pub(crate) fn endpoint_is_draining(
1845        &self,
1846        endpoint: ModuleEndpointId,
1847    ) -> Result<bool, ForwardingError> {
1848        Ok(self
1849            .read_inner()?
1850            .draining_endpoints
1851            .contains_key(&endpoint))
1852    }
1853
1854    pub(crate) fn module_is_draining(&self, module_id: &str) -> Result<bool, ForwardingError> {
1855        let inner = self.read_inner()?;
1856        Ok(inner
1857            .modules_by_id
1858            .get(module_id)
1859            .is_some_and(|module| inner.draining_endpoints.contains_key(&module.endpoint)))
1860    }
1861
1862    pub(crate) fn release_module_endpoint_routes(
1863        &self,
1864        endpoint: ModuleEndpointId,
1865    ) -> Result<Vec<GoodbyeTarget>, ForwardingError> {
1866        let mut inner = self.write_inner()?;
1867        let routes = inner
1868            .module_to_client
1869            .iter()
1870            .filter(|(module_key, _)| module_key.endpoint == endpoint)
1871            .map(|(module_key, route)| (*module_key, route.module_epoch))
1872            .collect::<Vec<_>>();
1873        let mut released = Vec::with_capacity(routes.len());
1874        for (module_key, epoch) in routes {
1875            if let RouteRelease::Removed(target) =
1876                release_module_route_locked(&mut inner, module_key, epoch)
1877            {
1878                released.push(target);
1879            }
1880        }
1881        Ok(released)
1882    }
1883
1884    /// Enumerate one endpoint's current routes without contacting the module.
1885    ///
1886    /// The read lock makes this safe while the endpoint drains: the returned
1887    /// `draining` marker describes the same table state that owns the route,
1888    /// rather than inferring liveness from a module that may be stopping.
1889    pub(crate) fn endpoint_routes(
1890        &self,
1891        endpoint: ModuleEndpointId,
1892    ) -> Result<Vec<EndpointRoute>, ForwardingError> {
1893        let inner = self.read_inner()?;
1894        Ok(endpoint_routes_locked(&inner, endpoint))
1895    }
1896
1897    /// Snapshot all live endpoint route sets under one forwarding-table read lock.
1898    pub(crate) fn route_census(
1899        &self,
1900        module_id: Option<&str>,
1901    ) -> Result<Vec<(String, Vec<EndpointRoute>)>, ForwardingError> {
1902        let inner = self.read_inner()?;
1903        let mut endpoints = inner
1904            .modules_by_id
1905            .iter()
1906            .filter(|(id, _)| module_id.is_none_or(|requested| requested == id.as_str()))
1907            .map(|(id, module)| (id.clone(), module.endpoint))
1908            .collect::<Vec<_>>();
1909        endpoints.sort_by(|left, right| left.0.cmp(&right.0));
1910        Ok(endpoints
1911            .into_iter()
1912            .map(|(id, endpoint)| (id, endpoint_routes_locked(&inner, endpoint)))
1913            .collect())
1914    }
1915
1916    /// Snapshot only the endpoint currently routable under this module id.
1917    pub(crate) fn live_roots(
1918        &self,
1919        module_id: &str,
1920    ) -> Result<ModuleControlResponseToModule, ForwardingError> {
1921        let inner = self.read_inner()?;
1922        let endpoint = inner
1923            .modules_by_id
1924            .get(module_id)
1925            .map(|module| module.endpoint);
1926        let mut roots = BTreeMap::new();
1927        let mut unknown_root_bindings = 0;
1928        let mut total_bindings = 0;
1929        if let Some(endpoint) = endpoint {
1930            for binding in inner
1931                .module_to_client
1932                .values()
1933                .filter(|binding| binding.module_endpoint == endpoint)
1934            {
1935                total_bindings += 1;
1936                if let Some(root) = &binding.project_root {
1937                    let entry = roots.entry(root.as_path().to_path_buf()).or_insert((0, 0));
1938                    entry.0 += 1;
1939                } else {
1940                    unknown_root_bindings += 1;
1941                }
1942            }
1943            for pending in inner
1944                .pending_relays
1945                .values()
1946                .filter(|pending| pending.reservation.module_key.endpoint == endpoint)
1947            {
1948                total_bindings += 1;
1949                if let Some(root) = &pending.reservation.project_root {
1950                    let entry = roots.entry(root.as_path().to_path_buf()).or_insert((0, 0));
1951                    entry.1 += 1;
1952                } else {
1953                    unknown_root_bindings += 1;
1954                }
1955            }
1956        }
1957        Ok(ModuleControlResponseToModule::LiveRoots {
1958            roots: roots
1959                .into_iter()
1960                .map(|(project_root, (bound, pending))| LiveRoot {
1961                    project_root,
1962                    bound,
1963                    pending,
1964                })
1965                .collect(),
1966            unknown_root_bindings,
1967            total_bindings,
1968        })
1969    }
1970
1971    /// True if this connection already owns committed or reserved CLIENT routes.
1972    /// A module registers (HELLO) before serving and never opens client routes, so
1973    /// a connection that has client routes must not also become a module endpoint
1974    /// — otherwise one connection holds both client and module state and cleanup
1975    /// only releases one side.
1976    pub(crate) fn connection_has_client_routes(
1977        &self,
1978        connection_id: ConnectionId,
1979    ) -> Result<bool, ForwardingError> {
1980        let inner = self.read_inner()?;
1981        let has = inner
1982            .client_to_module
1983            .keys()
1984            .any(|key| key.connection_id == connection_id)
1985            || inner
1986                .reserved_client
1987                .keys()
1988                .any(|key| key.connection_id == connection_id);
1989        Ok(has)
1990    }
1991
1992    pub(crate) fn cleanup_connection(
1993        &self,
1994        connection_id: ConnectionId,
1995    ) -> Result<Vec<GoodbyeTarget>, ForwardingError> {
1996        let mut inner = self.write_inner()?;
1997        inner.closing_connections.insert(connection_id);
1998        let released = if let Some(endpoint) = inner.endpoint_by_connection.remove(&connection_id) {
1999            remove_module_connection_locked(&mut inner, endpoint)
2000        } else {
2001            Self::cleanup_client_connection_locked(&mut inner, connection_id)
2002        };
2003        // The closing mark refuses new work for a connection whose teardown is
2004        // still pending. This is the latest point at which lifting it is safe:
2005        // teardown has just removed every per-connection entry above, under
2006        // this same write lock, so no lookup can still find live state for the
2007        // id; and ids come from a monotonic counter that never reissues one,
2008        // so the id can never name a future connection either. Keeping the
2009        // mark past this point only grew the set by one entry per connection
2010        // for the life of the daemon.
2011        inner.closing_connections.remove(&connection_id);
2012        Ok(released)
2013    }
2014
2015    fn cleanup_client_connection_locked(
2016        inner: &mut ForwardingInner,
2017        connection_id: ConnectionId,
2018    ) -> Vec<GoodbyeTarget> {
2019        let routes = inner
2020            .client_to_module
2021            .iter()
2022            .filter(|(key, _)| key.connection_id == connection_id)
2023            .map(|(key, route)| (*key, route.client_epoch))
2024            .collect::<Vec<_>>();
2025        let mut released = Vec::with_capacity(routes.len());
2026        for (client_key, epoch) in routes {
2027            if let RouteRelease::Removed(target) =
2028                release_client_route_locked(inner, client_key, epoch)
2029            {
2030                released.push(target);
2031            }
2032        }
2033
2034        let pending_keys = inner
2035            .pending_relays
2036            .iter()
2037            .filter(|(_, pending)| pending.reservation.client_key.connection_id == connection_id)
2038            .map(|(key, _)| *key)
2039            .collect::<Vec<_>>();
2040        for key in pending_keys {
2041            let Some(pending) = inner.pending_relays.remove(&key) else {
2042                continue;
2043            };
2044            release_reserved_route_locked(
2045                inner,
2046                pending.reservation.client_key,
2047                pending.reservation.module_key,
2048            );
2049            if pending.relay_enqueued {
2050                if let Some(target) = abandoned_route_target(inner, &pending.reservation) {
2051                    released.push(target);
2052                }
2053            }
2054            let _ = pending.sender.send(RouteBindRelayOutcome::ModuleGone(
2055                "client connection closed during route.bind relay".to_string(),
2056            ));
2057        }
2058
2059        let orphaned = inner
2060            .reserved_client
2061            .iter()
2062            .filter(|(key, _)| key.connection_id == connection_id)
2063            .map(|(client, module)| (*client, *module))
2064            .collect::<Vec<_>>();
2065        for (client_key, module_key) in orphaned {
2066            release_reserved_route_locked(inner, client_key, module_key);
2067        }
2068        inner.next_client_channel.remove(&connection_id);
2069        inner
2070            .client_slot_epochs
2071            .retain(|key, _| key.connection_id != connection_id);
2072        inner
2073            .last_published_epoch
2074            .retain(|key, _| key.connection_id != connection_id);
2075        inner
2076            .status
2077            .retain(|(key, _), _| key.connection_id != connection_id);
2078
2079        released
2080    }
2081
2082    pub(crate) fn escalate_client_delivery_failure(
2083        &self,
2084        connection_id: ConnectionId,
2085        channel: u16,
2086        expected_epoch: u32,
2087        reason: CloseReason,
2088    ) -> Result<bool, ForwardingError> {
2089        let should_close = {
2090            let mut inner = self.write_inner()?;
2091            let key = ClientRouteKey {
2092                connection_id,
2093                channel,
2094            };
2095            if inner.last_published_epoch.get(&key).copied() != Some(expected_epoch) {
2096                false
2097            } else {
2098                inner.closing_connections.insert(connection_id);
2099                true
2100            }
2101        };
2102        if should_close {
2103            self.request_connection_close(connection_id, reason);
2104        }
2105        Ok(should_close)
2106    }
2107
2108    fn record_route_release(&self, release: &RouteRelease) {
2109        match release {
2110            RouteRelease::Removed(_) => self.counters.increment_route_released_epoch_fenced(),
2111            RouteRelease::Stale => self.counters.increment_route_release_stale_skipped(),
2112            RouteRelease::Absent => {}
2113        }
2114    }
2115
2116    fn read_inner(&self) -> Result<RwLockReadGuard<'_, ForwardingInner>, ForwardingError> {
2117        self.inner.read().map_err(|_| ForwardingError::Poisoned)
2118    }
2119
2120    fn write_inner(&self) -> Result<RwLockWriteGuard<'_, ForwardingInner>, ForwardingError> {
2121        self.inner.write().map_err(|_| ForwardingError::Poisoned)
2122    }
2123
2124    fn lock_close_registry(
2125        &self,
2126    ) -> MutexGuard<'_, HashMap<ConnectionId, oneshot::Sender<CloseReason>>> {
2127        self.close_registry
2128            .lock()
2129            .unwrap_or_else(|poisoned| poisoned.into_inner())
2130    }
2131}
2132
2133impl ForwardingInner {
2134    fn allocate_route_slots(
2135        &mut self,
2136        connection_id: ConnectionId,
2137        endpoint: ModuleEndpointId,
2138    ) -> Result<(u16, u32, u16, u32), ForwardingError> {
2139        let client_start = *self.next_client_channel.entry(connection_id).or_insert(1);
2140        let mut client_channel = client_start;
2141        let client_channel = loop {
2142            let key = ClientRouteKey {
2143                connection_id,
2144                channel: client_channel,
2145            };
2146            let eligible = !self.client_to_module.contains_key(&key)
2147                && !self.reserved_client.contains_key(&key)
2148                && self.client_slot_epochs.get(&key).copied().unwrap_or(0) < u32::MAX;
2149            if eligible {
2150                break client_channel;
2151            }
2152            client_channel = next_channel(client_channel);
2153            if client_channel == client_start {
2154                return Err(ForwardingError::ClientRouteChannelExhausted { connection_id });
2155            }
2156        };
2157
2158        let module_start = *self.next_module_channel.entry(endpoint).or_insert(1);
2159        let mut module_channel = module_start;
2160        let module_channel = loop {
2161            let key = ModuleRouteKey {
2162                endpoint,
2163                channel: module_channel,
2164            };
2165            let eligible = !self.module_to_client.contains_key(&key)
2166                && !self.reserved_module.contains_key(&key)
2167                && self.module_slot_epochs.get(&key).copied().unwrap_or(0) < u32::MAX;
2168            if eligible {
2169                break module_channel;
2170            }
2171            module_channel = next_channel(module_channel);
2172            if module_channel == module_start {
2173                return Err(ForwardingError::ModuleRouteChannelExhausted { endpoint });
2174            }
2175        };
2176
2177        let client_key = ClientRouteKey {
2178            connection_id,
2179            channel: client_channel,
2180        };
2181        let module_key = ModuleRouteKey {
2182            endpoint,
2183            channel: module_channel,
2184        };
2185        let client_epoch = self
2186            .client_slot_epochs
2187            .get(&client_key)
2188            .copied()
2189            .unwrap_or(0)
2190            + 1;
2191        let module_epoch = self
2192            .module_slot_epochs
2193            .get(&module_key)
2194            .copied()
2195            .unwrap_or(0)
2196            + 1;
2197        self.client_slot_epochs.insert(client_key, client_epoch);
2198        self.module_slot_epochs.insert(module_key, module_epoch);
2199        self.next_client_channel
2200            .insert(connection_id, next_channel(client_channel));
2201        self.next_module_channel
2202            .insert(endpoint, next_channel(module_channel));
2203        Ok((client_channel, client_epoch, module_channel, module_epoch))
2204    }
2205
2206    fn allocate_control_corr(
2207        &mut self,
2208        endpoint: ModuleEndpointId,
2209    ) -> Result<u64, ForwardingError> {
2210        let candidate = self.next_control_corr.get(&endpoint).copied().unwrap_or(1);
2211        if candidate == 0 {
2212            self.closing_connections.insert(endpoint.connection_id);
2213            return Err(ForwardingError::RelayCorrelationExhausted);
2214        }
2215        self.next_control_corr.insert(
2216            endpoint,
2217            if candidate == u64::MAX {
2218                0
2219            } else {
2220                candidate + 1
2221            },
2222        );
2223        Ok(candidate)
2224    }
2225}
2226
2227fn next_channel(channel: u16) -> u16 {
2228    let next = channel.wrapping_add(1);
2229    if next == 0 {
2230        1
2231    } else {
2232        next
2233    }
2234}
2235
2236fn endpoint_routes_locked(
2237    inner: &ForwardingInner,
2238    endpoint: ModuleEndpointId,
2239) -> Vec<EndpointRoute> {
2240    let drain_reason = inner.draining_endpoints.get(&endpoint).copied();
2241    let draining = drain_reason.is_some();
2242    let mut routes = inner
2243        .module_to_client
2244        .iter()
2245        .filter(|(module_key, _)| module_key.endpoint == endpoint)
2246        .map(|(_, route)| EndpointRoute {
2247            goodbye_target: GoodbyeTarget {
2248                connection_id: route.client_connection_id,
2249                sink: route.client_sink.clone(),
2250                negotiated_ver: route.client_negotiated_ver,
2251                channel: route.client_channel,
2252                epoch: route.client_epoch,
2253                kind: GoodbyeTargetKind::Client,
2254                module_id: None,
2255            },
2256            principal: route.principal.clone(),
2257            bound_at: route.bound_at,
2258            draining,
2259            drain_reason,
2260        })
2261        .collect::<Vec<_>>();
2262    routes.sort_by_key(|route| {
2263        (
2264            route.goodbye_target.connection_id.get(),
2265            route.goodbye_target.channel,
2266            route.goodbye_target.epoch,
2267        )
2268    });
2269    routes
2270}
2271
2272fn release_reserved_route_locked(
2273    inner: &mut ForwardingInner,
2274    client_key: ClientRouteKey,
2275    module_key: ModuleRouteKey,
2276) {
2277    if inner.reserved_client.get(&client_key).copied() == Some(module_key) {
2278        inner.reserved_client.remove(&client_key);
2279    }
2280    if inner.reserved_module.get(&module_key).copied() == Some(client_key) {
2281        inner.reserved_module.remove(&module_key);
2282    }
2283    inner.status.retain(|(key, _), _| *key != client_key);
2284}
2285
2286fn release_client_route_locked(
2287    inner: &mut ForwardingInner,
2288    client_key: ClientRouteKey,
2289    expected_epoch: u32,
2290) -> RouteRelease {
2291    let Some(route) = inner.client_to_module.get(&client_key) else {
2292        return RouteRelease::Absent;
2293    };
2294    if route.client_epoch != expected_epoch {
2295        return RouteRelease::Stale;
2296    }
2297    let route = inner
2298        .client_to_module
2299        .remove(&client_key)
2300        .expect("route checked under the same forwarding lock");
2301    route.flow.close();
2302    inner.module_to_client.remove(&ModuleRouteKey {
2303        endpoint: route.module_endpoint,
2304        channel: route.module_channel,
2305    });
2306    inner.status.remove(&(client_key, expected_epoch));
2307    RouteRelease::Removed(GoodbyeTarget {
2308        connection_id: route.module_endpoint.connection_id,
2309        sink: route.module_sink.clone(),
2310        negotiated_ver: route.module_negotiated_ver,
2311        channel: route.module_channel,
2312        epoch: route.module_epoch,
2313        kind: GoodbyeTargetKind::Module,
2314        module_id: Some(route.module_id.clone()),
2315    })
2316}
2317
2318fn release_module_route_locked(
2319    inner: &mut ForwardingInner,
2320    module_key: ModuleRouteKey,
2321    expected_epoch: u32,
2322) -> RouteRelease {
2323    let Some(route) = inner.module_to_client.get(&module_key) else {
2324        return RouteRelease::Absent;
2325    };
2326    if route.module_epoch != expected_epoch {
2327        return RouteRelease::Stale;
2328    }
2329    let route = inner
2330        .module_to_client
2331        .remove(&module_key)
2332        .expect("route checked under the same forwarding lock");
2333    route.flow.close();
2334    let client_key = ClientRouteKey {
2335        connection_id: route.client_connection_id,
2336        channel: route.client_channel,
2337    };
2338    inner.client_to_module.remove(&client_key);
2339    inner.status.remove(&(client_key, route.client_epoch));
2340    RouteRelease::Removed(GoodbyeTarget {
2341        connection_id: route.client_connection_id,
2342        sink: route.client_sink.clone(),
2343        negotiated_ver: route.client_negotiated_ver,
2344        channel: route.client_channel,
2345        epoch: route.client_epoch,
2346        kind: GoodbyeTargetKind::Client,
2347        module_id: None,
2348    })
2349}
2350
2351fn commit_route_locked(
2352    inner: &mut ForwardingInner,
2353    pending: PendingRouteBindRelayEntry,
2354) -> Result<Option<GoodbyeTarget>, ForwardingError> {
2355    let reservation = pending.reservation;
2356    if inner
2357        .closing_connections
2358        .contains(&reservation.client_key.connection_id)
2359    {
2360        return Err(ForwardingError::ConnectionClosing {
2361            connection_id: reservation.client_key.connection_id,
2362        });
2363    }
2364    let module_id = inner
2365        .module_id_by_endpoint
2366        .get(&reservation.module_key.endpoint)
2367        .cloned()
2368        .ok_or(ForwardingError::StaleModuleEndpoint)?;
2369    if inner
2370        .draining_endpoints
2371        .contains_key(&reservation.module_key.endpoint)
2372    {
2373        return Err(ForwardingError::ModuleReloading { module_id });
2374    }
2375    if inner.reserved_client.remove(&reservation.client_key) != Some(reservation.module_key)
2376        || inner.reserved_module.remove(&reservation.module_key) != Some(reservation.client_key)
2377    {
2378        return Err(ForwardingError::UnknownReservation {
2379            client_channel: reservation.client_key.channel,
2380            module_channel: reservation.module_key.channel,
2381        });
2382    }
2383    let module = inner
2384        .modules_by_id
2385        .get(&module_id)
2386        .filter(|module| module.endpoint == reservation.module_key.endpoint)
2387        .cloned()
2388        .ok_or(ForwardingError::StaleModuleEndpoint)?;
2389    let binding = Arc::new(RouteBinding {
2390        client_connection_id: reservation.client_key.connection_id,
2391        client_sink: pending.client_sink,
2392        client_negotiated_ver: pending.client_negotiated_ver,
2393        client_channel: reservation.client_key.channel,
2394        client_epoch: reservation.client_epoch,
2395        module_id,
2396        module_endpoint: reservation.module_key.endpoint,
2397        module_sink: module.sink,
2398        module_negotiated_ver: module.negotiated_ver,
2399        module_channel: reservation.module_key.channel,
2400        module_epoch: reservation.module_epoch,
2401        principal: pending.principal,
2402        project_root: reservation.project_root.clone(),
2403        bound_at: Instant::now(),
2404        flow: Arc::new(ChannelFlow::new(window_for(&module.concurrency))),
2405    });
2406    inner
2407        .client_to_module
2408        .insert(reservation.client_key, Arc::clone(&binding));
2409    inner
2410        .module_to_client
2411        .insert(reservation.module_key, binding);
2412    let previous_published = inner
2413        .last_published_epoch
2414        .insert(reservation.client_key, reservation.client_epoch);
2415
2416    // OwnedPermit::send cannot fail, but its returned sender reveals a receiver
2417    // that closed after reservation and before this locked publication point.
2418    // Stamp at publication: the permit was reserved earlier, but queue residency
2419    // for the reply-write diagnosis starts when the frame actually enters the queue.
2420    let client_sender = pending.client_permit.send(crate::router::OutboundFrame {
2421        frame: pending.route_open_frame,
2422        enqueued_at: std::time::Instant::now(),
2423        flushed: None,
2424    });
2425    if client_sender.is_closed() {
2426        let abandoned = pending
2427            .relay_enqueued
2428            .then(|| abandoned_route_target(inner, &reservation))
2429            .flatten();
2430        if let Some(route) = inner.client_to_module.remove(&reservation.client_key) {
2431            route.flow.close();
2432        }
2433        inner.module_to_client.remove(&reservation.module_key);
2434        inner
2435            .status
2436            .remove(&(reservation.client_key, reservation.client_epoch));
2437        match previous_published {
2438            Some(epoch) => {
2439                inner
2440                    .last_published_epoch
2441                    .insert(reservation.client_key, epoch);
2442            }
2443            None => {
2444                inner.last_published_epoch.remove(&reservation.client_key);
2445            }
2446        }
2447        let _ = pending.sender.send(RouteBindRelayOutcome::ModuleGone(
2448            "client egress closed during route publication".to_string(),
2449        ));
2450        return Ok(abandoned);
2451    }
2452
2453    let _ = pending.sender.send(RouteBindRelayOutcome::Accepted);
2454    Ok(None)
2455}
2456
2457/// The live module connection registered under exactly `endpoint`, whichever
2458/// slot holds it: active, swap candidate, or superseded incumbent.
2459///
2460/// Resolving by endpoint rather than by module id is what keeps an incumbent
2461/// addressable after a swap promoted a candidate over its id. An endpoint that
2462/// is in none of the three (a stale one, replaced without a promotion) resolves
2463/// to nothing, as it always has.
2464fn module_connection_for_endpoint_locked(
2465    inner: &ForwardingInner,
2466    endpoint: ModuleEndpointId,
2467) -> Option<&ModuleConnection> {
2468    let module_id = inner.module_id_by_endpoint.get(&endpoint)?;
2469    inner
2470        .modules_by_id
2471        .get(module_id)
2472        .filter(|module| module.endpoint == endpoint)
2473        .or_else(|| {
2474            inner
2475                .candidates_by_id
2476                .get(module_id)
2477                .filter(|module| module.endpoint == endpoint)
2478        })
2479        .or_else(|| inner.superseded_endpoints.get(&endpoint))
2480}
2481
2482fn abandoned_route_target(
2483    inner: &ForwardingInner,
2484    reservation: &RouteReservation,
2485) -> Option<GoodbyeTarget> {
2486    let module_id = inner
2487        .module_id_by_endpoint
2488        .get(&reservation.module_key.endpoint)?;
2489    let module = module_connection_for_endpoint_locked(inner, reservation.module_key.endpoint)?;
2490    (module.endpoint == reservation.module_key.endpoint).then(|| GoodbyeTarget {
2491        connection_id: module.endpoint.connection_id,
2492        sink: module.sink.clone(),
2493        negotiated_ver: module.negotiated_ver,
2494        channel: reservation.module_key.channel,
2495        epoch: reservation.module_epoch,
2496        kind: GoodbyeTargetKind::Module,
2497        module_id: Some(module_id.clone()),
2498    })
2499}
2500
2501fn remove_module_connection_locked(
2502    inner: &mut ForwardingInner,
2503    endpoint: ModuleEndpointId,
2504) -> Vec<GoodbyeTarget> {
2505    inner.draining_endpoints.remove(&endpoint);
2506    let module_id = inner.module_id_by_endpoint.remove(&endpoint);
2507    if let Some(module_id) = module_id.as_ref() {
2508        if inner
2509            .modules_by_id
2510            .get(module_id)
2511            .is_some_and(|module| module.endpoint == endpoint)
2512        {
2513            inner.modules_by_id.remove(module_id);
2514        }
2515        if inner
2516            .candidates_by_id
2517            .get(module_id)
2518            .is_some_and(|module| module.endpoint == endpoint)
2519        {
2520            inner.candidates_by_id.remove(module_id);
2521        }
2522    }
2523    inner.superseded_endpoints.remove(&endpoint);
2524    inner.endpoint_by_connection.remove(&endpoint.connection_id);
2525    inner.next_module_channel.remove(&endpoint);
2526    inner.next_control_corr.remove(&endpoint);
2527    inner
2528        .health_probe_tombstones
2529        .retain(|(pending_endpoint, _), _| *pending_endpoint != endpoint);
2530    inner
2531        .module_slot_epochs
2532        .retain(|key, _| key.endpoint != endpoint);
2533    let reserved_module_keys: Vec<ModuleRouteKey> = inner
2534        .reserved_module
2535        .keys()
2536        .filter(|module_key| module_key.endpoint == endpoint)
2537        .copied()
2538        .collect();
2539    for module_key in reserved_module_keys {
2540        if let Some(client_key) = inner.reserved_module.get(&module_key).copied() {
2541            release_reserved_route_locked(inner, client_key, module_key);
2542        }
2543    }
2544
2545    let pending_keys: Vec<_> = inner
2546        .pending_relays
2547        .keys()
2548        .filter(|(pending_endpoint, _)| *pending_endpoint == endpoint)
2549        .copied()
2550        .collect();
2551    let pending: Vec<_> = pending_keys
2552        .into_iter()
2553        .filter_map(|key| inner.pending_relays.remove(&key))
2554        .collect();
2555    for pending in pending {
2556        let module_label = module_id.as_deref().unwrap_or("unknown");
2557        let _ = pending
2558            .sender
2559            .send(RouteBindRelayOutcome::ModuleGone(format!(
2560                "module '{module_label}' connection closed during route.bind relay"
2561            )));
2562    }
2563
2564    let pending_control_keys: Vec<_> = inner
2565        .pending_control_rpcs
2566        .keys()
2567        .filter(|(pending_endpoint, _)| *pending_endpoint == endpoint)
2568        .copied()
2569        .collect();
2570    let pending_control: Vec<_> = pending_control_keys
2571        .into_iter()
2572        .filter_map(|key| inner.pending_control_rpcs.remove(&key))
2573        .collect();
2574    for pending in pending_control {
2575        let module_label = module_id.as_deref().unwrap_or("unknown");
2576        let _ = pending
2577            .sender
2578            .send(ModuleControlRpcOutcome::ModuleGone(format!(
2579                "module '{module_label}' connection closed during module-control RPC"
2580            )));
2581    }
2582
2583    let module_routes = inner
2584        .module_to_client
2585        .iter()
2586        .filter(|(module_key, _)| module_key.endpoint == endpoint)
2587        .map(|(module_key, route)| (*module_key, route.module_epoch))
2588        .collect::<Vec<_>>();
2589    let mut released = Vec::with_capacity(module_routes.len());
2590    for (module_key, epoch) in module_routes {
2591        if let RouteRelease::Removed(target) = release_module_route_locked(inner, module_key, epoch)
2592        {
2593            released.push(target);
2594        }
2595    }
2596    released
2597}
2598
2599#[derive(Debug, Clone, Copy)]
2600struct RequestCredit {
2601    subscription: bool,
2602    excluded_from_drain: bool,
2603}
2604
2605#[derive(Debug, Default)]
2606struct CreditLedger {
2607    by_corr: HashMap<u64, Vec<RequestCredit>>,
2608}
2609
2610impl CreditLedger {
2611    fn acquire(&mut self, corr: u64, subscription: bool) {
2612        self.by_corr.entry(corr).or_default().push(RequestCredit {
2613            subscription,
2614            excluded_from_drain: false,
2615        });
2616    }
2617
2618    fn release(&mut self, corr: u64) -> bool {
2619        let Some(credits) = self.by_corr.get_mut(&corr) else {
2620            return false;
2621        };
2622        let released = credits.pop().is_some();
2623        if credits.is_empty() {
2624            self.by_corr.remove(&corr);
2625        }
2626        released
2627    }
2628
2629    fn capture_subscription_exclusions(&mut self) -> u32 {
2630        let mut excluded = 0u32;
2631        for credit in self.by_corr.values_mut().flatten() {
2632            if credit.subscription && !credit.excluded_from_drain {
2633                credit.excluded_from_drain = true;
2634                excluded = excluded.saturating_add(1);
2635            }
2636        }
2637        excluded
2638    }
2639
2640    #[cfg(test)]
2641    fn in_flight(&self) -> usize {
2642        self.by_corr.values().map(Vec::len).sum()
2643    }
2644
2645    fn drain_in_flight(&self) -> usize {
2646        self.by_corr
2647            .values()
2648            .flatten()
2649            .filter(|credit| !credit.excluded_from_drain)
2650            .count()
2651    }
2652
2653    /// The correlation ids of the requests a drain still waits on, one entry
2654    /// per held credit, in ascending order.
2655    fn drain_held_corrs(&self) -> Vec<u64> {
2656        let mut corrs = self
2657            .by_corr
2658            .iter()
2659            .flat_map(|(corr, credits)| {
2660                credits
2661                    .iter()
2662                    .filter(|credit| !credit.excluded_from_drain)
2663                    .map(move |_| *corr)
2664            })
2665            .collect::<Vec<_>>();
2666        corrs.sort_unstable();
2667        corrs
2668    }
2669}
2670
2671#[derive(Debug, Default)]
2672struct ChannelFlowState {
2673    closed: bool,
2674    credits: CreditLedger,
2675}
2676
2677/// Per-channel request-credit accounting shared by the client and module route halves.
2678#[derive(Debug)]
2679pub(crate) struct ChannelFlow {
2680    sem: Semaphore,
2681    window: usize,
2682    state: Mutex<ChannelFlowState>,
2683}
2684
2685impl ChannelFlow {
2686    pub(crate) fn new(window: usize) -> Self {
2687        debug_assert!(window > 0, "flow-control window must be non-zero");
2688        Self {
2689            sem: Semaphore::new(window),
2690            window,
2691            state: Mutex::new(ChannelFlowState::default()),
2692        }
2693    }
2694
2695    #[cfg(test)]
2696    pub(crate) async fn acquire(&self) -> Result<(), ChannelFlowClosed> {
2697        self.acquire_tagged(0, false).await
2698    }
2699
2700    pub(crate) async fn acquire_tagged(
2701        &self,
2702        corr: u64,
2703        subscription: bool,
2704    ) -> Result<(), ChannelFlowClosed> {
2705        let permit = self.sem.acquire().await.map_err(|_| ChannelFlowClosed)?;
2706        let mut state = self
2707            .state
2708            .lock()
2709            .unwrap_or_else(|poisoned| poisoned.into_inner());
2710        if state.closed {
2711            return Err(ChannelFlowClosed);
2712        }
2713        state.credits.acquire(corr, subscription);
2714        permit.forget();
2715        Ok(())
2716    }
2717
2718    #[cfg(test)]
2719    pub(crate) fn release(&self) {
2720        self.release_corr(0);
2721    }
2722
2723    pub(crate) fn release_corr(&self, corr: u64) {
2724        let released = self
2725            .state
2726            .lock()
2727            .unwrap_or_else(|poisoned| poisoned.into_inner())
2728            .credits
2729            .release(corr);
2730        if !released {
2731            // Protocol-conforming modules emit exactly one terminal per request.
2732            // This guard is a best-effort safety net against window growth, not a
2733            // security boundary against malicious peers.
2734            warn!(
2735                window = self.window,
2736                available = self.sem.available_permits(),
2737                "flow-control over-release ignored"
2738            );
2739            return;
2740        }
2741        if !self.sem.is_closed() {
2742            self.sem.add_permits(1);
2743        }
2744    }
2745
2746    #[cfg(test)]
2747    pub(crate) fn in_flight(&self) -> usize {
2748        self.state
2749            .lock()
2750            .unwrap_or_else(|poisoned| poisoned.into_inner())
2751            .credits
2752            .in_flight()
2753    }
2754
2755    pub(crate) fn drain_in_flight(&self) -> usize {
2756        self.state
2757            .lock()
2758            .unwrap_or_else(|poisoned| poisoned.into_inner())
2759            .credits
2760            .drain_in_flight()
2761    }
2762
2763    pub(crate) fn drain_held_corrs(&self) -> Vec<u64> {
2764        self.state
2765            .lock()
2766            .unwrap_or_else(|poisoned| poisoned.into_inner())
2767            .credits
2768            .drain_held_corrs()
2769    }
2770
2771    #[cfg(test)]
2772    pub(crate) fn available_permits(&self) -> usize {
2773        self.sem.available_permits()
2774    }
2775
2776    pub(crate) fn begin_drain(&self) -> u32 {
2777        let mut state = self
2778            .state
2779            .lock()
2780            .unwrap_or_else(|poisoned| poisoned.into_inner());
2781        state.closed = true;
2782        self.sem.close();
2783        state.credits.capture_subscription_exclusions()
2784    }
2785
2786    pub(crate) fn close(&self) {
2787        self.state
2788            .lock()
2789            .unwrap_or_else(|poisoned| poisoned.into_inner())
2790            .closed = true;
2791        self.sem.close();
2792    }
2793}
2794
2795#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2796pub(crate) struct ChannelFlowClosed;
2797
2798impl fmt::Display for ChannelFlowClosed {
2799    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2800        write!(f, "flow-control window closed")
2801    }
2802}
2803
2804impl Error for ChannelFlowClosed {}
2805
2806fn window_for(concurrency: &Concurrency) -> usize {
2807    match concurrency {
2808        Concurrency::Serial => 1,
2809        Concurrency::ModuleManaged => DEFAULT_MODULE_MANAGED_WINDOW,
2810        Concurrency::StatelessParallel => STATELESS_PARALLEL_WINDOW,
2811    }
2812}
2813
2814#[derive(Debug, Clone, PartialEq, Eq)]
2815pub enum ForwardingError {
2816    NoModuleConnection,
2817    ModuleReloading {
2818        module_id: String,
2819    },
2820    StaleModuleEndpoint,
2821    UnknownReservation {
2822        client_channel: u16,
2823        module_channel: u16,
2824    },
2825    ClientRouteChannelExhausted {
2826        connection_id: ConnectionId,
2827    },
2828    ModuleRouteChannelExhausted {
2829        endpoint: ModuleEndpointId,
2830    },
2831    RelayCorrelationExhausted,
2832    ConnectionClosing {
2833        connection_id: ConnectionId,
2834    },
2835    ClientEgressClosed {
2836        connection_id: ConnectionId,
2837    },
2838    RouteOpenBuild(String),
2839    /// A swap candidate is already registered for this module id.
2840    CandidateSlotOccupied {
2841        module_id: String,
2842    },
2843    Poisoned,
2844}
2845
2846impl fmt::Display for ForwardingError {
2847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2848        match self {
2849            Self::NoModuleConnection => write!(f, "no module connection is registered"),
2850            Self::ModuleReloading { module_id } => {
2851                write!(f, "module_id '{module_id}' is reloading")
2852            }
2853            Self::StaleModuleEndpoint => write!(f, "module connection generation is stale"),
2854            Self::UnknownReservation {
2855                client_channel,
2856                module_channel,
2857            } => write!(
2858                f,
2859                "route reservation client channel {client_channel} / module channel {module_channel} was not found"
2860            ),
2861            Self::ClientRouteChannelExhausted { connection_id } => write!(
2862                f,
2863                "no client route channels are available for connection {}",
2864                connection_id.get()
2865            ),
2866            Self::ModuleRouteChannelExhausted { endpoint } => write!(
2867                f,
2868                "no module route channels are available for endpoint generation {} on connection {}",
2869                endpoint.generation,
2870                endpoint.connection_id.get()
2871            ),
2872            Self::RelayCorrelationExhausted => {
2873                write!(f, "module control correlation ids are exhausted")
2874            }
2875            Self::ConnectionClosing { connection_id } => write!(
2876                f,
2877                "connection {} is closing and cannot accept route allocation",
2878                connection_id.get()
2879            ),
2880            Self::ClientEgressClosed { connection_id } => write!(
2881                f,
2882                "client connection {} egress is closed",
2883                connection_id.get()
2884            ),
2885            Self::RouteOpenBuild(message) => {
2886                write!(f, "failed to prebuild route.open response: {message}")
2887            }
2888            Self::CandidateSlotOccupied { module_id } => write!(
2889                f,
2890                "module_id '{module_id}' already has a swap candidate registered"
2891            ),
2892            Self::Poisoned => write!(f, "forwarding table lock was poisoned"),
2893        }
2894    }
2895}
2896
2897impl Error for ForwardingError {}
2898
2899#[cfg(test)]
2900mod tests {
2901    use std::time::Duration;
2902
2903    use super::*;
2904    use tokio::sync::mpsc;
2905
2906    #[test]
2907    fn ordinary_long_running_request_is_not_excluded_from_drain() {
2908        let mut ledger = CreditLedger::default();
2909        ledger.acquire(1, false);
2910
2911        assert_eq!(ledger.capture_subscription_exclusions(), 0);
2912        assert_eq!(ledger.drain_in_flight(), 1);
2913    }
2914
2915    #[test]
2916    fn bit_set_subscription_is_excluded_and_counted() {
2917        let mut ledger = CreditLedger::default();
2918        ledger.acquire(1, true);
2919
2920        assert_eq!(ledger.capture_subscription_exclusions(), 1);
2921        assert_eq!(ledger.drain_in_flight(), 0);
2922    }
2923
2924    #[test]
2925    fn subscription_opened_after_drain_snapshot_is_not_excluded() {
2926        let mut ledger = CreditLedger::default();
2927        ledger.acquire(1, true);
2928        assert_eq!(ledger.capture_subscription_exclusions(), 1);
2929
2930        ledger.acquire(2, true);
2931
2932        assert_eq!(ledger.drain_in_flight(), 1);
2933    }
2934
2935    #[test]
2936    fn drain_with_no_subscriptions_reports_zero_excluded() {
2937        let mut ledger = CreditLedger::default();
2938        assert_eq!(ledger.capture_subscription_exclusions(), 0);
2939    }
2940
2941    #[test]
2942    fn multi_provider_route_limit_reports_per_client_exhaustion_without_affecting_second_client() {
2943        let forwarding = ForwardingTable::default();
2944        let module_connection = ConnectionId::new(10);
2945        let exhausted_client = ConnectionId::new(20);
2946        let second_client = ConnectionId::new(30);
2947        let (module_tx, _module_rx) = mpsc::channel(1);
2948        let endpoint = forwarding
2949            .register_module_connection(
2950                module_connection,
2951                "route-limit-provider".to_string(),
2952                1,
2953                Concurrency::ModuleManaged,
2954                FrameSink::new(module_tx),
2955            )
2956            .unwrap();
2957
2958        {
2959            let mut inner = forwarding.inner.write().unwrap();
2960            for channel in 1..=u16::MAX {
2961                inner.reserved_client.insert(
2962                    ClientRouteKey {
2963                        connection_id: exhausted_client,
2964                        channel,
2965                    },
2966                    ModuleRouteKey {
2967                        endpoint,
2968                        channel: 1,
2969                    },
2970                );
2971            }
2972        }
2973
2974        let (exhausted_tx, _exhausted_rx) = mpsc::channel(1);
2975        let err = forwarding
2976            .begin_route_bind_relay_for_test(
2977                exhausted_client,
2978                FrameSink::new(exhausted_tx),
2979                1,
2980                "route-limit-provider",
2981            )
2982            .unwrap_err();
2983        assert!(matches!(
2984            err,
2985            ForwardingError::ClientRouteChannelExhausted { connection_id }
2986                if connection_id == exhausted_client
2987        ));
2988
2989        let (second_tx, _second_rx) = mpsc::channel(1);
2990        let pending = forwarding
2991            .begin_route_bind_relay_for_test(
2992                second_client,
2993                FrameSink::new(second_tx),
2994                2,
2995                "route-limit-provider",
2996            )
2997            .unwrap();
2998        assert_eq!(pending.client_channel, 1);
2999    }
3000
3001    #[test]
3002    fn released_module_channels_are_reused_after_wrap_without_slot_leak() {
3003        let forwarding = ForwardingTable::default();
3004        let module_connection = ConnectionId::new(40);
3005        let client = ConnectionId::new(50);
3006        let (module_tx, _module_rx) = mpsc::channel(1);
3007        forwarding
3008            .register_module_connection(
3009                module_connection,
3010                "slot-reuse-provider".to_string(),
3011                1,
3012                Concurrency::ModuleManaged,
3013                FrameSink::new(module_tx),
3014            )
3015            .unwrap();
3016
3017        let (client_tx, _client_rx) = mpsc::channel(1);
3018        let client_sink = FrameSink::new(client_tx);
3019        let mut wrapped_channel = None;
3020        for index in 0..=usize::from(u16::MAX) {
3021            let pending = forwarding
3022                .begin_route_bind_relay_for_test(
3023                    client,
3024                    client_sink.clone(),
3025                    index as u64 + 1,
3026                    "slot-reuse-provider",
3027                )
3028                .unwrap();
3029            if index == usize::from(u16::MAX) {
3030                wrapped_channel = Some(pending.module_channel);
3031            }
3032            forwarding
3033                .abort_pending_relay(
3034                    pending.endpoint,
3035                    pending.corr,
3036                    RouteBindRelayOutcome::ModuleGone("test abort".to_string()),
3037                )
3038                .unwrap();
3039        }
3040
3041        assert_eq!(wrapped_channel, Some(1));
3042    }
3043
3044    #[test]
3045    fn cleanup_connection_prunes_stale_next_client_channel_cursor() {
3046        let forwarding = ForwardingTable::default();
3047        let client = ConnectionId::new(60);
3048        forwarding
3049            .inner
3050            .write()
3051            .unwrap()
3052            .next_client_channel
3053            .insert(client, 41);
3054
3055        let released = forwarding.cleanup_connection(client).unwrap();
3056
3057        assert!(released.is_empty());
3058        assert!(!forwarding
3059            .inner
3060            .read()
3061            .unwrap()
3062            .next_client_channel
3063            .contains_key(&client));
3064    }
3065
3066    #[test]
3067    fn stale_module_cleanup_preserves_fast_reconnect_successor() {
3068        let forwarding = ForwardingTable::default();
3069        let module_id = "fast-reconnect-provider";
3070        let first_connection = ConnectionId::new(70);
3071        let second_connection = ConnectionId::new(80);
3072        let (first_tx, _first_rx) = mpsc::channel(1);
3073        let first_endpoint = forwarding
3074            .register_module_connection(
3075                first_connection,
3076                module_id.to_string(),
3077                1,
3078                Concurrency::ModuleManaged,
3079                FrameSink::new(first_tx),
3080            )
3081            .unwrap();
3082        let (second_tx, _second_rx) = mpsc::channel(1);
3083        let second_endpoint = forwarding
3084            .register_module_connection(
3085                second_connection,
3086                module_id.to_string(),
3087                1,
3088                Concurrency::ModuleManaged,
3089                FrameSink::new(second_tx),
3090            )
3091            .unwrap();
3092        assert_ne!(first_endpoint, second_endpoint);
3093
3094        let released = forwarding.cleanup_connection(first_connection).unwrap();
3095
3096        assert!(released.is_empty());
3097        assert_eq!(
3098            forwarding
3099                .inner
3100                .read()
3101                .unwrap()
3102                .modules_by_id
3103                .get(module_id)
3104                .map(|module| module.endpoint),
3105            Some(second_endpoint)
3106        );
3107        assert!(forwarding.has_live_module_connection(module_id).unwrap());
3108        let control_rpc = forwarding
3109            .begin_module_control_rpc_for(
3110                module_id,
3111                "health.check",
3112                Instant::now() + Duration::from_secs(1),
3113            )
3114            .unwrap();
3115        assert_eq!(control_rpc.endpoint, second_endpoint);
3116    }
3117
3118    fn route_fixture(
3119        module_id: &str,
3120    ) -> (
3121        ForwardingTable,
3122        ConnectionId,
3123        ModuleEndpointId,
3124        ConnectionId,
3125        FrameSink,
3126        mpsc::Receiver<crate::router::OutboundFrame>,
3127    ) {
3128        let forwarding = ForwardingTable::default();
3129        let module_connection = ConnectionId::new(100);
3130        let client_connection = ConnectionId::new(200);
3131        let (module_tx, _module_rx) = mpsc::channel(8);
3132        let endpoint = forwarding
3133            .register_module_connection(
3134                module_connection,
3135                module_id.to_string(),
3136                2,
3137                Concurrency::ModuleManaged,
3138                FrameSink::new(module_tx),
3139            )
3140            .unwrap();
3141        let (client_tx, client_rx) = mpsc::channel(8);
3142        (
3143            forwarding,
3144            module_connection,
3145            endpoint,
3146            client_connection,
3147            FrameSink::new(client_tx),
3148            client_rx,
3149        )
3150    }
3151
3152    #[test]
3153    #[cfg(unix)]
3154    fn daemon_drain_gates_current_and_racing_provider_registrations() {
3155        let (forwarding, _, endpoint, _, sink, _) = route_fixture("provider");
3156        assert_eq!(forwarding.begin_daemon_drain().unwrap(), ["provider"]);
3157        assert!(forwarding.endpoint_is_draining(endpoint).unwrap());
3158        assert!(matches!(
3159            forwarding.register_module_connection(
3160                ConnectionId::new(300),
3161                "late-provider".into(),
3162                2,
3163                Concurrency::ModuleManaged,
3164                sink,
3165            ),
3166            Err(ForwardingError::ConnectionClosing { .. })
3167        ));
3168    }
3169
3170    fn test_ping(corr: u64) -> Frame {
3171        Frame::build(
3172            FrameType::Ping,
3173            Flags::new(false, Priority::Passive, false),
3174            0,
3175            0,
3176            corr,
3177            Vec::new(),
3178        )
3179        .unwrap()
3180    }
3181
3182    fn begin_test_route(
3183        forwarding: &ForwardingTable,
3184        client_connection: ConnectionId,
3185        client_sink: FrameSink,
3186        corr: u64,
3187        module_id: &str,
3188    ) -> PendingRouteBindRelay {
3189        forwarding
3190            .begin_route_bind_relay_for_test(client_connection, client_sink, corr, module_id)
3191            .unwrap()
3192    }
3193
3194    /// The drain-timeout line reports these numbers, so they must count the
3195    /// requests the drain is waiting on and no others: a route with nothing in
3196    /// flight is not a holdout, and a flagged subscription is excluded from the
3197    /// drain and so from the count.
3198    #[tokio::test]
3199    async fn drain_holdouts_count_held_requests_and_name_the_connection() {
3200        let (forwarding, module_connection, endpoint, client, sink, mut client_rx) =
3201            route_fixture("holdouts");
3202        let mut bound = |corr| {
3203            let route = begin_test_route(&forwarding, client, sink.clone(), corr, "holdouts");
3204            forwarding
3205                .complete_pending_relay(
3206                    module_connection,
3207                    route.corr,
3208                    RouteBindRelayOutcome::Accepted,
3209                )
3210                .unwrap();
3211            client_rx.try_recv().unwrap();
3212            match forwarding
3213                .lookup_data_route(client, route.client_channel, route.client_epoch)
3214                .unwrap()
3215            {
3216                DataRoute::Client(DataRouteState::Bound(binding)) => binding,
3217                other => panic!("expected live route, got {other:?}"),
3218            }
3219        };
3220        let holding = bound(61);
3221        let _idle = bound(62);
3222        holding.flow.acquire_tagged(7, false).await.unwrap();
3223        holding.flow.acquire_tagged(2, false).await.unwrap();
3224        holding.flow.acquire_tagged(3, true).await.unwrap();
3225        forwarding
3226            .begin_module_drain("holdouts", RouteCloseReason::Restart)
3227            .unwrap();
3228
3229        let holdouts = forwarding.endpoint_drain_holdouts(endpoint).unwrap();
3230        assert_eq!(
3231            holdouts,
3232            DrainHoldouts {
3233                requests: 2,
3234                routes: 1,
3235                total_routes: 2,
3236                top_connections: vec![(client.get(), 2)],
3237                // The held requests by the module's channel and corr, ascending,
3238                // without the excluded subscription (corr 3).
3239                held: vec![(holding.module_channel, 2), (holding.module_channel, 7)],
3240            }
3241        );
3242    }
3243
3244    #[test]
3245    fn endpoint_routes_keep_goodbye_targets_and_mark_draining_routes() {
3246        let (forwarding, module_connection, endpoint, client, sink, _client_rx) =
3247            route_fixture("census");
3248        let pending = begin_test_route(&forwarding, client, sink, 1, "census");
3249        forwarding
3250            .complete_pending_relay(
3251                module_connection,
3252                pending.corr,
3253                RouteBindRelayOutcome::Accepted,
3254            )
3255            .unwrap();
3256
3257        let routes = forwarding.endpoint_routes(endpoint).unwrap();
3258        assert_eq!(routes.len(), 1);
3259        assert!(matches!(routes[0].principal, Principal::Direct));
3260        assert_eq!(routes[0].goodbye_target.connection_id, client);
3261        assert_eq!(routes[0].goodbye_target.channel, pending.client_channel);
3262        assert_eq!(routes[0].goodbye_target.epoch, pending.client_epoch);
3263        assert!(!routes[0].draining);
3264
3265        forwarding
3266            .begin_module_drain("census", RouteCloseReason::Restart)
3267            .unwrap();
3268        let draining_routes = forwarding.endpoint_routes(endpoint).unwrap();
3269        assert_eq!(draining_routes.len(), 1);
3270        assert!(draining_routes[0].draining);
3271    }
3272
3273    #[test]
3274    fn aborted_reservation_consumes_both_epochs_and_reuse_advances_them() {
3275        let (forwarding, _, endpoint, client, sink, _client_rx) = route_fixture("epoch-abort");
3276        let first = begin_test_route(&forwarding, client, sink.clone(), 1, "epoch-abort");
3277        assert_eq!((first.client_epoch, first.module_epoch), (1, 1));
3278        forwarding
3279            .abort_pending_relay(
3280                first.endpoint,
3281                first.corr,
3282                RouteBindRelayOutcome::ModuleGone("abort".into()),
3283            )
3284            .unwrap();
3285        forwarding.inject_client_slot_epoch(client, first.client_channel, first.client_epoch);
3286        forwarding.inject_module_slot_epoch(endpoint, first.module_channel, first.module_epoch);
3287
3288        let second = begin_test_route(&forwarding, client, sink, 2, "epoch-abort");
3289        assert_eq!(second.client_channel, first.client_channel);
3290        assert_eq!(second.module_channel, first.module_channel);
3291        assert_eq!((second.client_epoch, second.module_epoch), (2, 2));
3292    }
3293
3294    #[test]
3295    fn stale_release_cannot_remove_reused_successor_and_status_is_epoch_fenced() {
3296        let (forwarding, module_connection, endpoint, client, sink, mut client_rx) =
3297            route_fixture("epoch-release");
3298        let first = begin_test_route(&forwarding, client, sink.clone(), 10, "epoch-release");
3299        forwarding
3300            .complete_pending_relay(
3301                module_connection,
3302                first.corr,
3303                RouteBindRelayOutcome::Accepted,
3304            )
3305            .unwrap();
3306        assert_eq!(client_rx.try_recv().unwrap().header.corr, 10);
3307        assert!(matches!(
3308            forwarding
3309                .release_client_route(client, first.client_channel, first.client_epoch)
3310                .unwrap(),
3311            RouteRelease::Removed(_)
3312        ));
3313        forwarding.inject_client_slot_epoch(client, first.client_channel, first.client_epoch);
3314        forwarding.inject_module_slot_epoch(endpoint, first.module_channel, first.module_epoch);
3315
3316        let second = begin_test_route(&forwarding, client, sink, 11, "epoch-release");
3317        forwarding
3318            .complete_pending_relay(
3319                module_connection,
3320                second.corr,
3321                RouteBindRelayOutcome::Accepted,
3322            )
3323            .unwrap();
3324        assert_eq!(client_rx.try_recv().unwrap().header.corr, 11);
3325        assert!(matches!(
3326            forwarding
3327                .release_client_route(client, second.client_channel, first.client_epoch)
3328                .unwrap(),
3329            RouteRelease::Stale
3330        ));
3331        assert!(!forwarding
3332            .cache_status(
3333                endpoint,
3334                second.module_channel,
3335                first.module_epoch,
3336                "stale".into(),
3337            )
3338            .unwrap());
3339        assert!(forwarding
3340            .cache_status(
3341                endpoint,
3342                second.module_channel,
3343                second.module_epoch,
3344                "current".into(),
3345            )
3346            .unwrap());
3347        match forwarding
3348            .route_poll_snapshot(client, second.client_channel, second.client_epoch)
3349            .unwrap()
3350        {
3351            RoutePollSnapshot::Bound { status, .. } => {
3352                assert_eq!(status.as_deref(), Some("current"));
3353            }
3354            RoutePollSnapshot::Absent => panic!("successor binding was removed"),
3355        }
3356        let counters = forwarding.counters().snapshot();
3357        assert_eq!(counters["route_released_epoch_fenced"], 1);
3358        assert_eq!(counters["route_release_stale_skipped"], 1);
3359    }
3360
3361    #[test]
3362    fn max_epoch_reservation_retires_only_that_slot() {
3363        let (forwarding, _, endpoint, client, sink, _client_rx) = route_fixture("epoch-max");
3364        forwarding.inject_client_slot_epoch(client, 7, u32::MAX - 1);
3365        forwarding.inject_module_slot_epoch(endpoint, 9, u32::MAX - 1);
3366        let final_use = begin_test_route(&forwarding, client, sink.clone(), 20, "epoch-max");
3367        assert_eq!(
3368            (final_use.client_channel, final_use.client_epoch),
3369            (7, u32::MAX)
3370        );
3371        assert_eq!(
3372            (final_use.module_channel, final_use.module_epoch),
3373            (9, u32::MAX)
3374        );
3375        forwarding
3376            .abort_pending_relay(
3377                endpoint,
3378                final_use.corr,
3379                RouteBindRelayOutcome::ModuleGone("abort".into()),
3380            )
3381            .unwrap();
3382        forwarding.inject_client_slot_epoch(client, 7, u32::MAX);
3383        forwarding.inject_module_slot_epoch(endpoint, 9, u32::MAX);
3384        let next = begin_test_route(&forwarding, client, sink, 21, "epoch-max");
3385        assert_ne!(next.client_channel, 7);
3386        assert_ne!(next.module_channel, 9);
3387        assert_eq!((next.client_epoch, next.module_epoch), (1, 1));
3388    }
3389
3390    #[test]
3391    fn bind_and_module_control_share_monotonic_corr_and_deadline_arbitration() {
3392        let (forwarding, module_connection, endpoint, client, sink, _client_rx) =
3393            route_fixture("corr-shared");
3394        let bind = begin_test_route(&forwarding, client, sink, 30, "corr-shared");
3395        assert_eq!(bind.corr, 1);
3396        forwarding
3397            .abort_pending_relay(
3398                endpoint,
3399                bind.corr,
3400                RouteBindRelayOutcome::ModuleGone("abort".into()),
3401            )
3402            .unwrap();
3403        let rpc = forwarding
3404            .begin_module_control_rpc_for(
3405                "corr-shared",
3406                "health.check",
3407                Instant::now() - Duration::from_millis(1),
3408            )
3409            .unwrap();
3410        assert_eq!(rpc.corr, 2);
3411        assert_eq!(
3412            forwarding
3413                .complete_module_control_rpc(
3414                    module_connection,
3415                    rpc.corr,
3416                    Some("health.check"),
3417                    ModuleControlRpcOutcome::Response(ModuleControlResponse::HealthCheck {
3418                        status: subc_protocol::session::HealthStatus::Ok,
3419                        detail: None,
3420                        metrics: None,
3421                    }),
3422                )
3423                .unwrap(),
3424            ModuleControlRpcCompletion::Settled
3425        );
3426        assert!(matches!(
3427            rpc.receiver.blocking_recv().unwrap(),
3428            ModuleControlRpcOutcome::DeadlineElapsed
3429        ));
3430    }
3431
3432    #[tokio::test(start_paused = true)]
3433    async fn health_probe_tombstone_ttl_removes_an_endpoint_that_stops_probing() {
3434        let (forwarding, _, endpoint, _, _, _) = route_fixture("tombstone-ttl");
3435        let probe_started_at = Instant::now();
3436        let rpc = forwarding
3437            .begin_health_probe_rpc_for(
3438                "tombstone-ttl",
3439                "health.check",
3440                probe_started_at,
3441                probe_started_at + Duration::from_secs(5),
3442            )
3443            .unwrap();
3444        assert!(forwarding
3445            .tombstone_health_probe_rpc(endpoint, rpc.corr)
3446            .unwrap());
3447        assert_eq!(forwarding.health_probe_tombstone_count().unwrap(), 1);
3448
3449        tokio::time::advance(HEALTH_PROBE_TOMBSTONE_TTL).await;
3450        tokio::task::yield_now().await;
3451
3452        assert_eq!(forwarding.health_probe_tombstone_count().unwrap(), 0);
3453    }
3454
3455    #[test]
3456    fn correlation_exhaustion_emits_max_once_then_closes_endpoint() {
3457        let (forwarding, _, endpoint, _, _, _) = route_fixture("corr-max");
3458        let mut close = forwarding.register_connection_close(endpoint.connection_id);
3459        forwarding.inject_control_corr(endpoint, u64::MAX);
3460        let final_rpc = forwarding
3461            .begin_module_control_rpc_for(
3462                "corr-max",
3463                "health.check",
3464                Instant::now() + Duration::from_secs(1),
3465            )
3466            .unwrap();
3467        assert_eq!(final_rpc.corr, u64::MAX);
3468        forwarding
3469            .cancel_module_control_rpc(endpoint, final_rpc.corr)
3470            .unwrap();
3471        assert!(matches!(
3472            forwarding.begin_module_control_rpc_for(
3473                "corr-max",
3474                "health.check",
3475                Instant::now() + Duration::from_secs(1),
3476            ),
3477            Err(ForwardingError::RelayCorrelationExhausted)
3478        ));
3479        assert!(close.try_recv().is_ok());
3480    }
3481
3482    #[test]
3483    fn publication_epoch_controls_delivery_failure_escalation() {
3484        fn setup_successor(
3485            commit_successor: Option<bool>,
3486        ) -> (ForwardingTable, ConnectionId, u16, u32) {
3487            let (forwarding, module_connection, endpoint, client, sink, mut client_rx) =
3488                route_fixture("escalation");
3489            let first = begin_test_route(&forwarding, client, sink.clone(), 40, "escalation");
3490            forwarding
3491                .complete_pending_relay(
3492                    module_connection,
3493                    first.corr,
3494                    RouteBindRelayOutcome::Accepted,
3495                )
3496                .unwrap();
3497            client_rx.try_recv().unwrap();
3498            assert!(matches!(
3499                forwarding
3500                    .release_client_route(client, first.client_channel, first.client_epoch)
3501                    .unwrap(),
3502                RouteRelease::Removed(_)
3503            ));
3504            if let Some(commit_successor) = commit_successor {
3505                forwarding.inject_client_slot_epoch(
3506                    client,
3507                    first.client_channel,
3508                    first.client_epoch,
3509                );
3510                forwarding.inject_module_slot_epoch(
3511                    endpoint,
3512                    first.module_channel,
3513                    first.module_epoch,
3514                );
3515                let successor = begin_test_route(&forwarding, client, sink, 41, "escalation");
3516                if commit_successor {
3517                    forwarding
3518                        .complete_pending_relay(
3519                            module_connection,
3520                            successor.corr,
3521                            RouteBindRelayOutcome::Accepted,
3522                        )
3523                        .unwrap();
3524                    client_rx.try_recv().unwrap();
3525                } else {
3526                    forwarding
3527                        .abort_pending_relay(
3528                            endpoint,
3529                            successor.corr,
3530                            RouteBindRelayOutcome::ModuleGone("abort".into()),
3531                        )
3532                        .unwrap();
3533                }
3534            }
3535            (forwarding, client, first.client_channel, first.client_epoch)
3536        }
3537
3538        let (no_successor, client, channel, epoch) = setup_successor(None);
3539        let mut close = no_successor.register_connection_close(client);
3540        assert!(no_successor
3541            .escalate_client_delivery_failure(
3542                client,
3543                channel,
3544                epoch,
3545                CloseReason::new("delivery", "failed"),
3546            )
3547            .unwrap());
3548        assert!(close.try_recv().is_ok());
3549
3550        let (aborted, client, channel, epoch) = setup_successor(Some(false));
3551        let mut close = aborted.register_connection_close(client);
3552        assert!(aborted
3553            .escalate_client_delivery_failure(
3554                client,
3555                channel,
3556                epoch,
3557                CloseReason::new("delivery", "failed"),
3558            )
3559            .unwrap());
3560        assert!(close.try_recv().is_ok());
3561
3562        let (published, client, channel, epoch) = setup_successor(Some(true));
3563        let mut close = published.register_connection_close(client);
3564        assert!(!published
3565            .escalate_client_delivery_failure(
3566                client,
3567                channel,
3568                epoch,
3569                CloseReason::new("delivery", "stale failure"),
3570            )
3571            .unwrap());
3572        assert!(close.try_recv().is_err());
3573    }
3574
3575    #[test]
3576    fn route_concentration_separates_client_count_from_routes_per_client() {
3577        // The distinction this asserts is the one a bare connection count cannot
3578        // make: two connections holding one route each and one connection
3579        // holding two are the same total, and have opposite causes.
3580        let (forwarding, module_connection, _, client, sink, _client_rx) =
3581            route_fixture("concentration");
3582        assert_eq!(forwarding.client_route_concentration().unwrap(), (0, 0));
3583
3584        for corr in [70_u64, 71] {
3585            let pending =
3586                begin_test_route(&forwarding, client, sink.clone(), corr, "concentration");
3587            forwarding
3588                .complete_pending_relay(
3589                    module_connection,
3590                    pending.corr,
3591                    RouteBindRelayOutcome::Accepted,
3592                )
3593                .unwrap();
3594        }
3595
3596        // One connection, two routes — not two connections with a route each.
3597        assert_eq!(forwarding.active_binding_count().unwrap(), 2);
3598        assert_eq!(forwarding.client_route_concentration().unwrap(), (1, 2));
3599    }
3600
3601    #[test]
3602    fn cleanup_and_accepted_resolution_have_one_lock_winner() {
3603        let (forwarding, module_connection, _, client, sink, mut client_rx) =
3604            route_fixture("cleanup-race");
3605        let pending = begin_test_route(&forwarding, client, sink, 45, "cleanup-race");
3606        forwarding
3607            .mark_route_bind_relay_enqueued(pending.endpoint, pending.corr)
3608            .unwrap();
3609        let released = forwarding.cleanup_connection(client).unwrap();
3610        assert_eq!(released.len(), 1);
3611        let completion = forwarding
3612            .complete_pending_relay(
3613                module_connection,
3614                pending.corr,
3615                RouteBindRelayOutcome::Accepted,
3616            )
3617            .unwrap();
3618        assert!(!completion.settled);
3619        assert!(client_rx.try_recv().is_err());
3620        assert_eq!(forwarding.active_binding_count().unwrap(), 0);
3621
3622        let (forwarding, module_connection, _, client, sink, mut client_rx) =
3623            route_fixture("accepted-race");
3624        let pending = begin_test_route(&forwarding, client, sink, 46, "accepted-race");
3625        forwarding
3626            .complete_pending_relay(
3627                module_connection,
3628                pending.corr,
3629                RouteBindRelayOutcome::Accepted,
3630            )
3631            .unwrap();
3632        assert_eq!(client_rx.try_recv().unwrap().header.corr, 46);
3633        let released = forwarding.cleanup_connection(client).unwrap();
3634        assert_eq!(released.len(), 1);
3635        assert_eq!(forwarding.active_binding_count().unwrap(), 0);
3636    }
3637
3638    #[test]
3639    fn drain_marks_block_reservation_commit_and_live_request_admission_until_phase_two() {
3640        let (forwarding, module_connection, _, client, sink, mut client_rx) =
3641            route_fixture("drain-gap");
3642        let live = begin_test_route(&forwarding, client, sink.clone(), 47, "drain-gap");
3643        forwarding
3644            .complete_pending_relay(
3645                module_connection,
3646                live.corr,
3647                RouteBindRelayOutcome::Accepted,
3648            )
3649            .unwrap();
3650        client_rx.try_recv().unwrap();
3651        let binding = match forwarding
3652            .lookup_data_route(client, live.client_channel, live.client_epoch)
3653            .unwrap()
3654        {
3655            DataRoute::Client(DataRouteState::Bound(binding)) => binding,
3656            other => panic!("expected live route, got {other:?}"),
3657        };
3658
3659        let pending = begin_test_route(&forwarding, client, sink.clone(), 48, "drain-gap");
3660        forwarding
3661            .mark_route_bind_relay_enqueued(pending.endpoint, pending.corr)
3662            .unwrap();
3663        let control_rpc = forwarding
3664            .begin_module_control_rpc_for(
3665                "drain-gap",
3666                "health.check",
3667                Instant::now() + Duration::from_secs(1),
3668            )
3669            .unwrap();
3670        let target = forwarding
3671            .begin_module_drain("drain-gap", RouteCloseReason::Reload)
3672            .unwrap()
3673            .unwrap();
3674        assert!(matches!(
3675            control_rpc.receiver.blocking_recv().unwrap(),
3676            ModuleControlRpcOutcome::ModuleGone(_)
3677        ));
3678        assert_eq!(target.abandoned_bindings.len(), 1);
3679        assert!(binding.flow.sem.is_closed());
3680        assert!(
3681            !forwarding
3682                .complete_pending_relay(
3683                    module_connection,
3684                    pending.corr,
3685                    RouteBindRelayOutcome::Accepted,
3686                )
3687                .unwrap()
3688                .settled
3689        );
3690        assert!(matches!(
3691            forwarding.begin_route_bind_relay_for_test(client, sink, 49, "drain-gap"),
3692            Err(ForwardingError::ModuleReloading { .. })
3693        ));
3694        let released = forwarding
3695            .release_module_endpoint_routes(target.endpoint)
3696            .unwrap();
3697        assert_eq!(released.len(), 1);
3698        assert_eq!(forwarding.active_binding_count().unwrap(), 0);
3699    }
3700
3701    /// A client can be marked closing while its egress is still open: the daemon
3702    /// asks a connection to close (here through the production path, a module
3703    /// frame that its egress refused) and the connection loop tears down a moment
3704    /// later. A route.bind ack that lands inside that window is answered on the
3705    /// MODULE connection's frame handler, so resolving it must not produce an
3706    /// error -- an error there ends the module connection, and that connection
3707    /// carries every other client's routes to the module.
3708    #[test]
3709    fn accepted_bind_for_a_closing_client_releases_the_route_instead_of_failing_the_module() {
3710        let (forwarding, module_connection, endpoint, client, sink, mut client_rx) =
3711            route_fixture("closing-client");
3712
3713        // A published route on this client: escalate_client_delivery_failure only
3714        // marks a connection closing for a route it has already published.
3715        let live = begin_test_route(&forwarding, client, sink.clone(), 60, "closing-client");
3716        forwarding
3717            .complete_pending_relay(
3718                module_connection,
3719                live.corr,
3720                RouteBindRelayOutcome::Accepted,
3721            )
3722            .unwrap();
3723        client_rx.try_recv().unwrap();
3724
3725        // A second route.open from the same client, relayed and awaiting its ack.
3726        let pending = begin_test_route(&forwarding, client, sink.clone(), 61, "closing-client");
3727        forwarding
3728            .mark_route_bind_relay_enqueued(pending.endpoint, pending.corr)
3729            .unwrap();
3730
3731        // The window: closing, but the sink is still open.
3732        assert!(forwarding
3733            .escalate_client_delivery_failure(
3734                client,
3735                live.client_channel,
3736                live.client_epoch,
3737                CloseReason::new(
3738                    "module_to_client_delivery_failed",
3739                    "client egress refused a module frame",
3740                ),
3741            )
3742            .unwrap());
3743        assert!(!sink.is_closed());
3744
3745        let completion = forwarding
3746            .complete_pending_relay(
3747                module_connection,
3748                pending.corr,
3749                RouteBindRelayOutcome::Accepted,
3750            )
3751            .expect("a closing client must not turn a module's ack into an error");
3752
3753        assert!(completion.settled);
3754        let abandoned = completion
3755            .abandoned
3756            .expect("the module must be told to drop the binding it just created");
3757        assert_eq!(abandoned.connection_id, module_connection);
3758        assert_eq!(abandoned.channel, pending.module_channel);
3759        assert_eq!(abandoned.epoch, pending.module_epoch);
3760        assert!(matches!(abandoned.kind, GoodbyeTargetKind::Module));
3761        assert!(matches!(
3762            pending.receiver.blocking_recv().unwrap(),
3763            RouteBindRelayOutcome::ModuleGone(_)
3764        ));
3765        // No route was published to a client that is on its way out, and the
3766        // reserved handle pair went back.
3767        assert!(client_rx.try_recv().is_err());
3768        assert_eq!(forwarding.active_binding_count().unwrap(), 1);
3769
3770        // The module endpoint is untouched: still live, and still able to take a
3771        // route from another client.
3772        assert!(forwarding
3773            .has_live_module_connection("closing-client")
3774            .unwrap());
3775        let cotenant = ConnectionId::new(201);
3776        let (cotenant_tx, mut cotenant_rx) = mpsc::channel(8);
3777        let cotenant_route = begin_test_route(
3778            &forwarding,
3779            cotenant,
3780            FrameSink::new(cotenant_tx),
3781            62,
3782            "closing-client",
3783        );
3784        assert_eq!(cotenant_route.endpoint, endpoint);
3785        forwarding
3786            .complete_pending_relay(
3787                module_connection,
3788                cotenant_route.corr,
3789                RouteBindRelayOutcome::Accepted,
3790            )
3791            .unwrap();
3792        assert_eq!(cotenant_rx.try_recv().unwrap().header.corr, 62);
3793        assert_eq!(forwarding.active_binding_count().unwrap(), 2);
3794    }
3795
3796    #[test]
3797    fn pending_route_permit_is_released_on_rejection_and_abort() {
3798        let forwarding = ForwardingTable::default();
3799        let module_connection = ConnectionId::new(300);
3800        let client = ConnectionId::new(301);
3801        let (module_tx, _module_rx) = mpsc::channel(1);
3802        let endpoint = forwarding
3803            .register_module_connection(
3804                module_connection,
3805                "permit".into(),
3806                2,
3807                Concurrency::ModuleManaged,
3808                FrameSink::new(module_tx),
3809            )
3810            .unwrap();
3811        let (client_tx, mut client_rx) = mpsc::channel(1);
3812        let sink = FrameSink::new(client_tx);
3813        let rejected = begin_test_route(&forwarding, client, sink.clone(), 50, "permit");
3814        assert!(sink.try_send(test_ping(999)).is_err());
3815        forwarding
3816            .complete_pending_relay(
3817                module_connection,
3818                rejected.corr,
3819                RouteBindRelayOutcome::Rejected(ErrorBody {
3820                    code: "no".into(),
3821                    message: "rejected".into(),
3822                    detail: None,
3823                }),
3824            )
3825            .unwrap();
3826        sink.try_send(test_ping(1000)).unwrap();
3827        assert_eq!(client_rx.try_recv().unwrap().header.corr, 1000);
3828
3829        let aborted = begin_test_route(&forwarding, client, sink.clone(), 51, "permit");
3830        assert!(sink.try_send(test_ping(1001)).is_err());
3831        forwarding
3832            .abort_pending_relay(
3833                endpoint,
3834                aborted.corr,
3835                RouteBindRelayOutcome::ModuleGone("abort".into()),
3836            )
3837            .unwrap();
3838        sink.try_send(test_ping(1002)).unwrap();
3839        assert_eq!(client_rx.try_recv().unwrap().header.corr, 1002);
3840
3841        let receiver_closed = begin_test_route(&forwarding, client, sink, 52, "permit");
3842        forwarding
3843            .mark_route_bind_relay_enqueued(endpoint, receiver_closed.corr)
3844            .unwrap();
3845        drop(client_rx);
3846        let completion = forwarding
3847            .complete_pending_relay(
3848                module_connection,
3849                receiver_closed.corr,
3850                RouteBindRelayOutcome::Accepted,
3851            )
3852            .unwrap();
3853        assert!(completion.abandoned.is_some());
3854        assert_eq!(forwarding.active_binding_count().unwrap(), 0);
3855    }
3856
3857    /// Every connection teardown passes through `cleanup_connection`, and
3858    /// connection ids come from a monotonic counter that never hands an id out
3859    /// twice. If the closing mark survives teardown, the set grows by one entry
3860    /// per connection for the life of the daemon -- the self-watchdog alone
3861    /// reconnects once a minute.
3862    #[test]
3863    fn cleaned_up_connections_do_not_stay_in_the_closing_set() {
3864        let (forwarding, module_connection, _endpoint, _fixture_client, _sink, _rx) =
3865            route_fixture("closing-set-leak");
3866
3867        const CONNECTIONS: u64 = 32;
3868        for index in 0..CONNECTIONS {
3869            let client = ConnectionId::new(1000 + index);
3870            let (client_tx, _client_rx) = mpsc::channel(8);
3871            let route = begin_test_route(
3872                &forwarding,
3873                client,
3874                FrameSink::new(client_tx),
3875                index + 1,
3876                "closing-set-leak",
3877            );
3878            forwarding
3879                .complete_pending_relay(
3880                    module_connection,
3881                    route.corr,
3882                    RouteBindRelayOutcome::Accepted,
3883                )
3884                .unwrap();
3885            forwarding.cleanup_connection(client).unwrap();
3886        }
3887        forwarding.cleanup_connection(module_connection).unwrap();
3888
3889        assert_eq!(forwarding.closing_connection_count().unwrap(), 0);
3890    }
3891
3892    /// The closing mark exists to refuse new work for a connection that is on
3893    /// its way out but whose teardown has not run yet: the daemon asks the
3894    /// connection loop to end, and only when the loop reacts does
3895    /// `cleanup_connection` strip the connection's state. Inside that window an
3896    /// operation for the dying connection must still be refused; only after
3897    /// cleanup completes may the mark go.
3898    #[test]
3899    fn closing_connection_is_refused_new_work_until_cleanup_completes() {
3900        let (forwarding, module_connection, _endpoint, client, sink, mut client_rx) =
3901            route_fixture("closing-gate");
3902
3903        // A published route: escalate_client_delivery_failure only marks a
3904        // connection closing for a route it has already published.
3905        let live = begin_test_route(&forwarding, client, sink.clone(), 80, "closing-gate");
3906        forwarding
3907            .complete_pending_relay(
3908                module_connection,
3909                live.corr,
3910                RouteBindRelayOutcome::Accepted,
3911            )
3912            .unwrap();
3913        client_rx.try_recv().unwrap();
3914
3915        // Mark the connection closing through the production path without
3916        // running teardown, pinning the window open.
3917        assert!(forwarding
3918            .escalate_client_delivery_failure(
3919                client,
3920                live.client_channel,
3921                live.client_epoch,
3922                CloseReason::new(
3923                    "module_to_client_delivery_failed",
3924                    "client egress refused a module frame",
3925                ),
3926            )
3927            .unwrap());
3928        assert_eq!(forwarding.closing_connection_count().unwrap(), 1);
3929
3930        // A late route.open for the closing client is refused ...
3931        assert!(matches!(
3932            forwarding.begin_route_bind_relay_for_test(client, sink, 81, "closing-gate"),
3933            Err(ForwardingError::ConnectionClosing { connection_id })
3934                if connection_id == client
3935        ));
3936        // ... and so is a late attempt to register the connection as a module.
3937        let (late_tx, _late_rx) = mpsc::channel(1);
3938        assert!(matches!(
3939            forwarding.register_module_connection(
3940                client,
3941                "late-module".into(),
3942                2,
3943                Concurrency::ModuleManaged,
3944                FrameSink::new(late_tx),
3945            ),
3946            Err(ForwardingError::ConnectionClosing { connection_id })
3947                if connection_id == client
3948        ));
3949
3950        // Teardown is the point that lifts the mark: it has just removed every
3951        // per-connection entry under the same lock, so the gate has nothing
3952        // left to protect for this id.
3953        forwarding.cleanup_connection(client).unwrap();
3954        assert_eq!(forwarding.closing_connection_count().unwrap(), 0);
3955    }
3956}
3957
3958/// Blue/green swap slots: a candidate registered beside the active endpoint,
3959/// promoted by `cutover_candidate`, with the old incumbent drained by endpoint.
3960#[cfg(test)]
3961mod swap_slot_tests {
3962    use std::time::Duration;
3963
3964    use super::*;
3965    use tokio::sync::mpsc;
3966
3967    const MODULE_ID: &str = "swapped";
3968
3969    struct SwapFixture {
3970        forwarding: ForwardingTable,
3971        incumbent_connection: ConnectionId,
3972        incumbent: ModuleEndpointId,
3973        candidate_connection: ConnectionId,
3974        candidate: ModuleEndpointId,
3975        _module_rxs: Vec<mpsc::Receiver<crate::router::OutboundFrame>>,
3976    }
3977
3978    fn swap_fixture() -> SwapFixture {
3979        let forwarding = ForwardingTable::default();
3980        let incumbent_connection = ConnectionId::new(100);
3981        let candidate_connection = ConnectionId::new(110);
3982        let (incumbent_tx, incumbent_rx) = mpsc::channel(8);
3983        let incumbent = forwarding
3984            .register_module_connection(
3985                incumbent_connection,
3986                MODULE_ID.to_string(),
3987                2,
3988                Concurrency::ModuleManaged,
3989                FrameSink::new(incumbent_tx),
3990            )
3991            .unwrap();
3992        let (candidate_tx, candidate_rx) = mpsc::channel(8);
3993        let candidate = forwarding
3994            .register_candidate_module_connection(
3995                candidate_connection,
3996                MODULE_ID.to_string(),
3997                2,
3998                Concurrency::ModuleManaged,
3999                FrameSink::new(candidate_tx),
4000            )
4001            .unwrap();
4002        SwapFixture {
4003            forwarding,
4004            incumbent_connection,
4005            incumbent,
4006            candidate_connection,
4007            candidate,
4008            _module_rxs: vec![incumbent_rx, candidate_rx],
4009        }
4010    }
4011
4012    fn client(
4013        raw: u64,
4014    ) -> (
4015        ConnectionId,
4016        FrameSink,
4017        mpsc::Receiver<crate::router::OutboundFrame>,
4018    ) {
4019        let (tx, rx) = mpsc::channel(8);
4020        (ConnectionId::new(raw), FrameSink::new(tx), rx)
4021    }
4022
4023    fn committed_endpoints(forwarding: &ForwardingTable) -> Vec<ModuleEndpointId> {
4024        forwarding
4025            .read_inner()
4026            .unwrap()
4027            .client_to_module
4028            .values()
4029            .map(|route| route.module_endpoint)
4030            .collect()
4031    }
4032
4033    #[test]
4034    fn candidate_is_unroutable_until_cutover_and_by_id_lookups_resolve_the_active_slot() {
4035        let fixture = swap_fixture();
4036        let forwarding = &fixture.forwarding;
4037        assert_ne!(fixture.incumbent, fixture.candidate);
4038
4039        // Every by-id consumer still resolves the incumbent.
4040        assert!(forwarding.has_live_module_connection(MODULE_ID).unwrap());
4041        assert!(!forwarding.module_is_draining(MODULE_ID).unwrap());
4042        let (client_connection, client_sink, _client_rx) = client(200);
4043        let pending = forwarding
4044            .begin_route_bind_relay_for_test(client_connection, client_sink, 1, MODULE_ID)
4045            .unwrap();
4046        assert_eq!(pending.endpoint, fixture.incumbent);
4047        let rpc = forwarding
4048            .begin_module_control_rpc_for(
4049                MODULE_ID,
4050                "health.check",
4051                Instant::now() + Duration::from_secs(1),
4052            )
4053            .unwrap();
4054        assert_eq!(rpc.endpoint, fixture.incumbent);
4055        let census = forwarding.route_census(Some(MODULE_ID)).unwrap();
4056        assert_eq!(census.len(), 1, "the census lists one endpoint per id");
4057
4058        // Connection-keyed lookups see the candidate, so its own frames resolve.
4059        assert_eq!(
4060            forwarding
4061                .module_endpoint_for_connection(fixture.candidate_connection)
4062                .unwrap(),
4063            Some(fixture.candidate)
4064        );
4065        assert_eq!(
4066            forwarding
4067                .module_id_for_connection(fixture.candidate_connection)
4068                .unwrap()
4069                .as_deref(),
4070            Some(MODULE_ID)
4071        );
4072
4073        // One candidate per id.
4074        let (other_tx, _other_rx) = mpsc::channel(1);
4075        assert_eq!(
4076            forwarding.register_candidate_module_connection(
4077                ConnectionId::new(120),
4078                MODULE_ID.to_string(),
4079                2,
4080                Concurrency::ModuleManaged,
4081                FrameSink::new(other_tx),
4082            ),
4083            Err(ForwardingError::CandidateSlotOccupied {
4084                module_id: MODULE_ID.to_string()
4085            })
4086        );
4087    }
4088
4089    /// The cutover linearization point. A relay reserved on the incumbent before
4090    /// cutover must never become a route on the incumbent, and every relay
4091    /// reserved after it must land on the promoted candidate.
4092    #[test]
4093    fn relay_reserved_before_cutover_never_commits_and_later_relays_land_on_the_candidate() {
4094        let fixture = swap_fixture();
4095        let forwarding = &fixture.forwarding;
4096        let (early_client, early_sink, _early_rx) = client(200);
4097        let mut early = forwarding
4098            .begin_route_bind_relay_for_test(early_client, early_sink, 1, MODULE_ID)
4099            .unwrap();
4100        assert_eq!(early.endpoint, fixture.incumbent);
4101        assert!(forwarding
4102            .mark_route_bind_relay_enqueued(early.endpoint, early.corr)
4103            .unwrap());
4104
4105        let cutover = forwarding.cutover_candidate(MODULE_ID).unwrap().unwrap();
4106        assert_eq!(
4107            cutover,
4108            ForwardingCutover {
4109                promoted: fixture.candidate,
4110                incumbent: Some(fixture.incumbent),
4111            }
4112        );
4113
4114        // A route.open reserved after cutover goes to the promoted candidate.
4115        let (late_client, late_sink, _late_rx) = client(201);
4116        let late = forwarding
4117            .begin_route_bind_relay_for_test(late_client, late_sink, 2, MODULE_ID)
4118            .unwrap();
4119        assert_eq!(
4120            late.endpoint, fixture.candidate,
4121            "a route.open after cutover was reserved on the incumbent"
4122        );
4123
4124        // The incumbent acks the early relay after cutover.
4125        let completion = forwarding
4126            .complete_pending_relay(
4127                fixture.incumbent_connection,
4128                early.corr,
4129                RouteBindRelayOutcome::Accepted,
4130            )
4131            .expect("a superseded endpoint's ack is not an error on its connection");
4132        assert!(completion.settled);
4133        assert!(
4134            !committed_endpoints(forwarding).contains(&fixture.incumbent),
4135            "a relay reserved before cutover committed a route on the incumbent"
4136        );
4137        let goodbye = completion
4138            .abandoned
4139            .expect("the incumbent is told to drop the binding it just created");
4140        assert_eq!(goodbye.connection_id, fixture.incumbent_connection);
4141        assert_eq!(goodbye.channel, early.module_channel);
4142        assert_eq!(goodbye.epoch, early.module_epoch);
4143        assert_eq!(goodbye.kind, GoodbyeTargetKind::Module);
4144        match early.receiver.try_recv() {
4145            Ok(RouteBindRelayOutcome::Rejected(body)) => assert_eq!(body.code, "module_reloading"),
4146            other => panic!("expected a retryable module_reloading answer, got {other:?}"),
4147        }
4148        assert!(matches!(
4149            forwarding
4150                .lookup_data_route(early_client, early.client_channel, early.client_epoch)
4151                .unwrap(),
4152            DataRoute::Client(DataRouteState::Absent)
4153        ));
4154
4155        // The late relay commits on the candidate; only its pair stays reserved
4156        // until then.
4157        assert_eq!(forwarding.reserved_route_count().unwrap(), (1, 1));
4158        forwarding
4159            .complete_pending_relay(
4160                fixture.candidate_connection,
4161                late.corr,
4162                RouteBindRelayOutcome::Accepted,
4163            )
4164            .unwrap();
4165        assert_eq!(forwarding.reserved_route_count().unwrap(), (0, 0));
4166        assert_eq!(committed_endpoints(forwarding), vec![fixture.candidate]);
4167    }
4168
4169    #[test]
4170    fn endpoint_drain_after_cutover_drains_the_incumbent_not_the_promoted_candidate() {
4171        let fixture = swap_fixture();
4172        let forwarding = &fixture.forwarding;
4173        // One bound route and one in-flight relay on the incumbent.
4174        let (bound_client, bound_sink, _bound_rx) = client(200);
4175        let bound = forwarding
4176            .begin_route_bind_relay_for_test(bound_client, bound_sink, 1, MODULE_ID)
4177            .unwrap();
4178        forwarding
4179            .complete_pending_relay(
4180                fixture.incumbent_connection,
4181                bound.corr,
4182                RouteBindRelayOutcome::Accepted,
4183            )
4184            .unwrap();
4185        let (pending_client, pending_sink, _pending_rx) = client(201);
4186        let mut in_flight = forwarding
4187            .begin_route_bind_relay_for_test(pending_client, pending_sink, 2, MODULE_ID)
4188            .unwrap();
4189        forwarding
4190            .mark_route_bind_relay_enqueued(in_flight.endpoint, in_flight.corr)
4191            .unwrap();
4192
4193        let incumbent = forwarding
4194            .cutover_candidate(MODULE_ID)
4195            .unwrap()
4196            .unwrap()
4197            .incumbent
4198            .unwrap();
4199        let target = forwarding
4200            .begin_endpoint_drain(incumbent, RouteCloseReason::Restart)
4201            .unwrap()
4202            .expect("the superseded incumbent is still registered");
4203
4204        assert_eq!(target.endpoint, fixture.incumbent);
4205        assert!(forwarding.endpoint_is_draining(fixture.incumbent).unwrap());
4206        assert!(!forwarding.endpoint_is_draining(fixture.candidate).unwrap());
4207        assert!(!forwarding.module_is_draining(MODULE_ID).unwrap());
4208        assert_eq!(target.abandoned_bindings.len(), 1);
4209        assert_eq!(
4210            target.abandoned_bindings[0].channel,
4211            in_flight.module_channel
4212        );
4213        assert!(matches!(
4214            in_flight.receiver.try_recv(),
4215            Ok(RouteBindRelayOutcome::Rejected(body)) if body.code == "module_reloading"
4216        ));
4217        assert_eq!(
4218            forwarding.endpoint_routes(fixture.incumbent).unwrap().len(),
4219            1,
4220            "the incumbent's bound route stays until its drain finishes"
4221        );
4222
4223        let (next_client, next_sink, _next_rx) = client(202);
4224        let next = forwarding
4225            .begin_route_bind_relay_for_test(next_client, next_sink, 3, MODULE_ID)
4226            .expect("the promoted candidate keeps accepting routes");
4227        assert_eq!(next.endpoint, fixture.candidate);
4228    }
4229
4230    /// An endpoint replaced WITHOUT a promotion (a successor registered over it
4231    /// as an ordinary active HELLO) is stale, not superseded, and its ack still
4232    /// fails the acking connection exactly as it did before swap slots existed:
4233    /// `StaleModuleEndpoint`, the reservation indexes already stripped, and the
4234    /// waiting client's sender dropped unanswered.
4235    #[test]
4236    fn stale_endpoint_ack_without_a_promotion_still_fails_as_before() {
4237        let forwarding = ForwardingTable::default();
4238        let first_connection = ConnectionId::new(70);
4239        let (first_tx, _first_rx) = mpsc::channel(8);
4240        forwarding
4241            .register_module_connection(
4242                first_connection,
4243                MODULE_ID.to_string(),
4244                2,
4245                Concurrency::ModuleManaged,
4246                FrameSink::new(first_tx),
4247            )
4248            .unwrap();
4249        let (client_connection, client_sink, _client_rx) = client(200);
4250        let mut pending = forwarding
4251            .begin_route_bind_relay_for_test(client_connection, client_sink, 1, MODULE_ID)
4252            .unwrap();
4253        let (second_tx, _second_rx) = mpsc::channel(8);
4254        forwarding
4255            .register_module_connection(
4256                ConnectionId::new(80),
4257                MODULE_ID.to_string(),
4258                2,
4259                Concurrency::ModuleManaged,
4260                FrameSink::new(second_tx),
4261            )
4262            .unwrap();
4263
4264        assert_eq!(
4265            forwarding
4266                .complete_pending_relay(
4267                    first_connection,
4268                    pending.corr,
4269                    RouteBindRelayOutcome::Accepted
4270                )
4271                .unwrap_err(),
4272            ForwardingError::StaleModuleEndpoint
4273        );
4274        assert!(committed_endpoints(&forwarding).is_empty());
4275        assert_eq!(forwarding.reserved_route_count().unwrap(), (0, 0));
4276        assert!(matches!(
4277            pending.receiver.try_recv(),
4278            Err(oneshot::error::TryRecvError::Closed)
4279        ));
4280    }
4281
4282    #[test]
4283    fn cleanup_releases_candidate_and_superseded_slots_without_touching_the_active_one() {
4284        // A candidate whose connection drops leaves the incumbent routable.
4285        let fixture = swap_fixture();
4286        let forwarding = &fixture.forwarding;
4287        assert!(forwarding
4288            .cleanup_connection(fixture.candidate_connection)
4289            .unwrap()
4290            .is_empty());
4291        assert_eq!(forwarding.cutover_candidate(MODULE_ID).unwrap(), None);
4292        let (client_connection, client_sink, _client_rx) = client(200);
4293        assert_eq!(
4294            forwarding
4295                .begin_route_bind_relay_for_test(client_connection, client_sink, 1, MODULE_ID)
4296                .unwrap()
4297                .endpoint,
4298            fixture.incumbent
4299        );
4300
4301        // After a cutover, the incumbent's teardown releases its own routes and
4302        // leaves the promoted candidate in place.
4303        let fixture = swap_fixture();
4304        let forwarding = &fixture.forwarding;
4305        let (bound_client, bound_sink, _bound_rx) = client(200);
4306        let bound = forwarding
4307            .begin_route_bind_relay_for_test(bound_client, bound_sink, 1, MODULE_ID)
4308            .unwrap();
4309        forwarding
4310            .complete_pending_relay(
4311                fixture.incumbent_connection,
4312                bound.corr,
4313                RouteBindRelayOutcome::Accepted,
4314            )
4315            .unwrap();
4316        forwarding.cutover_candidate(MODULE_ID).unwrap().unwrap();
4317        let released = forwarding
4318            .cleanup_connection(fixture.incumbent_connection)
4319            .unwrap();
4320        assert_eq!(released.len(), 1);
4321        assert_eq!(released[0].connection_id, bound_client);
4322        assert!(forwarding
4323            .read_inner()
4324            .unwrap()
4325            .superseded_endpoints
4326            .is_empty());
4327        assert!(forwarding.has_live_module_connection(MODULE_ID).unwrap());
4328        let (next_client, next_sink, _next_rx) = client(201);
4329        assert_eq!(
4330            forwarding
4331                .begin_route_bind_relay_for_test(next_client, next_sink, 2, MODULE_ID)
4332                .unwrap()
4333                .endpoint,
4334            fixture.candidate
4335        );
4336    }
4337}