Skip to main content

aion_server/worker/
registry.rs

1//! Connected-worker registry keyed by worker-pool address and activity type.
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::num::NonZeroU32;
5use std::pin::Pin;
6use std::sync::{Arc, Mutex, MutexGuard};
7use std::task::{Context, Poll};
8use std::time::Instant;
9
10use aion_core::{
11    ClusterEvent, DeploymentAssociation, InterventionCapabilities, WorkerDeathReason,
12    WorkerTransport,
13};
14use aion_proto::{ProtoActivityTask, ProtoCancelActivity, ProtoLivenessPing, ProtoRegisterWorker};
15use aion_store::{NamespaceOrigin, NamespacePlacement, NamespaceStore, WorkerDeploymentStore};
16use tokio::sync::{Notify, mpsc};
17
18use crate::cluster_publisher::ClusterEventPublisher;
19use crate::config::AutoCreate;
20use crate::error::ServerError;
21use crate::namespace::{CallerIdentity, NamespaceGuard, NamespaceMinter, NamespaceOperation};
22use crate::observability::Metrics;
23use crate::worker::admission_audit::AdmissionAudit;
24use crate::worker::heartbeat::DispatchExclusion;
25
26mod capacity;
27mod census;
28mod reservation;
29
30use capacity::advertised_capacity;
31pub use reservation::DispatchReservation;
32use reservation::eligible_candidates_in_rotation;
33
34/// The literal task queue an empty/absent selector normalizes to.
35///
36/// A worker-pool address has two disjoint dimensions; the second one
37/// (`task_queue`) is a liveness selector, not a correctness boundary. An empty
38/// `task_queue` is normalized to this one named default pool so a producer that
39/// names no queue and a worker that advertises none both land on the same pool.
40///
41/// Re-exported from [`aion_core::DEFAULT_TASK_QUEUE`] so the server cannot drift
42/// from the canonical domain default; the name is kept stable here for existing
43/// call sites.
44pub use aion_core::DEFAULT_TASK_QUEUE;
45
46/// Server-side handle used to push activity tasks to a connected worker stream.
47pub type WorkerTaskSender = mpsc::Sender<WorkerMessage>;
48
49/// Transport through which the server delivers a dispatch to a registered worker.
50///
51/// A worker is selected the SAME way regardless of transport (`select_worker`
52/// over the `(namespace, task_queue, node)` pool key); only the delivery leg
53/// differs. The default gRPC path pushes a [`WorkerMessage`] onto the worker's
54/// stream `mpsc` ([`WorkerDelivery::Grpc`]); a liminal-connected worker is
55/// delivered to by pushing the dispatch out on its existing liminal connection
56/// ([`WorkerDelivery::Liminal`], feature-gated). This enum is the minimal
57/// transport-agnostic seam: the registry holds it on each [`WorkerHandle`], and
58/// the dispatch path reads the variant it needs. The gRPC variant carries exactly
59/// the `mpsc::Sender` it always did, so the gRPC dispatch path is unchanged.
60#[derive(Clone, Debug)]
61pub enum WorkerDelivery {
62    /// gRPC stream delivery: the dispatch path pushes a [`WorkerMessage`] onto
63    /// this `mpsc` sender, exactly as before this enum existed.
64    Grpc(WorkerTaskSender),
65    /// Liminal server-push delivery: the dispatch path pushes the serialized
66    /// dispatch out on the worker's existing liminal connection and awaits the
67    /// correlated reply. Carries the connection identity needed to address that
68    /// push.
69    #[cfg(feature = "liminal-transport")]
70    Liminal(crate::worker::liminal_transport::LiminalWorkerDelivery),
71}
72
73impl WorkerDelivery {
74    /// Which transport this delivery rides, stripped of the handle that
75    /// addresses it.
76    ///
77    /// The liveness probe needs to know a worker's transport WITHOUT holding its
78    /// connection handle, because the question it asks is not "how do I reach
79    /// this worker" but "do I carry any wire on which this worker could be
80    /// asked" (#25). Answering that from a cloned sender or a connection pid
81    /// would tie a coverage decision to a live handle it does not need.
82    ///
83    /// This is the ONE mapping from a held delivery to its wire discriminant.
84    /// Two byte-identical private copies of it existed — one here for the
85    /// cluster-event emitter, one in
86    /// [`cluster_stream`](crate::stream::cluster_stream) for the snapshot — and
87    /// a third was nearly written for the liveness verdict. A discriminant table
88    /// kept in three places is three chances for a transport to be added to two
89    /// of them.
90    #[must_use]
91    pub const fn transport(&self) -> WorkerTransport {
92        match self {
93            Self::Grpc(_) => WorkerTransport::Grpc,
94            #[cfg(feature = "liminal-transport")]
95            Self::Liminal(_) => WorkerTransport::Liminal,
96        }
97    }
98}
99
100/// Message queued from server-side dispatch/shutdown into a worker stream writer.
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub enum WorkerMessage {
103    /// Activity invocation pushed to a worker.
104    ActivityTask(Box<ProtoActivityTask>),
105    /// Graceful-shutdown notification; no new work will be dispatched.
106    DrainRequest,
107    /// Transport liveness ping pushed by the liveness probe (#197).
108    ///
109    /// It travels this channel — the SAME one dispatches travel — deliberately.
110    /// The fact the probe needs is whether the server can reach this worker's
111    /// DISPATCH path, and a ping on a parallel channel could be answered by a
112    /// process whose dispatch path is dead. Answered by the worker SDK RUNTIME,
113    /// never by action code.
114    LivenessPing(ProtoLivenessPing),
115    /// Ask the worker to stop ONE in-flight activity (#233).
116    ///
117    /// A request, not a guarantee: pushing this proves only that the server
118    /// asked. Whether the work stops depends on the worker still holding the
119    /// activity and on the action being interruptible, and neither is
120    /// observable from here.
121    ///
122    /// It travels this channel — the SAME one the dispatch travelled — so the
123    /// cancel cannot overtake or bypass the task it interrupts, and so a worker
124    /// whose dispatch path is dead cannot appear to have been told.
125    CancelActivity(ProtoCancelActivity),
126}
127
128/// Address of a worker pool: the two disjoint routing dimensions that select a
129/// pool, before an `activity_type` is matched within it.
130///
131/// `namespace` is the correctness/isolation boundary — a workflow's activities
132/// only ever reach workers in the workflow's namespace, so crossing it is a bug.
133/// `task_queue` is the pool/flavour selector within that namespace (norn /
134/// claude / cpu / gpu) — a miss is a liveness issue, never a correctness one.
135///
136/// This is a named type rather than a `(String, String)` tuple so a `node`
137/// dimension (Tier 3 affinity) can be added later without re-threading every
138/// call site.
139#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
140pub struct PoolAddress {
141    namespace: String,
142    task_queue: String,
143}
144
145impl PoolAddress {
146    /// Build a pool address, normalizing an empty `task_queue` to the named
147    /// [`DEFAULT_TASK_QUEUE`] pool. The `namespace` is the authorization
148    /// boundary and is never normalized.
149    #[must_use]
150    pub fn new(namespace: impl Into<String>, task_queue: impl Into<String>) -> Self {
151        let task_queue = task_queue.into();
152        let task_queue = if task_queue.is_empty() {
153            String::from(DEFAULT_TASK_QUEUE)
154        } else {
155            task_queue
156        };
157        Self {
158            namespace: namespace.into(),
159            task_queue,
160        }
161    }
162
163    /// The correctness/isolation boundary of this pool.
164    #[must_use]
165    pub fn namespace(&self) -> &str {
166        &self.namespace
167    }
168
169    /// The pool/flavour selector within the namespace.
170    #[must_use]
171    pub fn task_queue(&self) -> &str {
172        &self.task_queue
173    }
174}
175
176/// Registry match key: a worker-pool address plus the activity type matched
177/// within that pool. A named type (not an anonymous tuple) so the routing
178/// identity stays self-describing and extensible.
179#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180struct ActivityKey {
181    pool: PoolAddress,
182    activity_type: String,
183}
184
185impl ActivityKey {
186    fn new(pool: PoolAddress, activity_type: impl Into<String>) -> Self {
187        Self {
188            pool,
189            activity_type: activity_type.into(),
190        }
191    }
192}
193
194type WorkerMap = HashMap<WorkerId, WorkerHandle>;
195type RegistryMap = HashMap<ActivityKey, WorkerMap>;
196
197/// The capacity advertised by a worker that has no executor of its own.
198///
199/// A worker registered with a bare `mpsc` sender — the in-process façades'
200/// callers, the built-in workers, and every harness that drains the channel
201/// itself — runs nothing on its own account: what happens to a pushed task is
202/// decided by whoever holds the receiver, so there is no number of simultaneous
203/// activities such a registration would refuse. This states that, once, instead
204/// of a literal repeated at every call.
205///
206/// It is NOT a default and no production registration can reach it. A gRPC
207/// worker states its capacity in its registration frame and is refused if it
208/// does not; a liminal worker, whose frame cannot carry one, registers with
209/// capacity unknown and announces its own number a frame later. Neither route
210/// reaches this constant — it belongs to the in-process façades alone.
211pub const UNBOUNDED_SENDER_WORKER_CONCURRENCY: u32 = u32::MAX;
212
213/// Stable identifier assigned to a connected worker stream.
214#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
215pub struct WorkerId(u64);
216
217impl WorkerId {
218    /// Build a worker id from the numeric value exposed by administrative and
219    /// wire surfaces.
220    #[must_use]
221    pub const fn from_value(value: u64) -> Self {
222        Self(value)
223    }
224
225    /// Raw numeric value, as carried by the wire `RegisterAck.worker_id` so
226    /// workers can correlate their logs with the server's.
227    #[must_use]
228    pub const fn value(self) -> u64 {
229        self.0
230    }
231}
232
233/// Worker-supplied identity for one instance of a durable deployment.
234#[derive(Clone, Debug, PartialEq, Eq)]
235pub struct WorkerInstanceIdentity {
236    /// Durable deployment name supplied on the wire.
237    pub deployment: String,
238    /// Operator/launcher-assigned instance identifier.
239    pub instance_id: String,
240    /// Deployment-store lookup result captured when registration was accepted.
241    pub association: DeploymentAssociation,
242}
243
244/// What a registration says about the worker beyond where it is routed:
245/// the identity it registered under, its deployment instance, and the
246/// intervention capabilities its harness advertises.
247#[derive(Debug, Clone)]
248pub struct RegistrationOptions {
249    identity: String,
250    max_concurrency: Option<u32>,
251    instance: Option<WorkerInstanceIdentity>,
252    intervention_capabilities: InterventionCapabilities,
253}
254
255impl RegistrationOptions {
256    /// Options for a worker registering under `identity` and advertising
257    /// `max_concurrency`, with no deployment instance and the empty
258    /// (observability-only) capability set.
259    ///
260    /// Capacity is a CONSTRUCTOR argument rather than a `with_` setter because
261    /// there is no honest value to fall back to: a default would decide how
262    /// much work a process is handed on the strength of nobody having said, and
263    /// selection reads it on every dispatch. Every caller states it.
264    ///
265    /// Zero is rejected by [`ConnectedWorkerRegistry::register_delivery`], the
266    /// one funnel every transport and every façade passes through, so the check
267    /// has a single site and no caller can route around it.
268    #[must_use]
269    pub fn identified(identity: impl Into<String>, max_concurrency: u32) -> Self {
270        Self {
271            identity: identity.into(),
272            max_concurrency: Some(max_concurrency),
273            instance: None,
274            intervention_capabilities: InterventionCapabilities::none(),
275        }
276    }
277
278    /// Options for a worker that has registered over a transport whose
279    /// registration frame cannot carry capacity, and will announce it in the
280    /// next frame instead.
281    ///
282    /// This is NOT a default and NOT an unbounded worker. It records that the
283    /// server does not yet know, which selection reads as "do not dispatch"
284    /// until [`ConnectedWorkerRegistry::set_advertised_capacity`] supplies the
285    /// worker's own configured number. The only caller is the liminal
286    /// transport: `liminal::protocol::WorkerRegistration` is a published wire
287    /// type with six fields — namespaces, task queue, node, activity types,
288    /// identity, activities — and none of them is capacity, so a liminal worker
289    /// physically cannot state it in the frame that registers it.
290    ///
291    /// Failing closed for that one round trip is deliberate. The alternative is
292    /// to pick a number on the worker's behalf, and every value is wrong: too
293    /// low serializes a worker that fans, too high pushes it past its own
294    /// admission — which is the defect this whole landing exists to remove.
295    #[must_use]
296    pub fn identified_pending_capacity(identity: impl Into<String>) -> Self {
297        Self {
298            identity: identity.into(),
299            max_concurrency: None,
300            instance: None,
301            intervention_capabilities: InterventionCapabilities::none(),
302        }
303    }
304
305    /// The deployment instance the worker registered as, when it sent one.
306    #[must_use]
307    pub fn with_instance(mut self, instance: Option<WorkerInstanceIdentity>) -> Self {
308        self.instance = instance;
309        self
310    }
311
312    /// The neutral [`InterventionCapabilities`] the worker's harness advertises
313    /// (NOI-6). Metadata the intervention router gates on, never a routing
314    /// dimension.
315    #[must_use]
316    pub fn with_intervention_capabilities(
317        mut self,
318        capabilities: InterventionCapabilities,
319    ) -> Self {
320        self.intervention_capabilities = capabilities;
321        self
322    }
323}
324
325/// Cloneable handle for a registered worker stream.
326///
327/// A worker serves a SET of namespaces under a single `task_queue`, so it is
328/// indexed under one `(namespace, task_queue, activity_type)` key per namespace
329/// in its set. `node` is an OPTIONAL locality affinity (a locality, not a
330/// process — many handles may share a node id) used as a within-pool filter at
331/// selection time; `None` means the worker advertised no locality.
332#[derive(Clone, Debug)]
333pub struct WorkerHandle {
334    id: WorkerId,
335    /// The identity string the worker registered under — the wire's
336    /// `ProtoRegisterWorker.identity`, carried so a lease can name the process
337    /// that took an attempt (WA-010 R3). Never a routing dimension.
338    identity: String,
339    namespaces: BTreeSet<String>,
340    task_queue: String,
341    node: Option<String>,
342    activity_types: BTreeSet<String>,
343    instance: Option<WorkerInstanceIdentity>,
344    /// How many activities this worker will run AT ONCE, exactly as it
345    /// advertised at registration.
346    ///
347    /// `NonZeroU32` rather than a plain integer because zero is not a capacity
348    /// this server can act on — a worker advertising it could never be
349    /// selected — so it is refused at admission and the type carries that
350    /// refusal forward instead of leaving every reader to re-check it.
351    max_concurrency: Option<NonZeroU32>,
352    delivery: WorkerDelivery,
353    /// The neutral mid-run intervention primitives this worker's harness advertises
354    /// support for (NOI-6). The server gates every intervention command on THIS set
355    /// and NEVER routes an unadvertised primitive. Empty = observability-only (the
356    /// default for every non-agent worker), so a normal activity worker advertises
357    /// no controls and the intervention router refuses every command for it.
358    intervention_capabilities: InterventionCapabilities,
359}
360
361impl WorkerHandle {
362    /// Worker identifier assigned by this server process.
363    #[must_use]
364    pub const fn id(&self) -> WorkerId {
365        self.id
366    }
367
368    /// Namespaces authorized for this worker stream. The worker is reachable for
369    /// a dispatch only when its set includes the workflow's namespace.
370    #[must_use]
371    pub const fn namespaces(&self) -> &BTreeSet<String> {
372        &self.namespaces
373    }
374
375    /// The identity string this worker registered under, exactly as the
376    /// registration frame carried it (empty when the worker sent none).
377    #[must_use]
378    pub fn identity(&self) -> &str {
379        &self.identity
380    }
381
382    /// Task queue (pool/flavour) this worker serves within each namespace.
383    #[must_use]
384    pub fn task_queue(&self) -> &str {
385        &self.task_queue
386    }
387
388    /// Optional locality affinity this worker advertised. `None` means the
389    /// worker carries no node and is reachable only for unpinned dispatches.
390    #[must_use]
391    pub fn node(&self) -> Option<&str> {
392        self.node.as_deref()
393    }
394
395    /// Activity types advertised by this worker.
396    #[must_use]
397    pub fn activity_types(&self) -> &BTreeSet<String> {
398        &self.activity_types
399    }
400
401    /// Optional durable deployment/instance association supplied at registration.
402    #[must_use]
403    pub const fn instance(&self) -> Option<&WorkerInstanceIdentity> {
404        self.instance.as_ref()
405    }
406
407    /// How many activities this worker runs AT ONCE, as it advertised, or
408    /// `None` while it has registered but not yet said.
409    ///
410    /// A dispatch PRECONDITION, read by [`eligible_candidates_in_rotation`]:
411    /// a worker holding this many in-flight activities is passed over rather
412    /// than pushed past its own admission. Pushing past it is what produced a
413    /// worker that stopped reading its stream, answered no pings, and was then
414    /// deregistered as lost while it was in fact working.
415    ///
416    /// `None` is reachable only on the liminal transport, whose published
417    /// registration frame has no capacity field, and only for the one round
418    /// trip between that registration and the worker's capacity announcement.
419    /// It means UNKNOWN, never unlimited: selection treats an unknown capacity
420    /// as full, because the alternative is to invent a number for a process
421    /// that is about to state its own.
422    #[must_use]
423    pub const fn max_concurrency(&self) -> Option<NonZeroU32> {
424        self.max_concurrency
425    }
426
427    /// Whether the server still holds an OPEN push channel to this worker.
428    ///
429    /// The explicit connected fact, as opposed to the absence of noise. Silence
430    /// alone cannot tell a worker that is gone from one that is busy, and the
431    /// expiry sweep used to read the second as the first; this is the third
432    /// input that separates them.
433    ///
434    /// gRPC answers from the stream sender itself — a closed receiver means the
435    /// tonic task that owned the stream is gone. Liminal answers from its
436    /// connection: the transport holds a live push connection or it does not.
437    #[must_use]
438    pub fn is_connected(&self) -> bool {
439        match &self.delivery {
440            WorkerDelivery::Grpc(sender) => !sender.is_closed(),
441            #[cfg(feature = "liminal-transport")]
442            WorkerDelivery::Liminal(delivery) => delivery.is_connected(),
443        }
444    }
445
446    /// The transport this worker is delivered to through.
447    #[must_use]
448    pub const fn delivery(&self) -> &WorkerDelivery {
449        &self.delivery
450    }
451
452    /// The neutral intervention primitives this worker's harness advertises (NOI-6).
453    ///
454    /// The intervention router gates on this set and never routes an unadvertised
455    /// primitive. Empty (the default for a plain activity worker) means the worker
456    /// is observability-only: the router refuses every intervention command for it.
457    #[must_use]
458    pub const fn intervention_capabilities(&self) -> &InterventionCapabilities {
459        &self.intervention_capabilities
460    }
461
462    /// gRPC stream sender used by the gRPC dispatch path to push work, or `None`
463    /// when this worker is delivered to over a non-gRPC transport (liminal).
464    ///
465    /// The gRPC dispatch path registers every worker with a [`WorkerDelivery::Grpc`]
466    /// delivery, so this is always `Some` for a gRPC-registered worker — the
467    /// behaviour the path relied on before delivery became transport-agnostic.
468    #[must_use]
469    pub fn sender(&self) -> Option<&WorkerTaskSender> {
470        match &self.delivery {
471            WorkerDelivery::Grpc(sender) => Some(sender),
472            #[cfg(feature = "liminal-transport")]
473            WorkerDelivery::Liminal(_) => None,
474        }
475    }
476}
477
478#[derive(Debug)]
479struct RegistryState {
480    next_worker_id: u64,
481    workers: BTreeMap<WorkerId, WorkerHandle>,
482    by_activity: RegistryMap,
483    /// Round-robin cursor per `(namespace, task_queue, activity_type)` triple, so
484    /// each pool rotates independently of every other pool.
485    rotation: HashMap<ActivityKey, usize>,
486    /// When a worker advertising a given `(pool, activity_type)` and node last
487    /// LEFT service, per advertised node. Read by [`ConnectedWorkerRegistry::pool_census`]
488    /// to answer "how old is the last compatible poller" for an address that has
489    /// none right now (R1). Node-keyed because a node-pinned dispatch's notion
490    /// of "compatible" is node-specific: a worker still serving the activity on
491    /// another node must never make a pinned address look freshly served.
492    last_departure: HashMap<ActivityKey, BTreeMap<Option<String>, Instant>>,
493    /// Workers the server currently cannot reach on the server-to-worker push
494    /// leg, and which are therefore excluded from selection while they remain
495    /// so.
496    ///
497    /// Held here rather than derived at selection time because selection must
498    /// not take the heartbeat tracker's lock: the liveness probe owns the
499    /// evidence and publishes the verdict, and selection only reads it.
500    ///
501    /// This is exclusion from DISPATCH, not deregistration. An unreachable
502    /// worker stays registered, keeps its in-flight work, and becomes eligible
503    /// again the moment a ping is answered — nothing about it is torn down on
504    /// the strength of a push failure.
505    ///
506    /// 🔴 A MAP, not a set, and the value is load-bearing.
507    /// [`DispatchExclusion`] separates two facts its own documentation calls
508    /// "DIFFERENT FACTS an operator must be able to tell apart": an
509    /// `OpeningProbation` clears itself within seconds and is the ordinary cost
510    /// of connecting, while a `ReachabilityLost` is an incident that does not
511    /// clear on its own. A flat set collapses them, and a pool parked on the
512    /// second one then waits with nothing published about why — the same shape
513    /// as a delivery gate that cannot tell a key never begun from one
514    /// released. The prober has the value already; this is simply where it
515    /// stopped being thrown away.
516    dispatch_ineligible: BTreeMap<WorkerId, DispatchExclusion>,
517    /// How many dispatches each worker is currently holding.
518    ///
519    /// The selection-side projection of the heartbeat tracker's in-flight task
520    /// map, and NOT a second accounting kept in parallel with it: both are
521    /// written by the SAME call — [`HeartbeatTracker::track_task`] and
522    /// [`HeartbeatTracker::complete_task`](super::heartbeat::HeartbeatTracker::complete_task)
523    /// take this registry precisely so the two cannot be updated apart. The
524    /// projection lives here because selection must not take the tracker's
525    /// lock, which is the same reason `dispatch_ineligible` lives here rather
526    /// than being derived at selection time.
527    ///
528    /// "One call" is not the same as "one idempotency", and the difference bit
529    /// once: the tracker's map is keyed by `(worker, workflow, activity)` and an
530    /// insert over a live key overwrites, while this count increments. Tracking
531    /// one key twice without an intervening completion therefore counted twice
532    /// and could only be decremented once — a slot leaked for the life of the
533    /// registration. `track_task` now increments only when its insert actually
534    /// added an entry, so the two agree on how many times one dispatch counts.
535    ///
536    /// It is also raised by [`ConnectedWorkerRegistry::select_and_reserve`] and
537    /// [`ConnectedWorkerRegistry::reserve_worker`], which claim a slot at
538    /// SELECTION and hold it — as a [`DispatchReservation`] — until the tracker
539    /// takes the count over. That overlap is one extra held slot for the width
540    /// of the hand-over and can only park a dispatch that would otherwise have
541    /// raced, never admit one.
542    ///
543    /// An entry is dropped when the worker leaves service, so this map is
544    /// bounded by the workers actually registered.
545    in_flight: BTreeMap<WorkerId, u32>,
546}
547
548impl Default for RegistryState {
549    fn default() -> Self {
550        Self {
551            next_worker_id: 1,
552            workers: BTreeMap::new(),
553            by_activity: HashMap::new(),
554            rotation: HashMap::new(),
555            last_departure: HashMap::new(),
556            dispatch_ineligible: BTreeMap::new(),
557            in_flight: BTreeMap::new(),
558        }
559    }
560}
561
562/// Cloneable registry of currently connected worker streams.
563#[derive(Clone)]
564pub struct ConnectedWorkerRegistry {
565    inner: Arc<Mutex<RegistryState>>,
566    metrics: Option<Metrics>,
567    /// WS3 cluster-event publisher: emits `WorkerConnected`/`WorkerDisconnected`
568    /// topology deltas on register/deregister. `None` keeps existing
569    /// constructions (and every test) silent, exactly like `metrics`.
570    cluster_publisher: Option<ClusterEventPublisher>,
571    /// Minted-on-use hook (Control-Plane Phase 1). `None` disables minting, so
572    /// registration is byte-identical to before the registry existed; `Some`
573    /// durably records (open) or gates (closed) each authorized namespace.
574    minter: Option<NamespaceMinter>,
575    /// Durable deployment store used only to classify instance associations.
576    deployment_store: Option<Arc<dyn WorkerDeploymentStore>>,
577    worker_arrived: Arc<Notify>,
578    /// The refusal side of this registry's ledger, shared by every transport.
579    ///
580    /// The registry records who was ADMITTED; this records who was turned away,
581    /// so a repeated identical refusal can be met with silence. It lives here
582    /// because both callers of the admission gate already hold the registry, so
583    /// one home serves both — and one shared record is what stops the two
584    /// transports drifting apart the way #147 found them.
585    audit: Arc<AdmissionAudit>,
586    /// Pulsed whenever a worker's capacity slot is FREED, so a dispatcher that
587    /// parked work for want of capacity is woken by the event itself rather than
588    /// waiting out a timer.
589    ///
590    /// This is the outbox dispatcher's existing advisory wake (the one the stage
591    /// seam pulses when a row is committed), shared rather than duplicated: a
592    /// freed slot and a newly-staged row are the same kind of news to that loop
593    /// — "there may be work you can place now" — and it already coalesces and
594    /// degrades cleanly to its interval poll. `None` (every in-process façade
595    /// and most tests) simply means nobody is listening.
596    capacity_wake: Option<Arc<Notify>>,
597}
598
599impl std::fmt::Debug for ConnectedWorkerRegistry {
600    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
601        formatter
602            .debug_struct("ConnectedWorkerRegistry")
603            .field(
604                "deployment_store_attached",
605                &self.deployment_store.is_some(),
606            )
607            .finish_non_exhaustive()
608    }
609}
610
611impl Default for ConnectedWorkerRegistry {
612    fn default() -> Self {
613        Self {
614            inner: Arc::new(Mutex::new(RegistryState::default())),
615            metrics: None,
616            cluster_publisher: None,
617            minter: None,
618            deployment_store: None,
619            worker_arrived: Arc::new(Notify::new()),
620            audit: Arc::new(AdmissionAudit::new()),
621            capacity_wake: None,
622        }
623    }
624}
625
626/// A live subscription to the next change in what dispatch selection can see,
627/// taken from [`ConnectedWorkerRegistry::worker_arrival`] and awaited as a
628/// future.
629///
630/// # Why this is a value and not a bare `wait` method
631///
632/// The registry's two wake sources — a registration
633/// ([`ConnectedWorkerRegistry::register`] and every sibling, through their
634/// shared tail) and a published reachability verdict
635/// ([`ConnectedWorkerRegistry::set_dispatch_ineligible`]) — are both
636/// `Notify::notify_waiters`, which stores **no permit**. A caller that reads the
637/// registry, misses, and only then constructs its wait has already lost any
638/// arrival that landed during the read: the broadcast fired into an empty waiter
639/// list. That caller then sleeps on a pool it holds positive evidence is served,
640/// until some unrelated later registration happens to wake it — and on the gRPC
641/// transport, where no liveness probe runs and no verdict is ever published, an
642/// unrelated registration is the ONLY thing that ever could.
643///
644/// So the subscription is a value the caller takes **before** it looks, and
645/// awaits **after** it has missed. Everything that fires in between is retained.
646///
647/// # Why it is retained — tokio 1.52.3 at the bytes
648///
649/// Line numbers are in `tokio-1.52.3/src/sync/notify.rs`, the version
650/// `Cargo.lock` pins.
651///
652/// - `Notify::notified_owned` (`:613`) snapshots the process-wide
653///   `notify_waiters` call counter into the future **at construction**
654///   (`:619`, `notify_waiters_calls: get_num_notify_waiters_calls(state)`).
655/// - `notify_waiters` (`:743`) increments that counter unconditionally —
656///   `inner_notify_waiters` bumps it at `:755` when nobody is parked and at
657///   `:761` when someone is. A broadcast into an empty list is therefore not
658///   lost; it moves a number.
659/// - `poll_notified`'s `State::Init` arm compares the snapshot against the live
660///   counter at `:1124`, and again under the waiter lock at `:1156`; a
661///   difference sends the future straight to `State::Done` → `Poll::Ready`.
662///
663/// That comparison is the retention property, and it belongs to **construction**.
664/// `OwnedNotified::enable` (`:1059`) is called here too: it runs that same `Init`
665/// arm eagerly and, on the not-yet-notified path, pushes the waiter into the list
666/// at `:1219` — so the waiter is registered at a defined point rather than at
667/// first poll, and a `notify_one` permit would be retained as well if one were
668/// ever added to this `Notify`. There is none today; this type must not rest its
669/// correctness on that staying true.
670///
671/// # What it costs
672///
673/// `OwnedNotified` rather than the borrowed `Notified<'_>` because the value
674/// crosses a `&mut dyn FnMut(..)` boundary (the wait path's `park` contract) and
675/// sits in a `tokio::select!` arm (the bridge's park); the borrowed form forces
676/// a higher-ranked bound through the first and pinning ceremony at the second.
677///
678/// `Pin<Box<_>>` because `enable` needs `Pin<&mut Self>` at construction, and
679/// because it makes this type `Unpin` so no call site owes pinning ceremony. The
680/// price is one heap allocation per selection-loop iteration, on a path that is
681/// about to park. The alternative — hand back a bare `OwnedNotified` and ask
682/// every call site to remember `pin!` and `enable()` — puts the obligation back
683/// on the call sites, and a call site that forgot its obligation is precisely
684/// the defect this type exists to end.
685#[must_use = "a WorkerArrival that is constructed and dropped is a subscription thrown away"]
686pub struct WorkerArrival {
687    notified: Pin<Box<tokio::sync::futures::OwnedNotified>>,
688}
689
690impl std::fmt::Debug for WorkerArrival {
691    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692        formatter.debug_struct("WorkerArrival").finish()
693    }
694}
695
696impl std::future::Future for WorkerArrival {
697    type Output = ();
698
699    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
700        // `Pin<Box<_>>` is `Unpin`, so the outer pin carries no obligation and
701        // the inner one is what the notification state is actually pinned by.
702        std::future::Future::poll(self.get_mut().notified.as_mut(), context)
703    }
704}
705
706impl ConnectedWorkerRegistry {
707    /// Build a registry that records connected-worker gauge updates.
708    #[must_use]
709    pub fn with_metrics(metrics: Metrics) -> Self {
710        Self {
711            inner: Arc::new(Mutex::new(RegistryState::default())),
712            metrics: Some(metrics),
713            cluster_publisher: None,
714            minter: None,
715            deployment_store: None,
716            worker_arrived: Arc::new(Notify::new()),
717            audit: Arc::new(AdmissionAudit::new()),
718            capacity_wake: None,
719        }
720    }
721
722    /// Share the dispatcher wake this registry pulses when a capacity slot is
723    /// FREED.
724    ///
725    /// The busy answer a full pool gives is attempt-neutral, so a parked row is
726    /// waiting for capacity — and capacity-freeing is an event this server
727    /// produces, not a deadline it should guess at. Without this the only thing
728    /// re-offering a busy-parked row was its durable `visible_after`, which
729    /// rides the operator's FAILURE backoff: raising that legitimately (to slow
730    /// genuine retries) parked healthy work behind it for the same duration.
731    ///
732    /// Pulsed by
733    /// [`record_dispatch_finished`](Self::record_dispatch_finished), which every
734    /// slot-free goes through — a completed dispatch's untrack and a released
735    /// reservation alike.
736    #[must_use]
737    pub fn with_capacity_wake(mut self, wake: Arc<Notify>) -> Self {
738        self.capacity_wake = Some(wake);
739        self
740    }
741
742    /// The admission audit every registration transport names its refusals
743    /// through. Clones of a registry share one, so a worker refused over gRPC
744    /// and then over liminal is one site, not two.
745    #[must_use]
746    pub fn admission_audit(&self) -> &AdmissionAudit {
747        &self.audit
748    }
749
750    /// Attach the WS3 cluster-event publisher so worker topology changes are
751    /// pushed to the dashboard. Pure builder addition.
752    #[must_use]
753    pub fn with_cluster_publisher(mut self, publisher: ClusterEventPublisher) -> Self {
754        self.cluster_publisher = Some(publisher);
755        self
756    }
757
758    /// Attach the durable worker-deployment store used for association lookup.
759    #[must_use]
760    pub fn with_worker_deployment_store(mut self, store: Arc<dyn WorkerDeploymentStore>) -> Self {
761        self.deployment_store = Some(store);
762        self
763    }
764
765    /// Install the minted-on-use namespace hook (Control-Plane Phase 1).
766    ///
767    /// After a registration is authorized and its namespace set scoped, each
768    /// authorized namespace is durably recorded ([`AutoCreate::Open`]) or gated
769    /// ([`AutoCreate::Closed`]) through `store`. Without this builder the
770    /// registry never touches the namespace registry, so registration stays
771    /// byte-identical to before the registry existed. Pure builder addition,
772    /// mirroring [`Self::with_cluster_publisher`].
773    ///
774    /// When a cluster publisher has already been attached
775    /// ([`Self::with_cluster_publisher`], called first on the boot path), it is
776    /// threaded into the minter so a first worker-mint emits the live
777    /// `namespace created` delta to the ops console (S8). Order-independence is
778    /// not assumed: callers wire the publisher before minting on the boot path.
779    #[must_use]
780    pub fn with_namespace_minting(
781        mut self,
782        store: Arc<dyn NamespaceStore>,
783        policy: AutoCreate,
784    ) -> Self {
785        let minter = NamespaceMinter::new(store, policy);
786        let minter = match &self.cluster_publisher {
787            Some(publisher) => minter.with_cluster_publisher(publisher.clone()),
788            None => minter,
789        };
790        self.minter = Some(minter);
791        self
792    }
793
794    /// Thread the boot's namespace-mint routing context into the registry's
795    /// minter, so a worker registering for a namespace whose registry shard this
796    /// node does not own mints through the shard's owner instead of being
797    /// refused `NotOwner` forever.
798    ///
799    /// The SECOND of the two minter construction sites (the first is
800    /// [`ServerState::namespace_minter`](crate::ServerState::namespace_minter),
801    /// which serves the gRPC and HTTP start seams). Called after
802    /// [`Self::with_namespace_minting`] on the boot path; a no-op when no minter
803    /// is installed, and never called at all off-cluster, so default/test
804    /// registries stay byte-identical.
805    #[must_use]
806    pub fn with_namespace_routing(mut self, routing: crate::namespace::NamespaceRouting) -> Self {
807        self.minter = self.minter.map(|minter| minter.with_routing(routing));
808        self
809    }
810
811    /// Authorize a worker registration and insert it into the connected-worker registry.
812    ///
813    /// # Errors
814    ///
815    /// Returns [`ServerError`] if namespace authorization fails or the registry lock is poisoned.
816    pub async fn accept_registration(
817        &self,
818        guard: &NamespaceGuard,
819        caller: &CallerIdentity,
820        registration: &ProtoRegisterWorker,
821        sender: WorkerTaskSender,
822    ) -> Result<WorkerRegistration, ServerError> {
823        self.admit_delivery(
824            guard,
825            caller,
826            registration,
827            WorkerDelivery::Grpc(sender),
828            InterventionCapabilities::none(),
829        )
830        .await
831    }
832
833    /// The ONE admission every transport's registration passes through, in
834    /// this order: the guard's worker-registration policy, then EACH
835    /// namespace in the worker's set authorized against the caller, then the
836    /// auth-scoped mint-or-gate, then the placement-admission gate against
837    /// the worker's advertised node, then the insert. The delivery leg is the
838    /// only thing a transport contributes.
839    ///
840    /// It exists because the liminal transport used to insert into the
841    /// registry directly — `register_delivery` from its
842    /// connection callback — so a worker dialling the liminal listener was
843    /// registered into any namespace it named with none of the gates above,
844    /// while the identical worker over gRPC was refused. Two entry points,
845    /// one set of rules: the rules live here, and a transport that bypasses
846    /// them has to be written to do so on purpose.
847    ///
848    /// # Errors
849    ///
850    /// Returns the guard's namespace denial, the mint-or-gate refusal, the
851    /// placement refusal, or the registry's own insert error, exactly as the
852    /// gRPC path always has.
853    pub async fn admit_delivery(
854        &self,
855        guard: &NamespaceGuard,
856        caller: &CallerIdentity,
857        registration: &ProtoRegisterWorker,
858        delivery: WorkerDelivery,
859        intervention_capabilities: InterventionCapabilities,
860    ) -> Result<WorkerRegistration, ServerError> {
861        // Verify the operation against the guard's worker-registration policy,
862        // then authorize EACH namespace in the worker's set: a worker serves a
863        // SET of correctness boundaries, so the registration is denied unless
864        // the caller is granted every one. The wire's empty `node` carries no
865        // locality affinity; a non-empty value is the worker's advertised node.
866        guard
867            .scope(caller, &NamespaceOperation::register_worker(registration))
868            .await?;
869        let namespaces = guard.scope_worker_namespaces(caller, &registration.namespaces)?;
870        // MINT HOOK (Control-Plane Phase 1). This runs strictly AFTER the
871        // per-namespace authorization above (`scope` + `scope_worker_namespaces`),
872        // so it can only ever mint a namespace the caller is already authorized
873        // for — the mint is auth-scoped by construction (CVE-2025-14986: open
874        // minting and namespace isolation only coexist when minting is
875        // auth-gated). It runs BEFORE the worker is inserted, so a `closed`
876        // rejection never leaves a half-registered worker behind.
877        self.mint_or_gate_namespaces(&namespaces).await?;
878        let node = optional_node(&registration.node);
879        // PLACEMENT-ADMISSION GATE (Control-Plane Phase 2, P2-I1). Runs strictly
880        // AFTER the mint hook (so every authorized namespace has a durable record to
881        // read a placement from) and with BOTH the worker's advertised `node` and
882        // the full authorized namespace set in scope. It rejects the WHOLE
883        // registration (Open Decision 6) when the worker's node violates any
884        // `Pinned{L}` namespace it would serve, so only L-node workers ever enter a
885        // pinned namespace's pool.
886        self.enforce_pinned_placement(&namespaces, node.as_deref())
887            .await?;
888        // CAPACITY IS A DISPATCH PRECONDITION, never defaulted. Selection reads
889        // this number on every dispatch, so a worker the server has no number
890        // for cannot be served correctly: the server would have to invent how
891        // much work the process takes, which is how a worker ends up pushed past
892        // its own admission, silent, and then deregistered as lost while it is
893        // working.
894        //
895        // How the precondition is MET differs by transport, which is why this
896        // reads the delivery's own. A gRPC registration frame has the field, so
897        // an absent one is a worker declining to state it and is refused here —
898        // the precedent is `activities`, refused for the same reason one field
899        // over. A liminal registration frame has no such field to carry, so the
900        // worker registers with capacity unknown and states it on the
901        // capabilities channel a frame later; unknown is not a default, and
902        // selection passes such a worker over until it says.
903        let max_concurrency = advertised_capacity(registration, delivery.transport())?;
904        let instance = self
905            .resolve_instance_identity(registration.instance.as_ref())
906            .await?;
907        let options = match max_concurrency {
908            Some(advertised) => {
909                RegistrationOptions::identified(registration.identity.clone(), advertised)
910            }
911            None => RegistrationOptions::identified_pending_capacity(registration.identity.clone()),
912        };
913        self.register_delivery(
914            namespaces,
915            registration.task_queue.clone(),
916            node,
917            registration.activity_types.iter(),
918            delivery,
919            options
920                .with_instance(instance)
921                .with_intervention_capabilities(intervention_capabilities),
922        )
923    }
924
925    /// Resolve the wire's optional deployment/instance identity into the
926    /// registry's own, marking whether the named deployment is known to the
927    /// deployment store (or unchecked when no store is configured).
928    ///
929    /// # Errors
930    ///
931    /// Returns the deployment store's read error.
932    pub async fn resolve_instance_identity(
933        &self,
934        instance: Option<&aion_proto::ProtoWorkerInstanceIdentity>,
935    ) -> Result<Option<WorkerInstanceIdentity>, ServerError> {
936        let Some(instance) = instance else {
937            return Ok(None);
938        };
939        let association = match &self.deployment_store {
940            Some(store) => {
941                if store
942                    .get_worker_deployment(&instance.deployment)
943                    .await
944                    .map_err(ServerError::from)?
945                    .is_some()
946                {
947                    DeploymentAssociation::Known
948                } else {
949                    DeploymentAssociation::Absent
950                }
951            }
952            None => DeploymentAssociation::Unchecked,
953        };
954        Ok(Some(WorkerInstanceIdentity {
955            deployment: instance.deployment.clone(),
956            instance_id: instance.instance_id.clone(),
957            association,
958        }))
959    }
960
961    /// Apply the minted-on-use policy to an already-authorized namespace set.
962    ///
963    /// A no-op when no minter is installed (every default/test registry), so
964    /// registration stays byte-identical. With a minter, the work is delegated
965    /// to the shared [`NamespaceMinter::mint_or_gate`] — the single
966    /// transport-agnostic implementation reused by the workflow-start safety net
967    /// — with [`NamespaceOrigin::WorkerMint`] so a first mint is attributed to
968    /// worker registration. See that method for the open/closed policy, the
969    /// idempotent "namespace created" event, and the retryable `NotOwner`
970    /// surface.
971    ///
972    /// # Errors
973    ///
974    /// Returns [`ServerError::StoreBackend`] if a durable upsert/lookup fails
975    /// (including a retryable `NotOwner` fence), or [`ServerError::Namespace`]
976    /// when `closed` rejects an unknown namespace.
977    async fn mint_or_gate_namespaces(&self, namespaces: &[String]) -> Result<(), ServerError> {
978        let Some(minter) = &self.minter else {
979            return Ok(());
980        };
981        minter
982            .mint_or_gate(namespaces, NamespaceOrigin::WorkerMint)
983            .await
984    }
985
986    /// Reject the whole registration when the worker's advertised `node` violates
987    /// any `Pinned{L}` namespace it would serve (Control-Plane Phase 2, P2-I1).
988    ///
989    /// For each authorized namespace whose placement is [`NamespacePlacement::Pinned`],
990    /// the worker's advertised `node` must be `Some(n)` with `n ∈ L`; a `None` node
991    /// or an `n ∉ L` is a loud, whole-registration rejection naming the namespace,
992    /// the node, and the required set. This guarantees only L-node workers ever
993    /// serve a hard-pinned namespace's pool, which is exactly what lets the
994    /// `Some(N ∉ L)` composition case (§2.2) resolve to the correct isolation stall
995    /// at dispatch rather than needing a start-time enumeration of future nodes.
996    ///
997    /// Non-`Pinned` placements ([`NamespacePlacement::Unplaced`]/[`NamespacePlacement::Prefer`])
998    /// are UNAFFECTED — byte-identical registration. A no-op when no minter is
999    /// installed (every default/test registry), so those stay behaviour-identical:
1000    /// the gate reads placement from the SAME registry record the minter/placement
1001    /// endpoint writes, never a second source of truth.
1002    ///
1003    /// # Errors
1004    ///
1005    /// Returns [`ServerError::Namespace`] (placement-admission denial) when the
1006    /// worker's node violates a `Pinned` namespace, or [`ServerError::StoreBackend`]
1007    /// if a placement read fails at the backend.
1008    async fn enforce_pinned_placement(
1009        &self,
1010        namespaces: &[String],
1011        node: Option<&str>,
1012    ) -> Result<(), ServerError> {
1013        let Some(minter) = &self.minter else {
1014            return Ok(());
1015        };
1016        for namespace in namespaces {
1017            let NamespacePlacement::Pinned { nodes } = minter.placement_of(namespace).await? else {
1018                continue;
1019            };
1020            let admitted = node.is_some_and(|n| nodes.contains(n));
1021            if !admitted {
1022                return Err(ServerError::placement_admission_denied(
1023                    namespace, node, &nodes,
1024                ));
1025            }
1026        }
1027        Ok(())
1028    }
1029
1030    /// Insert an already-authorized worker stream into the default task queue of
1031    /// a single `namespace`, with no node affinity.
1032    ///
1033    /// Convenience over [`Self::register_namespaces`] for callers that serve one
1034    /// namespace and do not select a task queue (notably tests of the default
1035    /// pool).
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1040    pub fn register<'a>(
1041        &self,
1042        namespace: impl Into<String>,
1043        activity_types: impl IntoIterator<Item = &'a String>,
1044        sender: WorkerTaskSender,
1045        max_concurrency: u32,
1046    ) -> Result<WorkerRegistration, ServerError> {
1047        self.register_namespaces(
1048            [namespace.into()],
1049            String::from(DEFAULT_TASK_QUEUE),
1050            None,
1051            activity_types,
1052            sender,
1053            max_concurrency,
1054        )
1055    }
1056
1057    /// Insert an already-authorized worker stream into one explicit worker pool
1058    /// (single namespace + task queue), with no node affinity.
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1063    pub fn register_pool<'a>(
1064        &self,
1065        pool: PoolAddress,
1066        activity_types: impl IntoIterator<Item = &'a String>,
1067        sender: WorkerTaskSender,
1068        max_concurrency: u32,
1069    ) -> Result<WorkerRegistration, ServerError> {
1070        let PoolAddress {
1071            namespace,
1072            task_queue,
1073        } = pool;
1074        self.register_namespaces(
1075            [namespace],
1076            task_queue,
1077            None,
1078            activity_types,
1079            sender,
1080            max_concurrency,
1081        )
1082    }
1083
1084    /// Insert an already-authorized worker stream serving a SET of namespaces
1085    /// under one `task_queue`, with an optional `node` locality affinity.
1086    ///
1087    /// The worker is indexed under one `(namespace, task_queue, activity_type)`
1088    /// key per namespace in its set, so a dispatch in any of those namespaces
1089    /// can reach it. `node` is recorded on the handle and used only as a
1090    /// within-pool filter at selection time — it is NOT part of [`PoolAddress`].
1091    ///
1092    /// # Errors
1093    ///
1094    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1095    pub fn register_namespaces<'a>(
1096        &self,
1097        namespaces: impl IntoIterator<Item = String>,
1098        task_queue: impl Into<String>,
1099        node: Option<String>,
1100        activity_types: impl IntoIterator<Item = &'a String>,
1101        sender: WorkerTaskSender,
1102        max_concurrency: u32,
1103    ) -> Result<WorkerRegistration, ServerError> {
1104        // A sender-only registration carries no identity: the callers of this
1105        // façade (built-in and test workers) never sent one, and a lease that
1106        // names such a worker says so with the empty string rather than a
1107        // name nobody registered. Capacity is NOT treated that way — it is
1108        // required here too, because selection acts on it and a stand-in value
1109        // would decide how much work these workers are handed.
1110        self.register_delivery(
1111            namespaces,
1112            task_queue,
1113            node,
1114            activity_types,
1115            WorkerDelivery::Grpc(sender),
1116            RegistrationOptions::identified(String::new(), max_concurrency),
1117        )
1118    }
1119
1120    /// Insert an already-authorized worker serving a SET of namespaces under one
1121    /// `task_queue` and optional `node`, delivered to through an explicit
1122    /// [`WorkerDelivery`] transport, carrying the [`RegistrationOptions`] it
1123    /// registered with.
1124    ///
1125    /// This is the one transport-agnostic registration: [`Self::register_namespaces`]
1126    /// is the gRPC façade over it (it wraps the stream sender in
1127    /// [`WorkerDelivery::Grpc`]). Selection (`select_worker`/`workers_for`) is
1128    /// identical across transports; only the held delivery differs.
1129    ///
1130    /// # Errors
1131    ///
1132    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1133    pub fn register_delivery<'a>(
1134        &self,
1135        namespaces: impl IntoIterator<Item = String>,
1136        task_queue: impl Into<String>,
1137        node: Option<String>,
1138        activity_types: impl IntoIterator<Item = &'a String>,
1139        delivery: WorkerDelivery,
1140        options: RegistrationOptions,
1141    ) -> Result<WorkerRegistration, ServerError> {
1142        // THE ONE ZERO-CAPACITY CHECK. Every transport's admission and every
1143        // in-process façade funnels here, so a worker advertising no capacity
1144        // is refused once, by name, instead of being registered into a pool
1145        // selection would then always skip.
1146        let max_concurrency = options
1147            .max_concurrency
1148            .map(|advertised| {
1149                NonZeroU32::new(advertised).ok_or_else(|| ServerError::Wire {
1150                    wire: aion_proto::WireError::backend(
1151                        "worker registration advertises max_concurrency 0; a worker that runs no \
1152                         activities could never be selected for a dispatch",
1153                    ),
1154                })
1155            })
1156            .transpose()?;
1157        let namespaces = namespaces.into_iter().collect::<BTreeSet<_>>();
1158        let task_queue = task_queue.into();
1159        let activity_types = activity_types.into_iter().cloned().collect::<BTreeSet<_>>();
1160        let mut state = self.state()?;
1161        let worker_id = WorkerId(state.next_worker_id);
1162        state.next_worker_id = state.next_worker_id.saturating_add(1);
1163
1164        // Capture the node affinity for the WS3 WorkerConnected delta before the
1165        // handle moves it.
1166        let node_for_event = node.clone();
1167        let instance_for_event = options.instance.clone();
1168        let handle = WorkerHandle {
1169            id: worker_id,
1170            identity: options.identity,
1171            namespaces: namespaces.clone(),
1172            task_queue: task_queue.clone(),
1173            node,
1174            activity_types: activity_types.clone(),
1175            instance: options.instance,
1176            max_concurrency,
1177            delivery,
1178            intervention_capabilities: options.intervention_capabilities,
1179        };
1180
1181        for namespace in &namespaces {
1182            let pool = PoolAddress::new(namespace.clone(), task_queue.clone());
1183            for activity_type in &activity_types {
1184                let key = ActivityKey::new(pool.clone(), activity_type.clone());
1185                // This address is served again, so its departure record has no
1186                // remaining meaning — drop it in lockstep with the insert. The
1187                // departure map is therefore bounded by the addresses real
1188                // workers have served and left, never by caller-supplied
1189                // dispatch strings (which never create an entry at all).
1190                if let Some(by_node) = state.last_departure.get_mut(&key) {
1191                    by_node.remove(&handle.node);
1192                    if by_node.is_empty() {
1193                        state.last_departure.remove(&key);
1194                    }
1195                }
1196                state
1197                    .by_activity
1198                    .entry(key)
1199                    .or_default()
1200                    .insert(worker_id, handle.clone());
1201            }
1202        }
1203        let transport = handle.delivery.transport();
1204        state.workers.insert(worker_id, handle);
1205        drop(state);
1206
1207        if let Some(metrics) = &self.metrics {
1208            for namespace in &namespaces {
1209                metrics.worker_connected(namespace);
1210            }
1211        }
1212
1213        // WS3: one WorkerConnected delta carrying the full namespace set (the
1214        // event is namespace-list-valued; the deploy-scoped cluster channel sees
1215        // it whole). Edge-triggered by the real insert, never a poll.
1216        if let Some(publisher) = &self.cluster_publisher {
1217            let namespaces_vec: Vec<String> = namespaces.iter().cloned().collect();
1218            let task_queue_owned = task_queue.clone();
1219            drop(publisher.emit(|meta| {
1220                ClusterEvent::WorkerConnected {
1221                    meta,
1222                    worker_id: worker_id.value().to_string(),
1223                    namespaces: namespaces_vec,
1224                    task_queue: task_queue_owned,
1225                    transport,
1226                    node: node_for_event,
1227                    deployment: instance_for_event
1228                        .as_ref()
1229                        .map(|identity| identity.deployment.clone()),
1230                    deployment_association: instance_for_event.map(|identity| identity.association),
1231                }
1232            }));
1233        }
1234
1235        self.worker_arrived.notify_waiters();
1236
1237        Ok(WorkerRegistration {
1238            registry: self.clone(),
1239            parts: Some(WorkerRegistrationParts {
1240                worker_id,
1241                namespaces,
1242                task_queue,
1243                activity_types,
1244            }),
1245        })
1246    }
1247
1248    /// Subscribe to the next change in what dispatch selection can see: a new
1249    /// worker registers, or a reachability verdict is published
1250    /// ([`Self::set_dispatch_ineligible`]). Both are ways a pool that had no
1251    /// selectable worker gains one, and a wait that only woke on the first would
1252    /// sleep through a pool whose workers are all excluded — those workers are
1253    /// already registered, so no registration is coming for them.
1254    ///
1255    /// **Take the subscription BEFORE you read the registry, and await it only
1256    /// after the read has missed.** That ordering is the whole contract; see
1257    /// [`WorkerArrival`] for why a subscription taken after the read loses the
1258    /// arrival that landed during it.
1259    ///
1260    /// Callers must re-check the registry after waking: the newly arrived worker
1261    /// may not serve the namespace or activity type the caller needs, and a
1262    /// republished verdict may have restored nobody.
1263    ///
1264    /// The returned value carries its own `#[must_use]` message: a subscription
1265    /// constructed and dropped is a subscription thrown away.
1266    pub fn worker_arrival(&self) -> WorkerArrival {
1267        let mut notified = Box::pin(Arc::clone(&self.worker_arrived).notified_owned());
1268        // `enable` reports whether the wake had ALREADY landed. There is nothing
1269        // to do with that answer here either way: the future is fused, so an
1270        // already-notified subscription simply returns `Ready` on its first
1271        // poll, which IS the retention this type exists to provide. What the
1272        // call is for is its other half — putting the waiter in the list now,
1273        // at a defined point, rather than at whenever the caller first polls.
1274        notified.as_mut().enable();
1275        WorkerArrival { notified }
1276    }
1277
1278    /// Return a snapshot of the DISPATCH-ELIGIBLE workers registered for the
1279    /// `(namespace, task_queue, activity_type)` pool, ordered by worker id and
1280    /// then rotated so each call starts from the next worker in the pool. The
1281    /// rotation cursor is per triple, so each pool round-robins independently.
1282    ///
1283    /// When `node` is `Some`, the result is filtered to workers whose advertised
1284    /// node equals it — a dispatch pinned to a node reaches only workers on that
1285    /// node (NODE affinity = require). When `node` is `None`, the behaviour is
1286    /// exactly the unpinned pool: every worker in the `(namespace, task_queue)`
1287    /// pool is a candidate regardless of locality. node is a within-pool filter,
1288    /// NOT part of the pool key, so the per-triple rotation cursor is shared
1289    /// across pinned and unpinned lookups of the same pool.
1290    ///
1291    /// Candidates, ordering, eligibility and the cursor all come from
1292    /// [`eligible_candidates_in_rotation`], which [`Self::select_and_reserve`]
1293    /// reads too: ONE derivation and ONE cursor, so the gRPC push dispatcher and every
1294    /// other dispatch path rotate over the same workers in the same order and
1295    /// cannot drift about who is dispatchable or whose turn it is.
1296    ///
1297    /// # Errors
1298    ///
1299    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1300    pub fn workers_for(
1301        &self,
1302        namespace: &str,
1303        task_queue: &str,
1304        activity_type: &str,
1305        node: Option<&str>,
1306    ) -> Result<Vec<WorkerHandle>, ServerError> {
1307        let mut state = self.state()?;
1308        let key = ActivityKey::new(PoolAddress::new(namespace, task_queue), activity_type);
1309        Ok(eligible_candidates_in_rotation(&mut state, key, node))
1310    }
1311
1312    /// Return a snapshot of every connected worker stream.
1313    ///
1314    /// # Errors
1315    ///
1316    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1317    pub fn all_workers(&self) -> Result<Vec<WorkerHandle>, ServerError> {
1318        let state = self.state()?;
1319        Ok(state.workers.values().cloned().collect())
1320    }
1321
1322    /// Return the handle for a worker by id, or `None` when it is not registered.
1323    ///
1324    /// The intervention router resolves the owning worker of a target attempt by id
1325    /// (NOI-6): the attempt-owner back-index stores a [`WorkerId`], and the router
1326    /// reads back the live handle to gate on its advertised capabilities and select
1327    /// its delivery. A `None` result means the owner disconnected — the router
1328    /// treats that as the attempt-scoped no-op.
1329    ///
1330    /// # Errors
1331    ///
1332    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1333    pub fn worker_by_id(&self, worker_id: WorkerId) -> Result<Option<WorkerHandle>, ServerError> {
1334        Ok(self.state()?.workers.get(&worker_id).cloned())
1335    }
1336
1337    /// Replace the advertised intervention capabilities of a registered worker
1338    /// (NOI-6). The liminal registration frame cannot carry capabilities, so a
1339    /// liminal agent worker announces them on the reserved capabilities channel
1340    /// right after registering, and this applies the announcement to the live
1341    /// handle the intervention router gates on. Returns `false` when the worker
1342    /// is no longer registered (a disconnect racing the announcement — benign).
1343    ///
1344    /// # Errors
1345    ///
1346    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1347    pub fn set_intervention_capabilities(
1348        &self,
1349        worker_id: WorkerId,
1350        capabilities: &InterventionCapabilities,
1351    ) -> Result<bool, ServerError> {
1352        let mut state = self.state()?;
1353        if !state.workers.contains_key(&worker_id) {
1354            return Ok(false);
1355        }
1356        if let Some(handle) = state.workers.get_mut(&worker_id) {
1357            handle.intervention_capabilities = capabilities.clone();
1358        }
1359        // The selection index holds handle clones; keep them capability-consistent
1360        // even though capabilities are never a routing dimension.
1361        for workers in state.by_activity.values_mut() {
1362            if let Some(handle) = workers.get_mut(&worker_id) {
1363                handle.intervention_capabilities = capabilities.clone();
1364            }
1365        }
1366        Ok(true)
1367    }
1368
1369    /// Broadcast a graceful drain request to every connected worker stream.
1370    /// Workers are removed from routing before any transport signal is attempted.
1371    /// A liminal worker has no drain control frame, so it is force-fenced and
1372    /// deregistered with an error-level identity-bearing log instead of being
1373    /// silently skipped.
1374    ///
1375    /// # Errors
1376    ///
1377    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1378    pub fn broadcast_drain(&self) -> Result<usize, ServerError> {
1379        let workers = self.all_workers()?;
1380        let mut delivered = 0usize;
1381        for worker in workers {
1382            if self.drain_worker(worker.id())? {
1383                delivered = delivered.saturating_add(1);
1384            }
1385        }
1386        Ok(delivered)
1387    }
1388
1389    /// Stop assigning work to one worker and request a graceful transport drain.
1390    ///
1391    /// Returns `false` if the worker is not registered or if its transport has no
1392    /// drain channel. In the latter case the worker is deregistered immediately,
1393    /// after an error-level log names the worker and its transport limitation.
1394    ///
1395    /// # Errors
1396    ///
1397    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1398    pub fn drain_worker(&self, worker_id: WorkerId) -> Result<bool, ServerError> {
1399        let worker = {
1400            let mut state = self.state()?;
1401            let Some(worker) = state.workers.get(&worker_id).cloned() else {
1402                return Ok(false);
1403            };
1404            Self::remove_worker_from_service(&mut state, &worker);
1405            worker
1406        };
1407        match worker.delivery() {
1408            WorkerDelivery::Grpc(sender) => {
1409                if sender.try_send(WorkerMessage::DrainRequest).is_ok() {
1410                    tracing::info!(worker_id = worker_id.value(), "worker drain requested");
1411                    Ok(true)
1412                } else {
1413                    tracing::error!(
1414                        worker_id = worker_id.value(),
1415                        "worker drain signal failed; force-deregistering closed transport"
1416                    );
1417                    self.deregister(worker_id)?;
1418                    Ok(false)
1419                }
1420            }
1421            #[cfg(feature = "liminal-transport")]
1422            WorkerDelivery::Liminal(delivery) => {
1423                tracing::error!(
1424                    worker_id = worker_id.value(),
1425                    connection_pid = delivery.pid(),
1426                    "liminal transport has no drain control channel; worker fenced and \
1427                     deregistered at drain start"
1428                );
1429                self.deregister(worker_id)?;
1430                Ok(false)
1431            }
1432        }
1433    }
1434
1435    /// Publish the liveness probe's reachability verdict: the set of workers the
1436    /// server cannot currently reach on the push leg, which
1437    /// [`eligible_candidates_in_rotation`] then skips for BOTH selectors.
1438    ///
1439    /// Replaces the whole set rather than toggling one worker, so the published
1440    /// verdict is always exactly one round's evidence and a worker can never be
1441    /// left excluded by a stale entry nobody cleared.
1442    ///
1443    /// Publishing WAKES the selection wait ([`WorkerArrival`]). A dispatch that
1444    /// found no eligible worker is blocked on this verdict every bit as squarely
1445    /// as on a registration, and the workers it needs are already registered —
1446    /// so a park that only woke on registrations would sleep through their
1447    /// recovery. A publication landing between a dispatch's census and its park
1448    /// is retained, because the dispatch holds a [`WorkerArrival`] taken before
1449    /// the census; this method owes nothing to that ordering beyond firing.
1450    ///
1451    /// The wake is unconditional rather than gated on the set having shrunk,
1452    /// because a change-gated wake would make correctness depend on this method
1453    /// judging what "changed" means for a caller it cannot see — a set that
1454    /// shrank for a worker in some other pool is no restoration for THIS
1455    /// dispatch, and a set republished identically may still coincide with the
1456    /// registration that serves it. Every round publishes, so waking on each one
1457    /// is self-healing at the probe's own cadence — it invents no clock of its
1458    /// own — and a publication with nobody parked costs one waiterless
1459    /// `notify_waiters`.
1460    ///
1461    /// # Errors
1462    ///
1463    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1464    pub fn set_dispatch_ineligible(
1465        &self,
1466        unreachable: BTreeMap<WorkerId, DispatchExclusion>,
1467    ) -> Result<(), ServerError> {
1468        self.state()?.dispatch_ineligible = unreachable;
1469        self.worker_arrived.notify_waiters();
1470        Ok(())
1471    }
1472
1473    /// The transport each named worker is delivered over, for the workers still
1474    /// registered (#25).
1475    ///
1476    /// A worker absent from the result has LEFT the registry — the caller must
1477    /// treat that as "no transport", never as a default one. That distinction is
1478    /// the whole reason this returns a map rather than a vector in the caller's
1479    /// order: the liveness probe scopes its verdict by transport coverage, and a
1480    /// departed worker whose transport was guessed would be judged by a probe
1481    /// that never had a wire to it.
1482    ///
1483    /// Read under ONE lock acquisition rather than one per worker, so the
1484    /// answer describes a single registry state instead of a smear across a
1485    /// round of registrations and disconnects.
1486    ///
1487    /// # Errors
1488    ///
1489    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1490    pub fn transports_of(
1491        &self,
1492        workers: impl IntoIterator<Item = WorkerId>,
1493    ) -> Result<BTreeMap<WorkerId, WorkerTransport>, ServerError> {
1494        let state = self.state()?;
1495        Ok(workers
1496            .into_iter()
1497            .filter_map(|worker_id| {
1498                state
1499                    .workers
1500                    .get(&worker_id)
1501                    .map(|worker| (worker_id, worker.delivery.transport()))
1502            })
1503            .collect())
1504    }
1505
1506    /// Whether a worker is currently excluded from dispatch selection for
1507    /// unreachability.
1508    ///
1509    /// # Errors
1510    ///
1511    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1512    pub fn is_dispatch_ineligible(&self, worker_id: WorkerId) -> Result<bool, ServerError> {
1513        Ok(self.state()?.dispatch_ineligible.contains_key(&worker_id))
1514    }
1515
1516    /// Every gRPC-delivered worker the liveness probe must ping this round,
1517    /// paired with the stream sender the ping rides (#197).
1518    ///
1519    /// The liminal half of the same census is
1520    /// [`LiminalConnectionNotifier::liveness_targets`](crate::worker::LiminalConnectionNotifier::liveness_targets),
1521    /// which enumerates CONNECTIONS. This one enumerates REGISTRATIONS, because
1522    /// a gRPC worker's only server-side identity is its registry handle: the
1523    /// stream is owned by a tonic task and reachable solely through the sender
1524    /// the registration carries.
1525    ///
1526    /// Deliberately not filtered by current eligibility. A worker excluded from
1527    /// dispatch is precisely the worker whose next answered ping restores it,
1528    /// so skipping the excluded set would make exclusion permanent — the exact
1529    /// defect this lane exists to remove.
1530    ///
1531    /// # Errors
1532    ///
1533    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1534    pub fn grpc_liveness_targets(
1535        &self,
1536    ) -> Result<Vec<super::grpc_liveness::GrpcLivenessTarget>, ServerError> {
1537        Ok(self
1538            .state()?
1539            .workers
1540            .values()
1541            .filter_map(|worker| match &worker.delivery {
1542                WorkerDelivery::Grpc(sender) => Some(super::grpc_liveness::GrpcLivenessTarget {
1543                    worker_id: worker.id,
1544                    sender: sender.clone(),
1545                }),
1546                #[cfg(feature = "liminal-transport")]
1547                WorkerDelivery::Liminal(_) => None,
1548            })
1549            .collect())
1550    }
1551
1552    /// The currently published exclusion set, read as a whole.
1553    ///
1554    /// The liveness probe reads this BEFORE publishing a round's verdict, so it
1555    /// can announce the workers that just LEFT the set. Without the whole
1556    /// previous set there is no way to name a recovery: per-worker queries can
1557    /// only be asked about workers the new verdict already mentions, and a
1558    /// recovered worker is precisely the one it does not.
1559    ///
1560    /// # Errors
1561    ///
1562    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1563    pub fn dispatch_ineligible(
1564        &self,
1565    ) -> Result<BTreeMap<WorkerId, DispatchExclusion>, ServerError> {
1566        Ok(self.state()?.dispatch_ineligible.clone())
1567    }
1568
1569    /// Return whether a worker stream is currently registered.
1570    ///
1571    /// The activity dispatch path uses this after queuing a task to detect a
1572    /// worker whose stream tore down concurrently: a sweep that ran before
1573    /// the dispatch tracked its task can never complete it, so the dispatch
1574    /// must fail the activity itself instead of waiting forever.
1575    ///
1576    /// # Errors
1577    ///
1578    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1579    pub fn is_registered(&self, worker_id: WorkerId) -> Result<bool, ServerError> {
1580        Ok(self.state()?.workers.contains_key(&worker_id))
1581    }
1582
1583    /// Remove a worker by id from every namespace/activity index it advertised.
1584    ///
1585    /// Emits a WS3 [`WorkerDeathReason::Disconnect`] delta — the truthful default
1586    /// for a removed worker whose stream/registration went away. Callers that can
1587    /// PROVE a finer reason (a liveness-timeout sweep) call
1588    /// [`Self::deregister_with_reason`] instead, so the dashboard never sees a
1589    /// fabricated distinction.
1590    ///
1591    /// # Errors
1592    ///
1593    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1594    pub fn deregister(&self, worker_id: WorkerId) -> Result<(), ServerError> {
1595        self.deregister_with_reason(worker_id, WorkerDeathReason::Disconnect)
1596    }
1597
1598    /// Remove a worker by id, attributing the departure to an explicit
1599    /// [`WorkerDeathReason`] the caller can prove at its call site (for example a
1600    /// heartbeat sweep passes [`WorkerDeathReason::Timeout`]).
1601    ///
1602    /// # Errors
1603    ///
1604    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1605    pub fn deregister_with_reason(
1606        &self,
1607        worker_id: WorkerId,
1608        reason: WorkerDeathReason,
1609    ) -> Result<(), ServerError> {
1610        let mut state = self.state()?;
1611        let removed_namespaces = Self::remove_worker(&mut state, worker_id);
1612        drop(state);
1613
1614        let Some(namespaces) = removed_namespaces else {
1615            // Already gone: no metrics double-count, no duplicate delta.
1616            return Ok(());
1617        };
1618
1619        if let Some(metrics) = &self.metrics {
1620            for namespace in &namespaces {
1621                metrics.worker_disconnected(namespace);
1622            }
1623        }
1624        self.emit_worker_disconnected(worker_id, &namespaces, reason);
1625
1626        Ok(())
1627    }
1628
1629    /// Emit a WS3 `WorkerDisconnected` delta if a publisher is attached.
1630    fn emit_worker_disconnected(
1631        &self,
1632        worker_id: WorkerId,
1633        namespaces: &BTreeSet<String>,
1634        reason: WorkerDeathReason,
1635    ) {
1636        if let Some(publisher) = &self.cluster_publisher {
1637            let namespaces_vec: Vec<String> = namespaces.iter().cloned().collect();
1638            drop(publisher.emit(|meta| ClusterEvent::WorkerDisconnected {
1639                meta,
1640                worker_id: worker_id.value().to_string(),
1641                namespaces: namespaces_vec,
1642                reason,
1643            }));
1644        }
1645    }
1646
1647    /// Remove a worker from every `(namespace, task_queue, activity_type)` index
1648    /// it advertised. Returns the namespace set it served (for metrics), or
1649    /// `None` if the worker was already gone.
1650    fn remove_worker(state: &mut RegistryState, worker_id: WorkerId) -> Option<BTreeSet<String>> {
1651        let handle = state.workers.remove(&worker_id)?;
1652        Self::remove_worker_from_service(state, &handle);
1653        Some(handle.namespaces)
1654    }
1655
1656    /// Fence a worker from every routing index while retaining its connection
1657    /// handle so an in-progress graceful drain can finish and tear down normally.
1658    fn remove_worker_from_service(state: &mut RegistryState, handle: &WorkerHandle) {
1659        // The in-flight projection goes with the registration. A departing
1660        // worker's tasks are swept by the tracker on the same paths that call
1661        // this, and a count left behind would keep a worker id that no longer
1662        // exists occupying capacity nothing can free.
1663        state.in_flight.remove(&handle.id);
1664        let departed_at = Instant::now();
1665        for namespace in &handle.namespaces {
1666            let pool = PoolAddress::new(namespace.clone(), handle.task_queue.clone());
1667            for activity_type in &handle.activity_types {
1668                let key = ActivityKey::new(pool.clone(), activity_type.clone());
1669                // Departure time is recorded for EVERY address this worker
1670                // served, so a later census can age the last compatible poller
1671                // instead of reporting a bare "nobody" (R1).
1672                state
1673                    .last_departure
1674                    .entry(key.clone())
1675                    .or_default()
1676                    .insert(handle.node.clone(), departed_at);
1677                if let Some(workers) = state.by_activity.get_mut(&key) {
1678                    workers.remove(&handle.id);
1679                    if workers.is_empty() {
1680                        state.by_activity.remove(&key);
1681                        // Prune the round-robin cursor in lockstep: the cursor
1682                        // map is keyed on arbitrary caller-supplied strings and
1683                        // is lazily created by `workers_for`, so leaving stale
1684                        // entries behind leaks memory unboundedly on a
1685                        // never-dying server. When the last worker for a triple
1686                        // leaves, its cursor has no remaining meaning.
1687                        state.rotation.remove(&key);
1688                    }
1689                }
1690            }
1691        }
1692    }
1693
1694    fn state(&self) -> Result<MutexGuard<'_, RegistryState>, ServerError> {
1695        self.inner
1696            .lock()
1697            .map_err(|_| ServerError::lock_poisoned("connected worker registry"))
1698    }
1699}
1700
1701/// Normalize a wire `node` string into an optional locality affinity: an empty
1702/// value (the proto3 default) carries no node, anything else is the worker's
1703/// advertised node id.
1704///
1705/// Shared with contract admission ([`super::contracts::validate_worker_contracts`])
1706/// rather than restated there: admission decides which of a package's actions a
1707/// connection owes from the same locality this registry then routes by, and two
1708/// independent normalizations of the wire default would be a place for that
1709/// agreement to drift silently.
1710pub(crate) fn optional_node(node: &str) -> Option<String> {
1711    if node.is_empty() {
1712        None
1713    } else {
1714        Some(node.to_owned())
1715    }
1716}
1717
1718#[derive(Clone, Debug)]
1719struct WorkerRegistrationParts {
1720    worker_id: WorkerId,
1721    namespaces: BTreeSet<String>,
1722    task_queue: String,
1723    activity_types: BTreeSet<String>,
1724}
1725
1726/// Registration token owned by the worker stream task.
1727///
1728/// Dropping the token performs best-effort cleanup for disconnect paths. Call
1729/// [`WorkerRegistration::deregister`] when the caller needs a typed poison error.
1730#[derive(Debug)]
1731pub struct WorkerRegistration {
1732    registry: ConnectedWorkerRegistry,
1733    parts: Option<WorkerRegistrationParts>,
1734}
1735
1736impl WorkerRegistration {
1737    /// Worker id assigned to this registration.
1738    #[must_use]
1739    pub fn worker_id(&self) -> Option<WorkerId> {
1740        self.parts.as_ref().map(|parts| parts.worker_id)
1741    }
1742
1743    /// Authorized namespace set for this registration.
1744    #[must_use]
1745    pub fn namespaces(&self) -> Option<&BTreeSet<String>> {
1746        self.parts.as_ref().map(|parts| &parts.namespaces)
1747    }
1748
1749    /// Task queue (pool/flavour) this registration serves within each namespace.
1750    #[must_use]
1751    pub fn task_queue(&self) -> Option<&str> {
1752        self.parts.as_ref().map(|parts| parts.task_queue.as_str())
1753    }
1754
1755    /// Activity types advertised by this registration.
1756    #[must_use]
1757    pub fn activity_types(&self) -> Option<&BTreeSet<String>> {
1758        self.parts.as_ref().map(|parts| &parts.activity_types)
1759    }
1760
1761    /// Explicitly remove this worker from the registry.
1762    ///
1763    /// # Errors
1764    ///
1765    /// Returns [`ServerError::LockPoisoned`] if the registry lock is poisoned.
1766    pub fn deregister(mut self) -> Result<(), ServerError> {
1767        let Some(parts) = self.parts.take() else {
1768            return Ok(());
1769        };
1770        self.registry.deregister(parts.worker_id)
1771    }
1772}
1773
1774impl Drop for WorkerRegistration {
1775    fn drop(&mut self) {
1776        let Some(parts) = self.parts.take() else {
1777            return;
1778        };
1779        let removed_namespaces = self.registry.inner.lock().ok().and_then(|mut state| {
1780            ConnectedWorkerRegistry::remove_worker(&mut state, parts.worker_id)
1781        });
1782        if let Some(namespaces) = removed_namespaces {
1783            if let Some(metrics) = &self.registry.metrics {
1784                for namespace in &namespaces {
1785                    metrics.worker_disconnected(namespace);
1786                }
1787            }
1788            // A dropped registration token means the worker's stream/connection
1789            // went away — the truthful reason is Disconnect, not a fabricated
1790            // timeout/deregister distinction this path cannot prove.
1791            self.registry.emit_worker_disconnected(
1792                parts.worker_id,
1793                &namespaces,
1794                WorkerDeathReason::Disconnect,
1795            );
1796        }
1797    }
1798}
1799
1800#[cfg(test)]
1801mod tests {
1802    use std::time::Duration;
1803
1804    use crate::config::NamespaceMode;
1805    use crate::namespace::{NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces};
1806    use crate::worker::heartbeat::{DISPATCH_PROBATION_PINGS, HeartbeatTracker};
1807
1808    use super::*;
1809
1810    fn guard() -> NamespaceGuard {
1811        NamespaceGuard::new(NamespaceResolver::authorization_only(
1812            NamespaceMode::SharedEngine,
1813            StaticWorkflowNamespaces::default(),
1814            StaticScheduleNamespaces::default(),
1815        ))
1816    }
1817
1818    fn caller(namespace: &str) -> CallerIdentity {
1819        CallerIdentity::new("worker", [namespace.to_owned()])
1820    }
1821
1822    /// A test-expectation failure as a `ServerError`, so `Result`-returning
1823    /// tests can fail on an unexpected `None` without `panic!`/`expect`.
1824    fn test_failure(message: &str) -> ServerError {
1825        ServerError::worker_dispatch("default".to_owned(), "test".to_owned(), message.to_owned())
1826    }
1827
1828    /// A release gate whose party size is fixed when the gate OPENS, not when
1829    /// it is built.
1830    ///
1831    /// A `Barrier` has to be sized before the first racer is spawned. If the
1832    /// operating system then refuses one of those spawns, the racers that did
1833    /// start wait forever on a party that can never be complete, and the join
1834    /// that would have reported the refusal never runs — the test hangs
1835    /// burning a core instead of failing by name. Counting arrivals instead
1836    /// keeps the release simultaneous (nobody proceeds until every racer that
1837    /// started has arrived) while letting the releasing thread decide the
1838    /// party from the spawns it actually got.
1839    #[derive(Default)]
1840    struct StartGate {
1841        /// Arrival count and the open flag, under one lock so a waiter's
1842        /// predicate and the release are read and written atomically.
1843        state: Mutex<StartGateState>,
1844        /// Signalled on every arrival and once on release.
1845        change: std::sync::Condvar,
1846    }
1847
1848    /// The state [`StartGate`] guards.
1849    #[derive(Default)]
1850    struct StartGateState {
1851        /// How many racers have announced themselves.
1852        arrived: usize,
1853        /// Whether the releasing thread has opened the gate.
1854        open: bool,
1855    }
1856
1857    impl StartGate {
1858        /// Announce arrival and block until the gate opens.
1859        ///
1860        /// The arrival is counted BEFORE any early return, the poisoned one
1861        /// included. A racer that panics poisons the mutex; a peer that then
1862        /// returned without counting itself would leave the releasing thread's
1863        /// party permanently short, and `thread::scope` joins every racer
1864        /// before that thread's error can reach the caller — so the test would
1865        /// hang exactly where it meant to fail by name. Recovering the guard
1866        /// and counting first means every racer that reached this function is
1867        /// accounted for, whatever it does next.
1868        fn arrive_and_wait(&self) -> Result<(), ServerError> {
1869            let poisoned = self.state.is_poisoned();
1870            let mut state = self
1871                .state
1872                .lock()
1873                .unwrap_or_else(std::sync::PoisonError::into_inner);
1874            state.arrived += 1;
1875            self.change.notify_all();
1876            if poisoned {
1877                return Err(poisoned_start_gate());
1878            }
1879            while !state.open {
1880                match self.change.wait(state) {
1881                    Ok(next) => state = next,
1882                    Err(poison) => {
1883                        // Release the lock before leaving, so the thread that
1884                        // opens the gate is not blocked behind this racer.
1885                        drop(poison.into_inner());
1886                        return Err(poisoned_start_gate());
1887                    }
1888                }
1889            }
1890            Ok(())
1891        }
1892
1893        /// Wait until `party` racers have arrived, then release them together.
1894        ///
1895        /// Every exit opens the gate FIRST. Reporting a poisoned lock without
1896        /// releasing would leave the racers that did arrive parked in `wait`
1897        /// with `open` still false, and the scope's join would never return —
1898        /// the same hang, wearing the other hat. A poisoned peer also ends the
1899        /// wait: a racer that panicked before announcing itself is never going
1900        /// to arrive, so a party that counts it can never complete.
1901        fn open_for(&self, party: usize) -> Result<(), ServerError> {
1902            let mut poisoned = self.state.is_poisoned();
1903            let mut state = self
1904                .state
1905                .lock()
1906                .unwrap_or_else(std::sync::PoisonError::into_inner);
1907            while !poisoned && state.arrived < party {
1908                match self.change.wait(state) {
1909                    Ok(next) => state = next,
1910                    Err(poison) => {
1911                        state = poison.into_inner();
1912                        poisoned = true;
1913                    }
1914                }
1915            }
1916            state.open = true;
1917            self.change.notify_all();
1918            drop(state);
1919            if poisoned {
1920                return Err(poisoned_start_gate());
1921            }
1922            Ok(())
1923        }
1924    }
1925
1926    /// The failure a poisoned start gate is reported as: a racer panicked
1927    /// while holding the lock, which is a test failure in its own right and
1928    /// must not be collapsed into "no reservation won".
1929    fn poisoned_start_gate() -> ServerError {
1930        test_failure("the racing selection start gate was poisoned by a panicking racer")
1931    }
1932
1933    fn registration(namespace: &str, activity_types: &[&str]) -> ProtoRegisterWorker {
1934        registration_with_queue(namespace, "", activity_types)
1935    }
1936
1937    fn registration_with_queue(
1938        namespace: &str,
1939        task_queue: &str,
1940        activity_types: &[&str],
1941    ) -> ProtoRegisterWorker {
1942        registration_full(&[namespace], task_queue, "", activity_types)
1943    }
1944
1945    fn registration_full(
1946        namespaces: &[&str],
1947        task_queue: &str,
1948        node: &str,
1949        activity_types: &[&str],
1950    ) -> ProtoRegisterWorker {
1951        ProtoRegisterWorker {
1952            namespaces: namespaces.iter().map(|value| (*value).to_owned()).collect(),
1953            activity_types: activity_types
1954                .iter()
1955                .map(|value| (*value).to_owned())
1956                .collect(),
1957            task_queue: task_queue.to_owned(),
1958            node: node.to_owned(),
1959            activities: Vec::new(),
1960            identity: WIRE_IDENTITY.to_owned(),
1961            instance: None,
1962            max_concurrency: Some(4),
1963        }
1964    }
1965
1966    /// A registration that says nothing about its capacity is REFUSED, and the
1967    /// refusal names the field.
1968    ///
1969    /// There is no lenient path and no default here on purpose. The server
1970    /// counts a worker's in-flight dispatches against this number on every
1971    /// selection, so a registration without it could only be served by the
1972    /// server inventing how much work the process takes — which is how a worker
1973    /// ends up pushed past its own admission, silent, and then deregistered as
1974    /// lost while it works. The in-tree precedent is one field over:
1975    /// `activities` empty "predates contract commitment and is refused".
1976    #[tokio::test]
1977    async fn a_registration_without_capacity_is_refused_by_name() -> Result<(), ServerError> {
1978        let registry = ConnectedWorkerRegistry::default();
1979        let (sender, _receiver) = mpsc::channel(1);
1980        let mut frame = registration("tenant-a", &["charge"]);
1981        frame.max_concurrency = None;
1982
1983        let refusal = registry
1984            .accept_registration(&guard(), &caller("tenant-a"), &frame, sender)
1985            .await
1986            .err()
1987            .ok_or_else(|| test_failure("a capacity-less registration must be refused"))?;
1988        assert!(
1989            refusal.to_string().contains("max_concurrency"),
1990            "the refusal must NAME the field a worker has to fix: {refusal}"
1991        );
1992        assert!(
1993            registry
1994                .workers_for("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
1995                .is_empty(),
1996            "a refused registration must leave nothing behind in the pool"
1997        );
1998        Ok(())
1999    }
2000
2001    /// A registration advertising ZERO capacity is refused too, and with its own
2002    /// words: a worker that runs nothing could never be selected, so accepting
2003    /// it would register a worker that exists only to be skipped.
2004    #[tokio::test]
2005    async fn a_registration_advertising_zero_capacity_is_refused() -> Result<(), ServerError> {
2006        let registry = ConnectedWorkerRegistry::default();
2007        let (sender, _receiver) = mpsc::channel(1);
2008        let mut frame = registration("tenant-a", &["charge"]);
2009        frame.max_concurrency = Some(0);
2010
2011        let refusal = registry
2012            .accept_registration(&guard(), &caller("tenant-a"), &frame, sender)
2013            .await
2014            .err()
2015            .ok_or_else(|| test_failure("a zero-capacity registration must be refused"))?;
2016        assert!(
2017            refusal.to_string().contains("max_concurrency 0"),
2018            "the refusal must say what was WRONG with the value, not merely that one was \
2019             required: {refusal}"
2020        );
2021        Ok(())
2022    }
2023
2024    /// SELECTION NEVER EXCEEDS ADVERTISED CAPACITY, and the worker comes back
2025    /// the moment a slot frees.
2026    ///
2027    /// The pool here is one worker advertising two slots. With both held it is
2028    /// not a candidate — not deregistered, not ineligible, simply not selected —
2029    /// which is the resting state a busy worker should have had all along. The
2030    /// restore half is the vacuity control: without it, a filter that excluded
2031    /// the worker permanently (or a registry that had lost it) would pass.
2032    #[tokio::test]
2033    async fn selection_skips_a_worker_that_has_reached_its_advertised_capacity()
2034    -> Result<(), ServerError> {
2035        let registry = ConnectedWorkerRegistry::default();
2036        let (sender, _receiver) = mpsc::channel(4);
2037        let types = ["charge".to_owned()];
2038        let _registration = registry.register_namespaces(
2039            ["tenant-a".to_owned()],
2040            DEFAULT_TASK_QUEUE,
2041            None,
2042            types.iter(),
2043            sender,
2044            2,
2045        )?;
2046        let Some(worker) = registry
2047            .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2048            .map(|(worker, _reservation)| worker)
2049        else {
2050            return Err(test_failure("an idle worker must be selectable"));
2051        };
2052        let worker_id = worker.id();
2053
2054        registry.record_dispatch_started(worker_id)?;
2055        assert!(
2056            registry
2057                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2058                .map(|(worker, _reservation)| worker)
2059                .is_some(),
2060            "one dispatch against a capacity of two leaves a slot"
2061        );
2062
2063        registry.record_dispatch_started(worker_id)?;
2064        assert_eq!(registry.in_flight_for_worker(worker_id)?, 2);
2065        assert!(
2066            registry
2067                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2068                .map(|(worker, _reservation)| worker)
2069                .is_none(),
2070            "a worker holding every slot it advertised must not be selected again: pushing past \
2071             its own admission is what made a working worker look silent"
2072        );
2073        // It is FULL, not gone: the census still counts it as a compatible
2074        // worker, so the taxonomy can say the pool is busy rather than empty.
2075        let census = registry.pool_census("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?;
2076        assert_eq!(census.compatible_workers, 1);
2077        assert_eq!(census.compatible_workers_at_capacity, 1);
2078        assert_eq!(census.eligible_compatible_workers, 0);
2079
2080        registry.record_dispatch_finished(worker_id)?;
2081        assert!(
2082            registry
2083                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2084                .map(|(worker, _reservation)| worker)
2085                .is_some(),
2086            "the freed slot must make the worker selectable again — without this the test above \
2087             would pass for a worker that had simply been lost"
2088        );
2089        Ok(())
2090    }
2091
2092    /// A worker registered with capacity UNKNOWN is not dispatched to, and
2093    /// becomes dispatchable the moment it announces.
2094    ///
2095    /// This is the liminal transport's whole shape at the registry level. Its
2096    /// published registration frame has no capacity field, so it registers
2097    /// pending and states its configured number one frame later on the
2098    /// capabilities channel. Both halves are asserted because each alone is a
2099    /// trap: without the first, the server invents a number for a process that
2100    /// is about to state its own; without the second, a liminal worker
2101    /// registers, looks healthy in every listing, and is never sent any work.
2102    ///
2103    /// Driven through `register_delivery` rather than a liminal connection
2104    /// because the invariant is the REGISTRY's, not the transport's — a handle
2105    /// with no advertised capacity is unselectable whoever produced it.
2106    #[tokio::test]
2107    async fn a_worker_with_no_advertised_capacity_is_unselectable_until_it_announces()
2108    -> Result<(), ServerError> {
2109        let registry = ConnectedWorkerRegistry::default();
2110        let (sender, _receiver) = mpsc::channel(4);
2111        let types = ["charge".to_owned()];
2112        let registration = registry.register_delivery(
2113            ["tenant-a".to_owned()],
2114            DEFAULT_TASK_QUEUE,
2115            None,
2116            types.iter(),
2117            WorkerDelivery::Grpc(sender),
2118            RegistrationOptions::identified_pending_capacity("pending-1"),
2119        )?;
2120
2121        // REGISTERED — it is in the pool and the census can see it...
2122        let census = registry.pool_census("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?;
2123        assert_eq!(
2124            census.compatible_workers, 1,
2125            "a worker awaiting its capacity announcement is registered, not absent"
2126        );
2127        // ...but NOT dispatchable, because the server does not know what it runs.
2128        assert!(
2129            registry
2130                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2131                .map(|(worker, _reservation)| worker)
2132                .is_none(),
2133            "a worker whose capacity is unknown must not be selected: the only way to dispatch \
2134             to it is to invent a number on its behalf, which is the defect this replaces"
2135        );
2136
2137        // From the REGISTRATION guard, not from `workers_for`: that list is the
2138        // dispatch-eligible one, and the whole point of this test is that this
2139        // worker is registered and not yet dispatchable. Asking the eligibility
2140        // list for a worker it is designed to omit is how the first draft of
2141        // this test failed.
2142        let worker_id = registration
2143            .worker_id()
2144            .ok_or_else(|| test_failure("the pending worker must have a registration id"))?;
2145        assert!(
2146            registry
2147                .worker_by_id(worker_id)?
2148                .is_some_and(|handle| handle.max_concurrency().is_none()),
2149            "the handle must report capacity UNKNOWN rather than a stand-in number"
2150        );
2151        // The census must call it what it is. Selection excludes an unannounced
2152        // worker by the same test it excludes a full one, so this is exactly
2153        // where the two would get confused — and the operator sentence attached
2154        // to "at capacity" is wrong for a worker that is idle and mute.
2155        assert_eq!(
2156            census.compatible_workers_capacity_unannounced, 1,
2157            "an unannounced worker must be counted as unannounced"
2158        );
2159        assert_eq!(
2160            census.compatible_workers_at_capacity, 0,
2161            "an unannounced worker must NOT be counted as busy: the two conditions have \
2162             different remedies and the census is what the operator sentence is derived from"
2163        );
2164        assert_eq!(census.eligible_compatible_workers, 0);
2165
2166        // THE ANNOUNCEMENT. Two, not one: a value the registry could also have
2167        // reached by defaulting would not prove the announcement was read.
2168        assert!(
2169            registry.set_advertised_capacity(worker_id, 2)?,
2170            "announcing capacity for a registered worker must apply"
2171        );
2172        let announced = registry.pool_census("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?;
2173        assert_eq!(
2174            announced.compatible_workers_capacity_unannounced, 0,
2175            "the announcement must clear the unannounced count"
2176        );
2177        assert_eq!(announced.eligible_compatible_workers, 1);
2178        assert!(
2179            registry
2180                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2181                .map(|(worker, _reservation)| worker)
2182                .is_some(),
2183            "the announced capacity must make the worker dispatchable"
2184        );
2185        registry.record_dispatch_started(worker_id)?;
2186        assert!(
2187            registry
2188                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2189                .map(|(worker, _reservation)| worker)
2190                .is_some(),
2191            "one dispatch against an announced capacity of two leaves a slot: the announced \
2192             number is what selection now counts against"
2193        );
2194        registry.record_dispatch_started(worker_id)?;
2195        assert!(
2196            registry
2197                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2198                .map(|(worker, _reservation)| worker)
2199                .is_none(),
2200            "the announced capacity must bound selection exactly as a registered one does"
2201        );
2202
2203        // A zero announcement is refused by name, so the later channel cannot
2204        // admit what the registration funnel rejects.
2205        let refusal = registry
2206            .set_advertised_capacity(worker_id, 0)
2207            .err()
2208            .ok_or_else(|| test_failure("a zero-capacity announcement must be refused"))?;
2209        assert!(
2210            refusal.to_string().contains("max_concurrency 0"),
2211            "the refusal must say what was wrong with the value: {refusal}"
2212        );
2213        Ok(())
2214    }
2215
2216    /// A SIMULTANEOUS fan never over-subscribes the worker it lands on.
2217    ///
2218    /// This is the arm the sequential test above cannot cover. Selection has
2219    /// always filtered on the in-flight count; what it did not do was CLAIM the
2220    /// slot it selected, so the claim landed later, in the caller, after the
2221    /// pending entry and the lease record had been written. Every dispatch that
2222    /// started in that window read the same pre-dispatch count. A sequential
2223    /// test passes against that defect — one caller, one window, nobody to race
2224    /// — and the real fan of a fan-out does not.
2225    ///
2226    /// So: far more racers than slots, released together on a barrier, each
2227    /// HOLDING what it wins. The count that matters is how many got a
2228    /// reservation, and it must be exactly the advertised concurrency. A
2229    /// late-claiming selector gives every racer a worker and fails here loudly.
2230    ///
2231    /// The release half at the end is the vacuity control: a registry that had
2232    /// simply lost the worker, or a filter that excluded it permanently, would
2233    /// also hand out no surplus.
2234    ///
2235    /// What this test does and does not discriminate, measured rather than
2236    /// assumed: moving the claim a few instructions outside the selection lock
2237    /// SURVIVES here — 32 racers on this host never landed in a window that
2238    /// narrow. Moving it behind any real work does not: with the claim deferred
2239    /// the width of the pending insert and the lease record, this fails 3 runs
2240    /// out of 3, reporting all 32 racers claiming 4 slots. It is a pin on the
2241    /// defect at its real width, not a proof that no narrower one exists.
2242    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2243    async fn simultaneous_selections_claim_no_more_slots_than_advertised() -> Result<(), ServerError>
2244    {
2245        /// Advertised concurrency of the single worker every racer targets.
2246        const CAPACITY: u32 = 4;
2247        /// Racers released at once — deliberately far more than `CAPACITY`, so
2248        /// a selector that claims late has many chances to over-subscribe.
2249        const RACERS: usize = 32;
2250
2251        let registry = ConnectedWorkerRegistry::default();
2252        let (sender, _receiver) = mpsc::channel(RACERS);
2253        let types = ["charge".to_owned()];
2254        let _registration = registry.register_namespaces(
2255            ["tenant-a".to_owned()],
2256            DEFAULT_TASK_QUEUE,
2257            None,
2258            types.iter(),
2259            sender,
2260            CAPACITY,
2261        )?;
2262
2263        let gate = StartGate::default();
2264        let (won, refused): (
2265            Vec<Option<(WorkerHandle, DispatchReservation)>>,
2266            Option<String>,
2267        ) = std::thread::scope(|scope| {
2268            // Spawn through the fallible builder, not `Scope::spawn`: the
2269            // latter panics when the operating system refuses a thread,
2270            // and that panic is raised on THIS thread while the racers
2271            // that did start are already parked on a party that can never
2272            // complete — the scope's join then never runs and the test
2273            // hangs instead of naming the refusal.
2274            let mut handles = Vec::with_capacity(RACERS);
2275            let mut refused = None;
2276            for index in 0..RACERS {
2277                let registry = registry.clone();
2278                let gate = &gate;
2279                let spawned = std::thread::Builder::new()
2280                    .name(format!("registry-racer-{index}"))
2281                    .spawn_scoped(scope, move || {
2282                        gate.arrive_and_wait()?;
2283                        registry.select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)
2284                    });
2285                match spawned {
2286                    Ok(handle) => handles.push(handle),
2287                    Err(error) => {
2288                        refused = Some(format!(
2289                            "the operating system refused racing selection thread {index}: \
2290                                 {error}"
2291                        ));
2292                        break;
2293                    }
2294                }
2295            }
2296            // Release exactly the racers that started. Sizing the release
2297            // here rather than at construction is what makes a refused
2298            // spawn reportable: the survivors are freed, joined, and the
2299            // refusal is returned by name.
2300            gate.open_for(handles.len())?;
2301            let won = handles
2302                .into_iter()
2303                .map(|handle| {
2304                    // A racer thread that panicked would otherwise be
2305                    // reported as "no reservation won", which reads exactly
2306                    // like the correct answer. Surface it as its own
2307                    // failure instead.
2308                    handle
2309                        .join()
2310                        .map_err(|_| test_failure("a racing selection thread panicked"))?
2311                })
2312                .collect::<Result<Vec<_>, ServerError>>()?;
2313            Ok::<_, ServerError>((won, refused))
2314        })?;
2315        if let Some(refusal) = refused {
2316            return Err(test_failure(&refusal));
2317        }
2318
2319        let granted = won.iter().filter(|slot| slot.is_some()).count();
2320        assert_eq!(
2321            granted, CAPACITY as usize,
2322            "{RACERS} simultaneous selections claimed {granted} slots on a worker advertising              {CAPACITY}: the choice and the claim are not one critical section, so every racer              read the same pre-dispatch count"
2323        );
2324        assert_eq!(
2325            registry.in_flight_for_worker(
2326                won.iter()
2327                    .flatten()
2328                    .next()
2329                    .ok_or_else(|| test_failure("no racer won a slot"))?
2330                    .0
2331                    .id()
2332            )?,
2333            CAPACITY,
2334            "the projection must agree with the number of reservations actually held"
2335        );
2336
2337        drop(won);
2338        assert!(
2339            registry
2340                .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2341                .map(|(worker, _reservation)| worker)
2342                .is_some(),
2343            "released reservations must make the worker selectable again — without this the              count above would also pass for a worker that had been lost"
2344        );
2345        assert_eq!(
2346            registry.in_flight_for_worker(
2347                registry
2348                    .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2349                    .map(|(worker, _reservation)| worker)
2350                    .ok_or_else(|| test_failure("the worker must still be registered"))?
2351                    .id()
2352            )?,
2353            0,
2354            "every reservation must have given its slot back on drop; a leak here shrinks the              pool silently for the life of the registration"
2355        );
2356        Ok(())
2357    }
2358
2359    /// A freed slot WAKES a parked selection.
2360    ///
2361    /// A dispatch parked because every compatible worker is full is blocked on
2362    /// exactly one event and on nothing else: no worker is going to register,
2363    /// and no reachability verdict is going to change. A completion that
2364    /// corrected the count without waking would leave that dispatch asleep
2365    /// until something unrelated happened to fire.
2366    #[tokio::test]
2367    async fn a_freed_dispatch_slot_wakes_a_parked_selection() -> Result<(), ServerError> {
2368        let registry = ConnectedWorkerRegistry::default();
2369        let (sender, _receiver) = mpsc::channel(1);
2370        let types = ["charge".to_owned()];
2371        let _registration = registry.register_namespaces(
2372            ["tenant-a".to_owned()],
2373            DEFAULT_TASK_QUEUE,
2374            None,
2375            types.iter(),
2376            sender,
2377            1,
2378        )?;
2379        let Some(worker) = registry
2380            .select_and_reserve("tenant-a", DEFAULT_TASK_QUEUE, "charge", None)?
2381            .map(|(worker, _reservation)| worker)
2382        else {
2383            return Err(test_failure("an idle worker must be selectable"));
2384        };
2385        registry.record_dispatch_started(worker.id())?;
2386
2387        // Subscribe BEFORE the release, exactly as a parking dispatch does.
2388        let arrival = registry.worker_arrival();
2389        registry.record_dispatch_finished(worker.id())?;
2390        tokio::time::timeout(std::time::Duration::from_secs(1), arrival)
2391            .await
2392            .map_err(|_| {
2393                test_failure(
2394                    "a freed dispatch slot did not wake the selection wait; a dispatch parked on \
2395                     a full pool would sleep through the very completion that serves it",
2396                )
2397            })?;
2398        Ok(())
2399    }
2400
2401    /// The identity the wire fixture registers under, so a test can assert it
2402    /// reached the handle rather than being dropped at admission (WA-010 R3).
2403    const WIRE_IDENTITY: &str = "wire-worker-7";
2404
2405    fn multi_caller(namespaces: &[&str]) -> CallerIdentity {
2406        CallerIdentity::new("worker", namespaces.iter().map(|value| (*value).to_owned()))
2407    }
2408
2409    /// `set_intervention_capabilities` replaces a live worker's advertised set —
2410    /// the announcement path a liminal agent worker takes after its in-band
2411    /// registration (the registration frame cannot carry capabilities) — and
2412    /// reports an unknown worker as `false` (an announcement racing a
2413    /// disconnect is benign, never an error).
2414    #[tokio::test]
2415    async fn set_intervention_capabilities_updates_live_worker() -> Result<(), ServerError> {
2416        let registry = ConnectedWorkerRegistry::default();
2417        let (sender, _receiver) = mpsc::channel(1);
2418        let types = ["scout".to_owned()];
2419        let guard = registry.register_delivery(
2420            ["default".to_owned()],
2421            "default",
2422            None,
2423            types.iter(),
2424            WorkerDelivery::Grpc(sender),
2425            RegistrationOptions::identified(
2426                "scout-1",
2427                crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2428            )
2429            .with_intervention_capabilities(InterventionCapabilities::none()),
2430        )?;
2431        let Some(worker_id) = guard.worker_id() else {
2432            return Err(test_failure("registration carries an id"));
2433        };
2434
2435        let announced = InterventionCapabilities {
2436            supported: vec![aion_core::InterventionPrimitive::InjectMessage],
2437        };
2438        assert!(
2439            registry.set_intervention_capabilities(worker_id, &announced)?,
2440            "a live worker's capabilities must be updatable"
2441        );
2442        let Some(handle) = registry.worker_by_id(worker_id)? else {
2443            return Err(test_failure("worker stays registered"));
2444        };
2445        assert_eq!(handle.intervention_capabilities(), &announced);
2446
2447        assert!(
2448            !registry.set_intervention_capabilities(WorkerId(u64::MAX), &announced)?,
2449            "an unknown worker reports false, never an error"
2450        );
2451        Ok(())
2452    }
2453
2454    /// WA-010 R3: the wire's `identity` reaches the handle. Before this pin it
2455    /// was dropped at the registry insert, so a lease could never name
2456    /// the process that took the attempt.
2457    #[tokio::test]
2458    async fn admission_carries_the_wire_identity_onto_the_handle() -> Result<(), ServerError> {
2459        let registry = ConnectedWorkerRegistry::default();
2460        let (sender, _receiver) = mpsc::channel(1);
2461        let admitted = registry
2462            .accept_registration(
2463                &guard(),
2464                &caller("tenant-a"),
2465                &registration("tenant-a", &["charge"]),
2466                sender,
2467            )
2468            .await?;
2469        let Some(worker_id) = admitted.worker_id() else {
2470            return Err(test_failure("registration carries an id"));
2471        };
2472        let Some(handle) = registry.worker_by_id(worker_id)? else {
2473            return Err(test_failure("the admitted worker is readable"));
2474        };
2475        assert_eq!(handle.identity(), WIRE_IDENTITY);
2476
2477        let (plain_sender, _plain_receiver) = mpsc::channel(1);
2478        let types = ["charge".to_owned()];
2479        let plain = registry.register_namespaces(
2480            ["tenant-a".to_owned()],
2481            DEFAULT_TASK_QUEUE,
2482            None,
2483            types.iter(),
2484            plain_sender,
2485            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2486        )?;
2487        let Some(plain_id) = plain.worker_id() else {
2488            return Err(test_failure("registration carries an id"));
2489        };
2490        let Some(plain_handle) = registry.worker_by_id(plain_id)? else {
2491            return Err(test_failure("the plain worker is readable"));
2492        };
2493        assert_eq!(
2494            plain_handle.identity(),
2495            "",
2496            "a sender-only registration never sent an identity and must not invent one"
2497        );
2498        Ok(())
2499    }
2500
2501    #[tokio::test]
2502    async fn register_and_deregister_are_namespace_isolated() -> Result<(), ServerError> {
2503        let registry = ConnectedWorkerRegistry::default();
2504        let (tenant_a_tx, _tenant_a_rx) = mpsc::channel(1);
2505        let (tenant_b_tx, _tenant_b_rx) = mpsc::channel(1);
2506
2507        let tenant_a = registry
2508            .accept_registration(
2509                &guard(),
2510                &caller("tenant-a"),
2511                &registration("tenant-a", &["charge", "charge"]),
2512                tenant_a_tx,
2513            )
2514            .await?;
2515        let tenant_b = registry
2516            .accept_registration(
2517                &guard(),
2518                &caller("tenant-b"),
2519                &registration("tenant-b", &["charge"]),
2520                tenant_b_tx,
2521            )
2522            .await?;
2523
2524        let tq = DEFAULT_TASK_QUEUE;
2525        assert_eq!(
2526            registry.workers_for("tenant-a", tq, "charge", None)?.len(),
2527            1
2528        );
2529        assert_eq!(
2530            registry.workers_for("tenant-b", tq, "charge", None)?.len(),
2531            1
2532        );
2533        assert!(
2534            registry
2535                .workers_for("tenant-a", tq, "missing", None)?
2536                .is_empty()
2537        );
2538
2539        let tenant_a_id = tenant_a.worker_id();
2540        tenant_a.deregister()?;
2541
2542        assert!(
2543            registry
2544                .workers_for("tenant-a", tq, "charge", None)?
2545                .is_empty()
2546        );
2547        assert_eq!(
2548            registry.workers_for("tenant-b", tq, "charge", None)?.len(),
2549            1
2550        );
2551        assert_ne!(tenant_a_id, tenant_b.worker_id());
2552
2553        tenant_b.deregister()?;
2554        assert!(
2555            registry
2556                .workers_for("tenant-b", tq, "charge", None)?
2557                .is_empty()
2558        );
2559        Ok(())
2560    }
2561
2562    #[tokio::test]
2563    async fn denied_namespace_is_not_registered() -> Result<(), ServerError> {
2564        let registry = ConnectedWorkerRegistry::default();
2565        let (tx, _rx) = mpsc::channel(1);
2566        let denied = registry
2567            .accept_registration(
2568                &guard(),
2569                &caller("tenant-a"),
2570                &registration("tenant-b", &["charge"]),
2571                tx,
2572            )
2573            .await;
2574
2575        assert!(denied.is_err());
2576        assert!(
2577            registry
2578                .workers_for("tenant-b", DEFAULT_TASK_QUEUE, "charge", None)?
2579                .is_empty()
2580        );
2581        Ok(())
2582    }
2583
2584    #[tokio::test]
2585    async fn task_queues_partition_disjoint_pools_within_one_namespace() -> Result<(), ServerError>
2586    {
2587        // Same namespace + same activity_type, two DIFFERENT task queues: the
2588        // pools are disjoint, a lookup for one queue never returns the other's
2589        // worker, and round-robin holds independently per (ns, tq, type) triple.
2590        let registry = ConnectedWorkerRegistry::default();
2591        let (norn_tx, _norn_rx) = mpsc::channel(1);
2592        let (claude_a_tx, _claude_a_rx) = mpsc::channel(1);
2593        let (claude_b_tx, _claude_b_rx) = mpsc::channel(1);
2594
2595        let norn = registry
2596            .accept_registration(
2597                &guard(),
2598                &caller("local"),
2599                &registration_with_queue("local", "norn", &["dev"]),
2600                norn_tx,
2601            )
2602            .await?;
2603        // Two workers on the SAME (local, claude) pool to exercise round-robin.
2604        let claude_a = registry
2605            .accept_registration(
2606                &guard(),
2607                &caller("local"),
2608                &registration_with_queue("local", "claude", &["dev"]),
2609                claude_a_tx,
2610            )
2611            .await?;
2612        let claude_b = registry
2613            .accept_registration(
2614                &guard(),
2615                &caller("local"),
2616                &registration_with_queue("local", "claude", &["dev"]),
2617                claude_b_tx,
2618            )
2619            .await?;
2620
2621        let norn_pool = registry.workers_for("local", "norn", "dev", None)?;
2622        assert_eq!(norn_pool.len(), 1, "norn pool has exactly its one worker");
2623        let norn_id = norn.worker_id().ok_or_else(missing_id)?;
2624        assert_eq!(norn_pool[0].id(), norn_id);
2625
2626        let claude_pool = registry.workers_for("local", "claude", "dev", None)?;
2627        assert_eq!(
2628            claude_pool.len(),
2629            2,
2630            "claude pool sees only its two workers"
2631        );
2632        let claude_ids: BTreeSet<WorkerId> = claude_pool.iter().map(WorkerHandle::id).collect();
2633        assert!(
2634            !claude_ids.contains(&norn_id),
2635            "the norn worker must never appear in the claude pool"
2636        );
2637
2638        // A dispatch targeting `norn` never reaches a `claude` worker, and vice
2639        // versa: the disjoint key is the boundary.
2640        assert!(
2641            !registry
2642                .workers_for("local", "norn", "dev", None)?
2643                .iter()
2644                .any(|worker| claude_ids.contains(&worker.id()))
2645        );
2646
2647        // Round-robin per triple: the (local, claude, dev) cursor advances
2648        // independently and cycles through both claude workers, while the
2649        // (local, norn, dev) cursor keeps returning its single worker.
2650        let first = registry.workers_for("local", "claude", "dev", None)?[0].id();
2651        let second = registry.workers_for("local", "claude", "dev", None)?[0].id();
2652        assert_ne!(
2653            first, second,
2654            "claude pool round-robins across both workers"
2655        );
2656        assert_eq!(
2657            registry.workers_for("local", "norn", "dev", None)?[0].id(),
2658            norn_id,
2659            "the norn pool rotation is unaffected by claude traffic"
2660        );
2661
2662        norn.deregister()?;
2663        claude_a.deregister()?;
2664        claude_b.deregister()?;
2665        Ok(())
2666    }
2667
2668    #[tokio::test]
2669    async fn same_task_queue_in_different_namespaces_is_isolated() -> Result<(), ServerError> {
2670        // Same task_queue string, two DIFFERENT namespaces: namespace is the
2671        // correctness boundary, so the pools are isolated.
2672        let registry = ConnectedWorkerRegistry::default();
2673        let (local_tx, _local_rx) = mpsc::channel(1);
2674        let (remote_tx, _remote_rx) = mpsc::channel(1);
2675
2676        let local = registry
2677            .accept_registration(
2678                &guard(),
2679                &caller("local"),
2680                &registration_with_queue("local", "gpu", &["render"]),
2681                local_tx,
2682            )
2683            .await?;
2684        let remote = registry
2685            .accept_registration(
2686                &guard(),
2687                &caller("remote"),
2688                &registration_with_queue("remote", "gpu", &["render"]),
2689                remote_tx,
2690            )
2691            .await?;
2692
2693        let local_pool = registry.workers_for("local", "gpu", "render", None)?;
2694        let remote_pool = registry.workers_for("remote", "gpu", "render", None)?;
2695        assert_eq!(local_pool.len(), 1);
2696        assert_eq!(remote_pool.len(), 1);
2697        assert_ne!(
2698            local_pool[0].id(),
2699            remote_pool[0].id(),
2700            "a shared task_queue string does not merge two namespaces"
2701        );
2702
2703        local.deregister()?;
2704        assert!(
2705            registry
2706                .workers_for("local", "gpu", "render", None)?
2707                .is_empty(),
2708            "deregistering the local worker leaves the remote namespace untouched"
2709        );
2710        assert_eq!(
2711            registry.workers_for("remote", "gpu", "render", None)?.len(),
2712            1
2713        );
2714
2715        remote.deregister()?;
2716        Ok(())
2717    }
2718
2719    #[tokio::test]
2720    async fn worker_serving_a_namespace_set_is_reachable_in_each() -> Result<(), ServerError> {
2721        // A worker advertising {a, b} is reachable for dispatch in BOTH a and b;
2722        // a worker in {a} is NOT reachable in b.
2723        let registry = ConnectedWorkerRegistry::default();
2724        let (ab_tx, _ab_rx) = mpsc::channel(1);
2725        let (a_tx, _a_rx) = mpsc::channel(1);
2726
2727        let worker_ab = registry
2728            .accept_registration(
2729                &guard(),
2730                &multi_caller(&["a", "b"]),
2731                &registration_full(&["a", "b"], "default", "", &["dev"]),
2732                ab_tx,
2733            )
2734            .await?;
2735        let worker_a = registry
2736            .accept_registration(
2737                &guard(),
2738                &caller("a"),
2739                &registration_full(&["a"], "default", "", &["dev"]),
2740                a_tx,
2741            )
2742            .await?;
2743
2744        let in_a = registry.workers_for("a", "default", "dev", None)?;
2745        let in_b = registry.workers_for("b", "default", "dev", None)?;
2746        let both_id = worker_ab.worker_id().ok_or_else(missing_id)?;
2747        let only_a_id = worker_a.worker_id().ok_or_else(missing_id)?;
2748
2749        // Namespace a sees BOTH workers; namespace b sees ONLY the {a, b} worker.
2750        let a_ids: BTreeSet<WorkerId> = in_a.iter().map(WorkerHandle::id).collect();
2751        assert_eq!(a_ids, BTreeSet::from([both_id, only_a_id]));
2752        assert_eq!(in_b.len(), 1, "only the {{a, b}} worker is reachable in b");
2753        assert_eq!(in_b[0].id(), both_id);
2754        assert!(
2755            !in_b.iter().any(|worker| worker.id() == only_a_id),
2756            "the {{a}}-only worker must not be reachable in b"
2757        );
2758
2759        // Deregistering the {a, b} worker removes it from BOTH buckets.
2760        worker_ab.deregister()?;
2761        assert!(
2762            registry
2763                .workers_for("b", "default", "dev", None)?
2764                .is_empty()
2765        );
2766        assert_eq!(registry.workers_for("a", "default", "dev", None)?.len(), 1);
2767
2768        worker_a.deregister()?;
2769        Ok(())
2770    }
2771
2772    #[tokio::test]
2773    async fn node_pin_filters_within_pool() -> Result<(), ServerError> {
2774        // Two workers in the same (namespace, task_queue) pool on different
2775        // nodes: unpinned round-robins across both; pinned to node N reaches
2776        // ONLY the worker(s) on N; pinned to a node with no worker finds none.
2777        let registry = ConnectedWorkerRegistry::default();
2778        let (n1_tx, _n1_rx) = mpsc::channel(1);
2779        let (n2_tx, _n2_rx) = mpsc::channel(1);
2780
2781        let on_n1 = registry
2782            .accept_registration(
2783                &guard(),
2784                &caller("ns"),
2785                &registration_full(&["ns"], "tq", "n1", &["dev"]),
2786                n1_tx,
2787            )
2788            .await?;
2789        let on_n2 = registry
2790            .accept_registration(
2791                &guard(),
2792                &caller("ns"),
2793                &registration_full(&["ns"], "tq", "n2", &["dev"]),
2794                n2_tx,
2795            )
2796            .await?;
2797        let n1_id = on_n1.worker_id().ok_or_else(missing_id)?;
2798        let n2_id = on_n2.worker_id().ok_or_else(missing_id)?;
2799
2800        // Unpinned: both workers are candidates and round-robin advances.
2801        let unpinned = registry.workers_for("ns", "tq", "dev", None)?;
2802        assert_eq!(unpinned.len(), 2, "unpinned reaches the whole pool");
2803        let first = registry.workers_for("ns", "tq", "dev", None)?[0].id();
2804        let second = registry.workers_for("ns", "tq", "dev", None)?[0].id();
2805        assert_ne!(first, second, "unpinned round-robins across both nodes");
2806
2807        // Pinned to n1: only the n1 worker; pinned to n2: only the n2 worker.
2808        let pinned_n1 = registry.workers_for("ns", "tq", "dev", Some("n1"))?;
2809        assert_eq!(pinned_n1.len(), 1);
2810        assert_eq!(pinned_n1[0].id(), n1_id);
2811        let pinned_n2 = registry.workers_for("ns", "tq", "dev", Some("n2"))?;
2812        assert_eq!(pinned_n2.len(), 1);
2813        assert_eq!(pinned_n2[0].id(), n2_id);
2814
2815        // select_worker honours the same filter.
2816        assert_eq!(
2817            registry
2818                .select_and_reserve("ns", "tq", "dev", Some("n1"))?
2819                .map(|(worker, _reservation)| worker)
2820                .map(|worker| worker.id()),
2821            Some(n1_id)
2822        );
2823
2824        // Pinned to a node with no worker finds no candidate (the dispatcher
2825        // then waits via the same no-worker path the existing test exercises).
2826        assert!(
2827            registry
2828                .workers_for("ns", "tq", "dev", Some("absent"))?
2829                .is_empty(),
2830            "a pin to a node with no worker yields no candidate"
2831        );
2832        assert!(
2833            registry
2834                .select_and_reserve("ns", "tq", "dev", Some("absent"))?
2835                .map(|(worker, _reservation)| worker)
2836                .is_none()
2837        );
2838
2839        on_n1.deregister()?;
2840        on_n2.deregister()?;
2841        Ok(())
2842    }
2843
2844    #[tokio::test]
2845    async fn shared_node_id_round_robins_across_workers() -> Result<(), ServerError> {
2846        // Two workers SHARING a node id in the same pool: a dispatch pinned to
2847        // that node round-robins across BOTH (node is locality, not process).
2848        let registry = ConnectedWorkerRegistry::default();
2849        let (a_tx, _a_rx) = mpsc::channel(1);
2850        let (b_tx, _b_rx) = mpsc::channel(1);
2851
2852        let worker_a = registry
2853            .accept_registration(
2854                &guard(),
2855                &caller("ns"),
2856                &registration_full(&["ns"], "tq", "shared", &["dev"]),
2857                a_tx,
2858            )
2859            .await?;
2860        let worker_b = registry
2861            .accept_registration(
2862                &guard(),
2863                &caller("ns"),
2864                &registration_full(&["ns"], "tq", "shared", &["dev"]),
2865                b_tx,
2866            )
2867            .await?;
2868        let a_id = worker_a.worker_id().ok_or_else(missing_id)?;
2869        let b_id = worker_b.worker_id().ok_or_else(missing_id)?;
2870
2871        let pinned = registry.workers_for("ns", "tq", "dev", Some("shared"))?;
2872        assert_eq!(
2873            pinned.len(),
2874            2,
2875            "both workers on the shared node are candidates"
2876        );
2877        let pinned_ids: BTreeSet<WorkerId> = pinned.iter().map(WorkerHandle::id).collect();
2878        assert_eq!(pinned_ids, BTreeSet::from([a_id, b_id]));
2879
2880        let first = registry.workers_for("ns", "tq", "dev", Some("shared"))?[0].id();
2881        let second = registry.workers_for("ns", "tq", "dev", Some("shared"))?[0].id();
2882        assert_ne!(
2883            first, second,
2884            "a pin to a shared node round-robins across both workers on it"
2885        );
2886
2887        worker_a.deregister()?;
2888        worker_b.deregister()?;
2889        Ok(())
2890    }
2891
2892    /// T1 — the finding, as a test. `select_worker` took the LOWEST matching
2893    /// worker id, so a workflow whose activities are dispatched over anything
2894    /// but the gRPC push leg sent every one of them to the first-registered
2895    /// worker and left every other node idle. Selection is the candidate at the
2896    /// pool's rotation cursor, so four consecutive unpinned selections walk the
2897    /// two nodes twice.
2898    ///
2899    /// The sequence is asserted, not merely "they differ": a selector that
2900    /// alternated by luck of hash order would satisfy a difference assertion and
2901    /// still not be a round-robin.
2902    #[tokio::test]
2903    async fn unpinned_selection_rotates_across_nodes() -> Result<(), ServerError> {
2904        let registry = ConnectedWorkerRegistry::default();
2905        let (n1_tx, _n1_rx) = mpsc::channel(1);
2906        let (n2_tx, _n2_rx) = mpsc::channel(1);
2907
2908        let on_n1 = registry
2909            .accept_registration(
2910                &guard(),
2911                &caller("ns"),
2912                &registration_full(&["ns"], "tq", "n1", &["dev"]),
2913                n1_tx,
2914            )
2915            .await?;
2916        let on_n2 = registry
2917            .accept_registration(
2918                &guard(),
2919                &caller("ns"),
2920                &registration_full(&["ns"], "tq", "n2", &["dev"]),
2921                n2_tx,
2922            )
2923            .await?;
2924
2925        let mut visited = Vec::new();
2926        for _ in 0..4 {
2927            let selected = registry
2928                .select_and_reserve("ns", "tq", "dev", None)?
2929                .map(|(worker, _reservation)| worker)
2930                .ok_or_else(|| test_failure("an unpinned selection must find a worker"))?;
2931            visited.push(selected.node().map(str::to_owned));
2932        }
2933        assert_eq!(
2934            visited,
2935            vec![
2936                Some(String::from("n1")),
2937                Some(String::from("n2")),
2938                Some(String::from("n1")),
2939                Some(String::from("n2")),
2940            ],
2941            "unpinned selection must rotate across the nodes rather than pin itself to the \
2942             lowest worker id"
2943        );
2944
2945        on_n1.deregister()?;
2946        on_n2.deregister()?;
2947        Ok(())
2948    }
2949
2950    /// T2 — one eligible-candidate derivation means the exclusion a liveness
2951    /// verdict publishes binds BOTH selectors.
2952    ///
2953    /// `workers_for` ignored it entirely and handed the push dispatcher a worker
2954    /// the server had already found unreachable; `select_worker` honoured it. The
2955    /// excluded worker here is the LOWEST id on purpose — the one the old
2956    /// `min_by_key` selector would have taken every time — so a rotation that
2957    /// merely skipped position zero could not pass this.
2958    #[test]
2959    fn an_ineligible_worker_is_never_selected_by_either_selector() -> Result<(), ServerError> {
2960        let registry = ConnectedWorkerRegistry::default();
2961        let types = [String::from("dev")];
2962        let mut receivers = Vec::new();
2963        let mut registrations = Vec::new();
2964        for _ in 0..3 {
2965            let (tx, rx) = mpsc::channel(1);
2966            receivers.push(rx);
2967            registrations.push(registry.register_namespaces(
2968                [String::from("ns")],
2969                "tq",
2970                None,
2971                types.iter(),
2972                tx,
2973                crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2974            )?);
2975        }
2976        let mut ids = Vec::new();
2977        for registration in &registrations {
2978            ids.push(registration.worker_id().ok_or_else(missing_id)?);
2979        }
2980        ids.sort_unstable();
2981        let excluded = *ids.first().ok_or_else(missing_id)?;
2982        registry.set_dispatch_ineligible(
2983            [(excluded, DispatchExclusion::ReachabilityLost)]
2984                .into_iter()
2985                .collect(),
2986        )?;
2987
2988        // Four calls: two full rotations of the two-worker eligible set, through
2989        // each selector in turn, so an exclusion honoured only at one cursor
2990        // position could not hide.
2991        for _ in 0..4 {
2992            let selected = registry
2993                .select_and_reserve("ns", "tq", "dev", None)?
2994                .map(|(worker, _reservation)| worker)
2995                .ok_or_else(|| test_failure("two eligible workers remain in the pool"))?;
2996            assert_ne!(
2997                selected.id(),
2998                excluded,
2999                "select_worker must never return the worker the liveness verdict excluded"
3000            );
3001            let candidates = registry.workers_for("ns", "tq", "dev", None)?;
3002            assert_eq!(
3003                candidates.len(),
3004                2,
3005                "workers_for must offer the two ELIGIBLE workers, not all three registered ones"
3006            );
3007            assert!(
3008                candidates.iter().all(|worker| worker.id() != excluded),
3009                "workers_for must not list the excluded worker in ANY position: the push \
3010                 dispatcher walks the whole list"
3011            );
3012        }
3013
3014        for registration in registrations {
3015            registration.deregister()?;
3016        }
3017        Ok(())
3018    }
3019
3020    /// The hazard the eligible-set derivation introduces on the push dispatcher,
3021    /// demonstrated at the bytes rather than argued.
3022    ///
3023    /// `pool_census` deliberately counts REGISTERED node-matched workers with no
3024    /// eligibility filter (#197 R3: `classify` must be able to tell an empty pool
3025    /// from an excluded one), so an all-ineligible pool reports itself SERVED
3026    /// while selection finds nobody. `dispatch_to_node` reads exactly this pair,
3027    /// and treating the disagreement as the registration race it used to be —
3028    /// re-selecting at once — would spin that loop hot with no park and no log
3029    /// for as long as the exclusion lasts. It parks instead; the proof is
3030    /// `a_dispatch_to_an_all_ineligible_pool_parks_until_eligibility_returns` in
3031    /// `dispatch.rs`, and the park's wake-up is proven directly below.
3032    #[test]
3033    fn an_all_ineligible_pool_selects_nobody_while_the_census_still_reads_served()
3034    -> Result<(), ServerError> {
3035        let registry = ConnectedWorkerRegistry::default();
3036        let types = [String::from("dev")];
3037        let (tx, _rx) = mpsc::channel(1);
3038        let worker = registry.register_namespaces(
3039            [String::from("ns")],
3040            "tq",
3041            None,
3042            types.iter(),
3043            tx,
3044            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3045        )?;
3046        let worker_id = worker.worker_id().ok_or_else(missing_id)?;
3047        // 🔴 An OPENING PROBATION specifically, and the choice is load-bearing.
3048        // This test's subject is the census/selection disagreement that
3049        // `dispatch_to_node` must park on rather than spin through, and that
3050        // disagreement survives exactly for the exclusion that clears itself.
3051        // A `ReachabilityLost` pool no longer reaches this state: it now
3052        // classifies as `POLLERS_UNREACHABLE` and publishes a reason instead of
3053        // parking silently, which is the whole of the withdrawn-pool fix. Using
3054        // it here would make this test assert the behaviour that was replaced.
3055        registry.set_dispatch_ineligible(
3056            [(
3057                worker_id,
3058                DispatchExclusion::OpeningProbation { answers: 1 },
3059            )]
3060            .into_iter()
3061            .collect(),
3062        )?;
3063
3064        assert!(
3065            registry.workers_for("ns", "tq", "dev", None)?.is_empty(),
3066            "the push dispatcher's candidate list must exclude an unreachable worker"
3067        );
3068        assert!(
3069            registry
3070                .select_and_reserve("ns", "tq", "dev", None)?
3071                .map(|(worker, _reservation)| worker)
3072                .is_none(),
3073            "and so must the single selector"
3074        );
3075        let census = registry.pool_census("ns", "tq", "dev", None)?;
3076        assert_eq!(
3077            census.compatible_workers, 1,
3078            "the census counts the REGISTERED worker: #197 R3 needs the count to separate an \
3079             empty pool from an excluded one"
3080        );
3081        assert!(
3082            census.is_served(),
3083            "so `classify` reads this address as served and returns None while selection has \
3084             nobody — the disagreement dispatch_to_node must park on rather than spin through"
3085        );
3086        assert!(
3087            census.will_be_served(),
3088            "and the park is the RIGHT outcome here: a probation clears itself within seconds, \
3089             so nobody should be warned and nothing should be published. The pool that has LOST \
3090             reachability is the one that must not reach this state — see \
3091             `the_exclusion_cause_is_what_decides`"
3092        );
3093        assert_eq!(
3094            census.compatible_workers_reachability_lost, 0,
3095            "precondition for the assertion above: this fixture's exclusion is a probation"
3096        );
3097
3098        worker.deregister()?;
3099        Ok(())
3100    }
3101
3102    /// T8 — the arrival subscription RETAINS a wake that lands before it is
3103    /// awaited, and a subscription taken after that wake does not.
3104    ///
3105    /// This is the flight-1 judge's finding reproduced at the bytes and then
3106    /// closed, both arms against the same registry so the pair is read together
3107    /// and neither can rot alone.
3108    ///
3109    /// Both registry wake sources are `Notify::notify_waiters`, which stores NO
3110    /// permit. A dispatcher that read the registry, missed, and only then
3111    /// constructed its wait therefore parked past a registration that had
3112    /// already happened — holding positive census evidence of a live worker,
3113    /// with no second event on the way. On `OutboxTransport::Grpc` no liveness
3114    /// probe runs and no reachability verdict is ever published, so "the next
3115    /// unrelated registration anywhere in the registry" was the only wake left.
3116    ///
3117    /// Polled by hand with a no-op waker rather than raced against a runtime, so
3118    /// retention is PROVEN rather than timed: no clock, no timeout, no runtime.
3119    #[test]
3120    fn an_arrival_subscription_retains_a_wake_taken_before_it() -> Result<(), ServerError> {
3121        use std::future::Future;
3122        use std::task::Waker;
3123
3124        let registry = ConnectedWorkerRegistry::default();
3125        let mut context = Context::from_waker(Waker::noop());
3126
3127        // CONTROL ARM — the base's sequence, subscribe AFTER the look.
3128        // The registration fires `notify_waiters` into an empty waiter list.
3129        let (first_tx, _first_rx) = mpsc::channel(1);
3130        let first = registry.register(
3131            "ns",
3132            [String::from("dev")].iter(),
3133            first_tx,
3134            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3135        )?;
3136        let mut too_late = std::pin::pin!(registry.worker_arrived.notified());
3137        assert!(
3138            matches!(too_late.as_mut().poll(&mut context), Poll::Pending),
3139            "a wait constructed AFTER the registration cannot have retained it: \
3140             `notify_waiters` stored no permit and there was no waiter in the list to \
3141             broadcast to. This is the finding, reproduced."
3142        );
3143        assert!(
3144            matches!(too_late.as_mut().poll(&mut context), Poll::Pending),
3145            "and it stays parked — without this control the arm above would pass on a wait \
3146             that merely reports Pending on its first poll for registration reasons"
3147        );
3148
3149        // SUBJECT ARM — the fix's sequence, subscribe BEFORE the look.
3150        let arrival = registry.worker_arrival();
3151        let (second_tx, _second_rx) = mpsc::channel(1);
3152        let second = registry.register(
3153            "ns",
3154            [String::from("dev")].iter(),
3155            second_tx,
3156            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3157        )?;
3158        let mut arrival = std::pin::pin!(arrival);
3159        assert!(
3160            matches!(arrival.as_mut().poll(&mut context), Poll::Ready(())),
3161            "a subscription taken BEFORE the registry read must retain the registration that \
3162             landed during it: the dispatch that missed by a microsecond must not park"
3163        );
3164        assert!(
3165            matches!(too_late.as_mut().poll(&mut context), Poll::Ready(())),
3166            "the control's own wait was in the list by now, so the SECOND registration wakes \
3167             it — proving the control arm above was parked on the lost wake and not on a \
3168             registry that never notified at all"
3169        );
3170
3171        first.deregister()?;
3172        second.deregister()?;
3173        Ok(())
3174    }
3175
3176    /// The other half of that resolution: publishing a reachability verdict WAKES
3177    /// the selection wait.
3178    ///
3179    /// A dispatch parked because every compatible worker is ineligible is waiting
3180    /// on the verdict, not on a registration — those workers are already
3181    /// registered, so waiting for a registration sleeps through their recovery.
3182    ///
3183    /// Polled by hand with a no-op waker rather than raced against a runtime, so
3184    /// the wake is proven rather than timed. The middle poll is THE CONTROL: a
3185    /// wait that completed on its second poll for any reason at all would satisfy
3186    /// the final assertion and prove nothing about the publication.
3187    #[test]
3188    fn publishing_a_reachability_verdict_wakes_the_selection_wait() -> Result<(), ServerError> {
3189        use std::future::Future;
3190        use std::task::Waker;
3191
3192        let registry = ConnectedWorkerRegistry::default();
3193        let mut waiting = std::pin::pin!(registry.worker_arrival());
3194        let mut context = Context::from_waker(Waker::noop());
3195
3196        assert!(
3197            matches!(waiting.as_mut().poll(&mut context), Poll::Pending),
3198            "the wait parks until something changes what selection can see"
3199        );
3200        assert!(
3201            matches!(waiting.as_mut().poll(&mut context), Poll::Pending),
3202            "and stays parked while nothing has been published: without this control the \
3203             assertion below would pass on a wait that simply completes on a second poll"
3204        );
3205
3206        // Exactly what a probe round does once a worker has served its probation.
3207        registry.set_dispatch_ineligible(BTreeMap::new())?;
3208        assert!(
3209            matches!(waiting.as_mut().poll(&mut context), Poll::Ready(())),
3210            "a published verdict must wake a dispatch parked on eligibility; no registration is \
3211             coming for a worker that never left the registry"
3212        );
3213        Ok(())
3214    }
3215
3216    #[tokio::test]
3217    async fn rotation_cursor_is_pruned_when_last_worker_leaves() -> Result<(), ServerError> {
3218        // The round-robin cursor is keyed on arbitrary caller-supplied strings;
3219        // it must not outlive the pool it rotates, or a never-dying server leaks
3220        // memory. After the last worker for a triple deregisters, no cursor for
3221        // that triple may remain.
3222        let registry = ConnectedWorkerRegistry::default();
3223        let (tx, _rx) = mpsc::channel(1);
3224        let worker = registry
3225            .accept_registration(
3226                &guard(),
3227                &caller("ns"),
3228                &registration_full(&["ns"], "tq", "", &["dev"]),
3229                tx,
3230            )
3231            .await?;
3232
3233        // Drive the lazy cursor insert.
3234        let _ = registry.workers_for("ns", "tq", "dev", None)?;
3235        let key = ActivityKey::new(PoolAddress::new("ns", "tq"), "dev");
3236        assert!(
3237            registry.state()?.rotation.contains_key(&key),
3238            "a lookup must have created the rotation cursor"
3239        );
3240
3241        worker.deregister()?;
3242        let state = registry.state()?;
3243        assert!(
3244            !state.rotation.contains_key(&key),
3245            "the rotation cursor must be pruned once the last worker leaves"
3246        );
3247        assert!(
3248            !state.by_activity.contains_key(&key),
3249            "the activity bucket must also be gone"
3250        );
3251        Ok(())
3252    }
3253
3254    fn missing_id() -> ServerError {
3255        ServerError::lock_poisoned("registration unexpectedly missing a worker id")
3256    }
3257
3258    // ---- Minted-on-use (Control-Plane Phase 1) -----------------------------
3259
3260    fn namespace_store() -> Arc<dyn NamespaceStore> {
3261        Arc::new(aion_store::InMemoryStore::default())
3262    }
3263
3264    fn minting_registry(
3265        store: &Arc<dyn NamespaceStore>,
3266        policy: AutoCreate,
3267    ) -> ConnectedWorkerRegistry {
3268        ConnectedWorkerRegistry::default().with_namespace_minting(Arc::clone(store), policy)
3269    }
3270
3271    #[tokio::test]
3272    async fn open_register_mints_durable_record_and_is_idempotent() -> Result<(), ServerError> {
3273        let store = namespace_store();
3274        let registry = minting_registry(&store, AutoCreate::Open);
3275
3276        // First registration mints the namespace.
3277        let (tx_one, _rx_one) = mpsc::channel(1);
3278        let first = registry
3279            .accept_registration(
3280                &guard(),
3281                &caller("orders"),
3282                &registration("orders", &["charge"]),
3283                tx_one,
3284            )
3285            .await?;
3286        let record = store
3287            .get_namespace("orders")
3288            .await?
3289            .ok_or_else(|| ServerError::namespace_denied("expected a minted record"))?;
3290        assert_eq!(record.name, "orders");
3291        assert_eq!(record.origin, NamespaceOrigin::WorkerMint);
3292
3293        // Re-registering the same namespace is idempotent: no duplicate row,
3294        // and the prior worker is unaffected.
3295        let (tx_two, _rx_two) = mpsc::channel(1);
3296        registry
3297            .accept_registration(
3298                &guard(),
3299                &caller("orders"),
3300                &registration("orders", &["refund"]),
3301                tx_two,
3302            )
3303            .await?;
3304        let all = store.list_namespaces().await?;
3305        assert_eq!(
3306            all.iter().filter(|r| r.name == "orders").count(),
3307            1,
3308            "re-register must not create a duplicate namespace row"
3309        );
3310        drop(first);
3311        Ok(())
3312    }
3313
3314    #[tokio::test]
3315    async fn open_register_mints_each_namespace_in_a_multi_namespace_worker()
3316    -> Result<(), ServerError> {
3317        let store = namespace_store();
3318        let registry = minting_registry(&store, AutoCreate::Open);
3319        let (tx, _rx) = mpsc::channel(1);
3320
3321        registry
3322            .accept_registration(
3323                &guard(),
3324                &multi_caller(&["alpha", "beta"]),
3325                &registration_full(&["alpha", "beta"], "", "", &["charge"]),
3326                tx,
3327            )
3328            .await?;
3329
3330        assert!(store.get_namespace("alpha").await?.is_some());
3331        assert!(store.get_namespace("beta").await?.is_some());
3332        Ok(())
3333    }
3334
3335    // ---- Placement admission (Control-Plane Phase 2, P2-I1) -----------------
3336
3337    /// Pre-mint `namespace` and set its placement to `Pinned{nodes}`, returning a
3338    /// minting registry over the same store so `accept_registration` reads the
3339    /// placement from the SAME durable record.
3340    async fn pinned_registry(
3341        store: &Arc<dyn NamespaceStore>,
3342        namespace: &str,
3343        nodes: &[&str],
3344    ) -> Result<ConnectedWorkerRegistry, ServerError> {
3345        store
3346            .register_namespace(namespace, NamespaceOrigin::Explicit)
3347            .await?;
3348        store
3349            .set_namespace_placement(
3350                namespace,
3351                NamespacePlacement::Pinned {
3352                    nodes: nodes.iter().map(|n| (*n).to_owned()).collect(),
3353                },
3354            )
3355            .await?;
3356        Ok(minting_registry(store, AutoCreate::Open))
3357    }
3358
3359    /// A worker on a node IN the required set registers successfully into a
3360    /// `Pinned{n1}` namespace, and is reachable in the pool.
3361    #[tokio::test]
3362    async fn pinned_admits_a_worker_on_a_required_node() -> Result<(), ServerError> {
3363        let store = namespace_store();
3364        let registry = pinned_registry(&store, "iso", &["n1"]).await?;
3365        let (tx, _rx) = mpsc::channel(1);
3366
3367        let _registration = registry
3368            .accept_registration(
3369                &guard(),
3370                &caller("iso"),
3371                &registration_full(&["iso"], "", "n1", &["charge"]),
3372                tx,
3373            )
3374            .await?;
3375
3376        assert_eq!(
3377            registry
3378                .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", Some("n1"))?
3379                .len(),
3380            1,
3381            "an n1 worker must be admitted into the Pinned{{n1}} namespace's pool"
3382        );
3383        Ok(())
3384    }
3385
3386    /// A worker on a node NOT in the required set is rejected — the WHOLE
3387    /// registration fails (loud) and no worker is inserted. This would FAIL under
3388    /// no admission gate (the worker would join and steal Pinned dispatches).
3389    #[tokio::test]
3390    async fn pinned_rejects_a_wrong_node_worker() -> Result<(), ServerError> {
3391        let store = namespace_store();
3392        let registry = pinned_registry(&store, "iso", &["n1"]).await?;
3393        let (tx, _rx) = mpsc::channel(1);
3394
3395        let denied = registry
3396            .accept_registration(
3397                &guard(),
3398                &caller("iso"),
3399                &registration_full(&["iso"], "", "n2", &["charge"]),
3400                tx,
3401            )
3402            .await;
3403        assert!(
3404            matches!(denied, Err(ServerError::Namespace { .. })),
3405            "a wrong-node (n2) worker must be rejected from a Pinned{{n1}} namespace"
3406        );
3407        assert!(
3408            registry
3409                .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", None)?
3410                .is_empty(),
3411            "a rejected registration must not insert a worker on any node"
3412        );
3413        Ok(())
3414    }
3415
3416    /// A worker advertising NO node (`node == ""` → `None`) is rejected from a
3417    /// `Pinned{n1}` namespace: an unlabelled worker can never satisfy a hard pin.
3418    #[tokio::test]
3419    async fn pinned_rejects_a_node_less_worker() -> Result<(), ServerError> {
3420        let store = namespace_store();
3421        let registry = pinned_registry(&store, "iso", &["n1"]).await?;
3422        let (tx, _rx) = mpsc::channel(1);
3423
3424        let denied = registry
3425            .accept_registration(
3426                &guard(),
3427                &caller("iso"),
3428                &registration_full(&["iso"], "", "", &["charge"]),
3429                tx,
3430            )
3431            .await;
3432        assert!(
3433            matches!(denied, Err(ServerError::Namespace { .. })),
3434            "a node-less worker must be rejected from a Pinned{{n1}} namespace"
3435        );
3436        assert!(
3437            registry
3438                .workers_for("iso", DEFAULT_TASK_QUEUE, "charge", None)?
3439                .is_empty(),
3440            "a rejected node-less registration must not insert a worker"
3441        );
3442        Ok(())
3443    }
3444
3445    /// Reject-WHOLE-registration (Open Decision 6): a worker serving BOTH a
3446    /// non-isolated namespace and a `Pinned{n1}` namespace on a wrong node is
3447    /// rejected entirely — the compliant namespace does NOT get a partial admit.
3448    #[tokio::test]
3449    async fn pinned_violation_rejects_the_whole_multi_namespace_registration()
3450    -> Result<(), ServerError> {
3451        let store = namespace_store();
3452        let registry = pinned_registry(&store, "iso", &["n1"]).await?;
3453        let (tx, _rx) = mpsc::channel(1);
3454
3455        let denied = registry
3456            .accept_registration(
3457                &guard(),
3458                &multi_caller(&["free", "iso"]),
3459                &registration_full(&["free", "iso"], "", "n2", &["charge"]),
3460                tx,
3461            )
3462            .await;
3463        assert!(
3464            matches!(denied, Err(ServerError::Namespace { .. })),
3465            "a wrong-node worker serving a Pinned namespace fails the WHOLE registration"
3466        );
3467        assert!(
3468            registry
3469                .workers_for("free", DEFAULT_TASK_QUEUE, "charge", None)?
3470                .is_empty(),
3471            "the compliant namespace must NOT be partially admitted"
3472        );
3473        Ok(())
3474    }
3475
3476    /// Unplaced and Prefer namespaces are UNAFFECTED: a node-less worker registers
3477    /// normally (byte-identical to the pre-P2-I1 behaviour). Only Pinned gates.
3478    #[tokio::test]
3479    async fn unplaced_and_prefer_admission_is_unaffected_by_the_pinned_gate()
3480    -> Result<(), ServerError> {
3481        let store = namespace_store();
3482        // `unpl` is left Unplaced (default); `pref` is Prefer{n1}. A node-less
3483        // worker must be admitted into BOTH.
3484        store
3485            .register_namespace("pref", NamespaceOrigin::Explicit)
3486            .await?;
3487        store
3488            .set_namespace_placement(
3489                "pref",
3490                NamespacePlacement::Prefer {
3491                    nodes: ["n1".to_owned()].into_iter().collect(),
3492                },
3493            )
3494            .await?;
3495        let registry = minting_registry(&store, AutoCreate::Open);
3496
3497        let (tx_a, _rx_a) = mpsc::channel(1);
3498        let _reg_a = registry
3499            .accept_registration(
3500                &guard(),
3501                &caller("unpl"),
3502                &registration_full(&["unpl"], "", "", &["charge"]),
3503                tx_a,
3504            )
3505            .await?;
3506        let (tx_b, _rx_b) = mpsc::channel(1);
3507        let _reg_b = registry
3508            .accept_registration(
3509                &guard(),
3510                &caller("pref"),
3511                &registration_full(&["pref"], "", "", &["charge"]),
3512                tx_b,
3513            )
3514            .await?;
3515
3516        assert_eq!(
3517            registry
3518                .workers_for("unpl", DEFAULT_TASK_QUEUE, "charge", None)?
3519                .len(),
3520            1,
3521            "an Unplaced namespace admits a node-less worker unchanged"
3522        );
3523        assert_eq!(
3524            registry
3525                .workers_for("pref", DEFAULT_TASK_QUEUE, "charge", None)?
3526                .len(),
3527            1,
3528            "a Prefer namespace admits a node-less worker unchanged (only Pinned gates)"
3529        );
3530        Ok(())
3531    }
3532
3533    /// A default (no-minter) registry is byte-identical: the placement gate is a
3534    /// no-op with no minter installed, so a node-less worker registers freely even
3535    /// though there is no way to have set a placement in the first place.
3536    #[tokio::test]
3537    async fn no_minter_registry_skips_the_placement_gate() -> Result<(), ServerError> {
3538        let registry = ConnectedWorkerRegistry::default();
3539        let (tx, _rx) = mpsc::channel(1);
3540        let _registration = registry
3541            .accept_registration(
3542                &guard(),
3543                &caller("plain"),
3544                &registration_full(&["plain"], "", "", &["charge"]),
3545                tx,
3546            )
3547            .await?;
3548        assert_eq!(
3549            registry
3550                .workers_for("plain", DEFAULT_TASK_QUEUE, "charge", None)?
3551                .len(),
3552            1,
3553            "with no minter the placement gate is a no-op — registration is unchanged"
3554        );
3555        Ok(())
3556    }
3557
3558    #[tokio::test]
3559    async fn concurrent_registrations_for_a_new_namespace_create_exactly_one_record()
3560    -> Result<(), ServerError> {
3561        let store = namespace_store();
3562        let registry = minting_registry(&store, AutoCreate::Open);
3563
3564        let mut handles = Vec::new();
3565        for _ in 0..8 {
3566            let registry = registry.clone();
3567            handles.push(tokio::spawn(async move {
3568                let (tx, rx) = mpsc::channel(1);
3569                let outcome = registry
3570                    .accept_registration(
3571                        &guard(),
3572                        &caller("rush"),
3573                        &registration("rush", &["charge"]),
3574                        tx,
3575                    )
3576                    .await;
3577                // Keep the receiver alive for the duration of the registration.
3578                drop(rx);
3579                outcome.map(|registration| registration.worker_id())
3580            }));
3581        }
3582        for handle in handles {
3583            handle
3584                .await
3585                .map_err(|_| ServerError::lock_poisoned("registration task panicked"))??;
3586        }
3587
3588        let all = store.list_namespaces().await?;
3589        assert_eq!(
3590            all.iter().filter(|r| r.name == "rush").count(),
3591            1,
3592            "racing registrations must converge on exactly one durable record"
3593        );
3594        Ok(())
3595    }
3596
3597    #[tokio::test]
3598    async fn closed_rejects_unknown_namespace_and_does_not_create_it() -> Result<(), ServerError> {
3599        let store = namespace_store();
3600        let registry = minting_registry(&store, AutoCreate::Closed);
3601        let (tx, _rx) = mpsc::channel(1);
3602
3603        let denied = registry
3604            .accept_registration(
3605                &guard(),
3606                &caller("ghost"),
3607                &registration("ghost", &["charge"]),
3608                tx,
3609            )
3610            .await;
3611        assert!(
3612            matches!(denied, Err(ServerError::Namespace { .. })),
3613            "closed policy must reject an unknown namespace"
3614        );
3615        assert!(
3616            store.get_namespace("ghost").await?.is_none(),
3617            "closed policy must NOT create the namespace it rejected"
3618        );
3619        let tq = DEFAULT_TASK_QUEUE;
3620        assert!(
3621            registry
3622                .workers_for("ghost", tq, "charge", None)?
3623                .is_empty(),
3624            "a rejected registration must not insert a worker"
3625        );
3626        Ok(())
3627    }
3628
3629    #[tokio::test]
3630    async fn closed_admits_a_known_namespace() -> Result<(), ServerError> {
3631        let store = namespace_store();
3632        // Pre-mint the namespace (the POST /namespaces escape hatch's effect).
3633        store
3634            .register_namespace("known", NamespaceOrigin::Explicit)
3635            .await?;
3636        let registry = minting_registry(&store, AutoCreate::Closed);
3637        let (tx, _rx) = mpsc::channel(1);
3638
3639        // Bind the registration token: dropping it deregisters the worker.
3640        let _registration = registry
3641            .accept_registration(
3642                &guard(),
3643                &caller("known"),
3644                &registration("known", &["charge"]),
3645                tx,
3646            )
3647            .await?;
3648        let tq = DEFAULT_TASK_QUEUE;
3649        assert_eq!(
3650            registry.workers_for("known", tq, "charge", None)?.len(),
3651            1,
3652            "a known namespace must register under closed policy"
3653        );
3654        Ok(())
3655    }
3656
3657    #[tokio::test]
3658    async fn no_minter_leaves_registration_untouched() -> Result<(), ServerError> {
3659        // The default registry installs no minter: registration succeeds and
3660        // never touches any namespace registry (byte-identical legacy path).
3661        let registry = ConnectedWorkerRegistry::default();
3662        let (tx, _rx) = mpsc::channel(1);
3663        let _registration = registry
3664            .accept_registration(
3665                &guard(),
3666                &caller("orders"),
3667                &registration("orders", &["charge"]),
3668                tx,
3669            )
3670            .await?;
3671        let tq = DEFAULT_TASK_QUEUE;
3672        assert_eq!(registry.workers_for("orders", tq, "charge", None)?.len(), 1);
3673        Ok(())
3674    }
3675
3676    /// R1 census: an address no worker has ever served reports an empty fleet
3677    /// and no poller age at all — "never seen" is not "seen long ago".
3678    #[test]
3679    fn a_never_served_address_censuses_empty_with_no_poller_age() -> Result<(), ServerError> {
3680        let registry = ConnectedWorkerRegistry::default();
3681        let census = registry.pool_census("default", "general", "greet", None)?;
3682        assert_eq!(census.workers_in_pool, 0);
3683        assert_eq!(census.workers_serving_activity, 0);
3684        assert_eq!(census.compatible_workers, 0);
3685        assert_eq!(census.last_compatible_poller_age, None);
3686        assert!(!census.is_served());
3687        Ok(())
3688    }
3689
3690    /// R1 census: pool membership, activity coverage, and node coverage are
3691    /// three separate counts — that separation is what tells `NO_LIVE_POLLERS`
3692    /// apart from `POLLERS_INCOMPATIBLE`.
3693    #[test]
3694    fn the_census_separates_pool_activity_and_node_coverage() -> Result<(), ServerError> {
3695        let registry = ConnectedWorkerRegistry::default();
3696        let (tx, _rx) = mpsc::channel(1);
3697        let _worker = registry.register_namespaces(
3698            [String::from("default")],
3699            "general",
3700            Some(String::from("n1")),
3701            [String::from("greet")].iter(),
3702            tx,
3703            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3704        )?;
3705
3706        let unpinned = registry.pool_census("default", "general", "greet", None)?;
3707        assert_eq!(unpinned.workers_in_pool, 1);
3708        assert_eq!(unpinned.workers_serving_activity, 1);
3709        assert_eq!(unpinned.compatible_workers, 1);
3710        assert_eq!(unpinned.last_compatible_poller_age, Some(Duration::ZERO));
3711
3712        // Same pool, activity nobody advertises: pollers are live but
3713        // incompatible.
3714        let other_activity = registry.pool_census("default", "general", "settle", None)?;
3715        assert_eq!(other_activity.workers_in_pool, 1);
3716        assert_eq!(other_activity.workers_serving_activity, 0);
3717        assert_eq!(other_activity.compatible_workers, 0);
3718
3719        // Same activity, wrong node: coverage exists in the pool but not for
3720        // this dispatch.
3721        let wrong_node = registry.pool_census("default", "general", "greet", Some("n2"))?;
3722        assert_eq!(wrong_node.workers_serving_activity, 1);
3723        assert_eq!(wrong_node.compatible_workers, 0);
3724        assert_eq!(wrong_node.last_compatible_poller_age, None);
3725        Ok(())
3726    }
3727
3728    /// ACCEPTANCE (b) FOR #58, wiring half: a worker whose PROCESS looks alive
3729    /// but which the server cannot REACH on the push leg must lose dispatch
3730    /// eligibility — and losing it must actually remove it from selection.
3731    ///
3732    /// This is the composition the lease split exists for. The tracker half is
3733    /// pinned in `heartbeat::reachability_tests`; this pins the half that turns
3734    /// that verdict into a dispatch decision, using the SAME two types the
3735    /// liveness probe wires together — a real [`HeartbeatTracker`] census
3736    /// feeding a real [`ConnectedWorkerRegistry`].
3737    ///
3738    /// The staging is Finding B exactly: the worker's own liveness pump keeps
3739    /// beating (`record_connection_activity`) well past the window, so every
3740    /// signal that says "this process is alive" says so — and NO ping is ever
3741    /// answered, so the one signal that says "the server can reach it" is
3742    /// absent. Before the split those were one lease and the pump's weaker
3743    /// positive evidence buried the ping's negative evidence.
3744    ///
3745    /// The control is the second half of the test: the same worker, reachable,
3746    /// must still be selected. Without it a registry that selected NOBODY would
3747    /// satisfy the first assertion and prove nothing.
3748    #[test]
3749    fn a_pump_alive_worker_the_server_cannot_reach_is_not_selected() -> Result<(), ServerError> {
3750        const WINDOW: Duration = Duration::from_secs(30);
3751
3752        let registry = ConnectedWorkerRegistry::default();
3753        let tracker = HeartbeatTracker::new(WINDOW);
3754        let (tx, _rx) = mpsc::channel(1);
3755        let worker = registry.register_namespaces(
3756            [String::from("default")],
3757            "general",
3758            None,
3759            [String::from("greet")].iter(),
3760            tx,
3761            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3762        )?;
3763        let Some(worker_id) = worker.worker_id() else {
3764            return Err(test_failure("registration carries an id"));
3765        };
3766        let start = Instant::now();
3767        tracker.register_connection(worker_id, start)?;
3768
3769        // Publishing the tracker's verdict is one motion the liveness probe
3770        // performs at the end of every round, and this test performs it four
3771        // times. Written once so the four publications cannot drift apart and
3772        // quietly stop testing the same thing.
3773        let publish = |now: Instant| -> Result<(), ServerError> {
3774            registry.set_dispatch_ineligible(
3775                tracker
3776                    .unreachable_workers(now)?
3777                    .into_iter()
3778                    .map(|excluded| (excluded.worker_id, excluded.exclusion))
3779                    .collect(),
3780            )
3781        };
3782
3783        // Baseline: the worker SERVES ITS PROBATION and is therefore selectable.
3784        // Registration alone grants nothing — the handshake ack is sent, never
3785        // confirmed received — so eligibility here is earned by answered pings.
3786        // If this did not hold, no worker could ever be dispatched to.
3787        for _ in 0..DISPATCH_PROBATION_PINGS {
3788            assert!(
3789                tracker.record_dispatch_reachability(worker_id, start)?,
3790                "the worker is tracked while it serves its probation"
3791            );
3792        }
3793        publish(start)?;
3794        assert!(
3795            registry
3796                .select_and_reserve("default", "general", "greet", None)?
3797                .map(|(worker, _reservation)| worker)
3798                .is_some(),
3799            "a worker that has answered a full run of pings must be selectable: this is the \
3800             control, and without it a registry selecting NOBODY would satisfy every assertion \
3801             below"
3802        );
3803
3804        // Now the poisoned-connection shape: the pump beats past the window and
3805        // not one ping is answered. Each unanswered probe is recorded exactly as
3806        // the liveness probe records it, because that is the trajectory a real
3807        // poisoned connection takes — staleness alone would be a shape only a
3808        // stopped probe could produce.
3809        let much_later = start + WINDOW * 4;
3810        assert!(
3811            tracker.record_connection_activity(worker_id, much_later)?,
3812            "the worker is still tracked; its process is plainly alive"
3813        );
3814        assert!(
3815            tracker.record_dispatch_unreachable(worker_id)?,
3816            "the probe fired and went unanswered"
3817        );
3818        publish(much_later)?;
3819
3820        assert!(
3821            registry.is_dispatch_ineligible(worker_id)?,
3822            "a worker the server cannot reach must be marked ineligible however alive its \
3823             process looks"
3824        );
3825        assert!(
3826            registry
3827                .select_and_reserve("default", "general", "greet", None)?
3828                .map(|(worker, _reservation)| worker)
3829                .is_none(),
3830            "an ineligible worker must not be SELECTED: selecting one produces a dispatch that \
3831             can only fail, and on the liminal transport it fails by consuming connection \
3832             capacity — making the unreachability worse"
3833        );
3834
3835        // The recovery path: exclusion must be WITHDRAWABLE, or a single bad
3836        // round would strand a healthy worker forever. It is withdrawn by a
3837        // served probation, not by one answer — and the intermediate assertion
3838        // below pins that distinction rather than assuming it.
3839        assert!(
3840            tracker.record_dispatch_reachability(worker_id, much_later)?,
3841            "the worker is still tracked"
3842        );
3843        publish(much_later)?;
3844        assert!(
3845            registry.is_dispatch_ineligible(worker_id)?,
3846            "one answer part-way through a fresh probation must NOT restore eligibility: a link \
3847             answering one probe in three would otherwise flap in and out of selection"
3848        );
3849
3850        for _ in 1..DISPATCH_PROBATION_PINGS {
3851            assert!(
3852                tracker.record_dispatch_reachability(worker_id, much_later)?,
3853                "the worker is still tracked"
3854            );
3855        }
3856        publish(much_later)?;
3857        assert!(
3858            !registry.is_dispatch_ineligible(worker_id)?,
3859            "an answered ping must clear the exclusion"
3860        );
3861        assert!(
3862            registry
3863                .select_and_reserve("default", "general", "greet", None)?
3864                .map(|(worker, _reservation)| worker)
3865                .is_some(),
3866            "and the worker must be selectable again"
3867        );
3868        Ok(())
3869    }
3870
3871    /// R1 census: after the last compatible worker leaves, the address reports
3872    /// how long ago it was served — the age an operator sees on the parked
3873    /// dispatch's WARN.
3874    #[test]
3875    fn a_departed_worker_leaves_a_last_compatible_poller_age() -> Result<(), ServerError> {
3876        let registry = ConnectedWorkerRegistry::default();
3877        let (tx, _rx) = mpsc::channel(1);
3878        let worker = registry.register_namespaces(
3879            [String::from("default")],
3880            "general",
3881            None,
3882            [String::from("greet")].iter(),
3883            tx,
3884            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3885        )?;
3886        worker.deregister()?;
3887
3888        let census = registry.pool_census("default", "general", "greet", None)?;
3889        assert_eq!(census.workers_in_pool, 0);
3890        assert_eq!(census.compatible_workers, 0);
3891        let age = census
3892            .last_compatible_poller_age
3893            .ok_or_else(|| test_failure("a departed worker must leave an age behind"))?;
3894        assert!(
3895            age < Duration::from_secs(60),
3896            "the recorded departure is implausibly old: {age:?}"
3897        );
3898
3899        // A worker returning to the address clears the departure record: the
3900        // map that answers "how long ago" never accumulates live addresses.
3901        let (tx, _rx) = mpsc::channel(1);
3902        let _back = registry.register_namespaces(
3903            [String::from("default")],
3904            "general",
3905            None,
3906            [String::from("greet")].iter(),
3907            tx,
3908            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3909        )?;
3910        let served = registry.pool_census("default", "general", "greet", None)?;
3911        assert_eq!(served.last_compatible_poller_age, Some(Duration::ZERO));
3912        assert!(served.is_served());
3913        Ok(())
3914    }
3915}