Skip to main content

aion_server/worker/
dispatch.rs

1//! Push dispatch for remote activity workers and result handoff to the engine contract.
2
3use std::collections::BTreeMap;
4
5use aion_core::{ActivityError, ActivityId, Payload, RunId, WorkflowId};
6use aion_proto::{
7    ProtoActivityId, ProtoActivityResult, ProtoActivityTask, ProtoPayload, ProtoRunId,
8    ProtoWorkflowId, WireError, proto_activity_result,
9};
10
11use crate::error::ServerError;
12use crate::shutdown::DrainState;
13use crate::worker::delivery_intent::SharedDeliveryIntent;
14use crate::worker::envelope::{CompletionFences, CompletionToken, idempotency_key};
15use crate::worker::grpc_task_delivery::GrpcTaskDelivery;
16use crate::worker::lease_record::LeaseRecorderSeam;
17use crate::worker::queue_service::declarations::QueueDeclarationSource;
18use crate::worker::queue_service::policy::QueueServiceConfig;
19use crate::worker::queue_service::state::QueueServiceState;
20use crate::worker::queue_service::taxonomy::{QueueServiceReason, ServiceAddress};
21use crate::worker::queue_service::wait::{
22    ServiceWait, clear_selection_miss, observe_selection_miss,
23};
24use crate::worker::registry::{ConnectedWorkerRegistry, WorkerDelivery};
25use crate::worker::task_delivery::{DeliveryAccepted, TaskDelivery, WorkerTaskDelivery};
26use tracing::{Instrument, info_span};
27
28/// Scheduled remote activity that must be placed with a connected worker.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct ScheduledActivity {
31    /// Namespace selected by the adapter boundary before dispatch — the
32    /// correctness/isolation boundary the activity may dispatch within.
33    pub namespace: String,
34    /// Task queue (pool/flavour) selected within the namespace. The worker-pool
35    /// address is `(namespace, task_queue)`; an empty value is normalized to the
36    /// named default pool by the registry lookup.
37    pub task_queue: String,
38    /// Activity type to match against worker registrations, *within* the
39    /// selected pool.
40    pub activity_type: String,
41    /// Optional node locality affinity. `Some(node)` pins this dispatch to
42    /// workers advertising that node (require semantics: it waits if none are
43    /// present, exactly like the no-worker path); `None` is unpinned and reaches
44    /// any worker in the `(namespace, task_queue)` pool — byte-identical to the
45    /// pre-NODE behaviour. Producers stamp `None` until SDK selection (NODE-4)
46    /// and the durable column (NODE-2) land.
47    pub node: Option<String>,
48    /// Owning workflow id.
49    pub workflow_id: WorkflowId,
50    /// Correlating activity id.
51    pub activity_id: ActivityId,
52    /// Concrete workflow run that staged this task, when known.
53    pub run_id: Option<RunId>,
54    /// Opaque activity input payload.
55    pub input: Payload,
56    /// One-based delivery attempt stamped by the dispatching engine seam.
57    /// Zero is malformed on the wire; producers must always stamp it.
58    pub attempt: u32,
59    /// Display labels the workflow attached to the activity. Display metadata
60    /// only — carried to the worker for its logs and the dashboard.
61    pub labels: BTreeMap<String, String>,
62    /// Which caller staged this dispatch, and therefore what it owes a delivery
63    /// that is still waiting. See [`DispatchOrigin`].
64    pub origin: DispatchOrigin,
65}
66
67/// Which caller staged a dispatch — and, because the two owe a waiting delivery
68/// different things, which abandonment condition applies to it.
69///
70/// # Why this is a named type rather than an optional dispatch key
71///
72/// An `Option<String>` whose presence selected the intent would be control flow
73/// wearing configuration: an engine-scheduled activity that accidentally
74/// acquired a key would silently become an outbox claim, and nothing would say
75/// so. The same objection retires `outbox.transport` as a routing key, so
76/// reintroducing its shape here would be a poor trade. With two named arms the
77/// caller **declares** which it is, absence never decides, and the match in
78/// `send_to_candidates` is exhaustive — a third origin cannot be added without
79/// the compiler asking what it owes.
80#[derive(Clone, Debug, Eq, PartialEq)]
81pub enum DispatchOrigin {
82    /// Scheduled by the engine seam directly. There is no row and no claim, so
83    /// the delivery is wanted while the deployment is not draining and the
84    /// chosen worker is still registered.
85    Engine,
86    /// Claimed from the durable outbox by a pass that holds the row's claim in
87    /// the delivery gate. The delivery is wanted only while that claim stands —
88    /// a term no transport can evaluate for itself, because a key that was
89    /// never begun reads identically to one that was released.
90    OutboxRow {
91        /// The row's dispatch key, as held in the delivery gate.
92        dispatch_key: String,
93    },
94}
95
96impl ScheduledActivity {
97    /// Return the concrete run required to derive a run-scoped effect key.
98    /// Refuses a legacy row without a run id because no run-scoped
99    /// idempotency key can be truthfully derived.
100    fn require_run_id(&self) -> Result<&RunId, ServerError> {
101        self.run_id.as_ref().ok_or_else(|| {
102            ServerError::worker_dispatch(
103                self.namespace.clone(),
104                self.activity_type.clone(),
105                "activity run id is missing; refusing unfenced external effect",
106            )
107        })
108    }
109
110    /// Build the wire task pushed to the worker stream.
111    ///
112    /// # Errors
113    ///
114    /// Refuses a legacy row without a run id because no run-scoped
115    /// idempotency key can be truthfully derived.
116    pub fn to_task(
117        &self,
118        completion_token: &CompletionToken,
119    ) -> Result<ProtoActivityTask, ServerError> {
120        let run_id = self.require_run_id()?;
121        Ok(ProtoActivityTask {
122            workflow_id: Some(ProtoWorkflowId::from(self.workflow_id.clone())),
123            activity_id: Some(ProtoActivityId::from(self.activity_id.clone())),
124            activity_type: self.activity_type.clone(),
125            input: Some(ProtoPayload::from(self.input.clone())),
126            attempt: self.attempt,
127            labels: self.labels.clone().into_iter().collect(),
128            run_id: Some(ProtoRunId::from(run_id.clone())),
129            completion_token: completion_token.as_str().to_owned(),
130            idempotency_key: idempotency_key(&self.workflow_id, run_id, &self.activity_id),
131        })
132    }
133}
134
135/// Push dispatcher backed by the connected-worker registry.
136#[derive(Clone)]
137pub struct ActivityDispatcher {
138    registry: ConnectedWorkerRegistry,
139    drain_state: DrainState,
140    completion_fences: CompletionFences,
141    /// Deployed queue declarations, live unserved state, and the operator's
142    /// queue-service policy — the three things a selection miss must be
143    /// classified against for the park to be visible rather than silent.
144    ///
145    /// Defaulted like `drain_state` above, and shared with the rest of the
146    /// server by `with_queue_service`. An unshared default still classifies
147    /// and still logs; what it loses is only the queryable state, which is why
148    /// the loud half of the report can never be switched off by wiring.
149    queue_declarations: QueueDeclarationSource,
150    queue_service_state: QueueServiceState,
151    queue_service_config: QueueServiceConfig,
152    /// Cluster-event publisher an unbounded park announces itself on (#266
153    /// T4). `None` (isolated tests) loses only the pushed echo; the WARN and
154    /// the queryable state above cannot be switched off by wiring.
155    cluster_publisher: Option<crate::cluster_publisher::ClusterEventPublisher>,
156    /// The deployment's drain gate, consulted through this pass's
157    /// [`DispatcherPass`] intent so a delivery in flight stops waiting when the
158    /// server is going away.
159    ///
160    /// Defaulted like `drain_state`: an unshared default simply never reports a
161    /// drain, which loses the early abandon and nothing else — the delivery
162    /// still resolves on its own reply or its worker's departure.
163    delivery_gate: crate::worker::outbox_dispatcher::DeliveryGate,
164    /// The liminal delivery arm, when this server has one.
165    ///
166    /// `None` on every gRPC-only deployment, where no liminal worker can be
167    /// selected in the first place. When a liminal worker IS selected and this
168    /// is `None`, the delivery reports a failure and the worker keeps its
169    /// registration — a server's missing wiring must not destroy a healthy
170    /// worker (#52).
171    #[cfg(feature = "liminal-transport")]
172    liminal_delivery: Option<std::sync::Arc<dyn WorkerTaskDelivery>>,
173    /// Where an accepted delivery's lease is recorded (WA-010 R3). Shared with
174    /// the bridge through `PendingActivities` by `with_lease_recorder`; an
175    /// unshared default records nothing and counts every lease as lost, out
176    /// loud, so a dispatcher wired without a recorder cannot be silent about
177    /// it.
178    lease_recorder: LeaseRecorderSeam,
179    /// The liveness tracker this path hands a delivered dispatch over to, so
180    /// the work it places is COUNTED against the worker's advertised capacity.
181    ///
182    /// The bridge has always had one. This path did not, and the consequence was
183    /// that the capacity filter — which reads the count `track_task` maintains —
184    /// saw zero for a worker whose slots were entirely consumed by outbox work.
185    /// A fan of N legs at one worker all read a count nobody had claimed, all
186    /// passed the filter, and all pushed; the surplus was then refused, and
187    /// refusals ride a BOUNDED transport-loss ledger, so a persistently
188    /// over-pushed worker dead-lettered the overflow. The server asserted a
189    /// bound it did not have on the path with the most parallelism.
190    ///
191    /// `None` only in isolated tests. A dispatcher wired without one still
192    /// reserves per candidate — so it can never over-push a single pass — but it
193    /// cannot hand the count over, and it says so at ERROR on every delivery
194    /// rather than quietly under-counting.
195    heartbeat_tracker: Option<crate::worker::heartbeat::HeartbeatTracker>,
196}
197
198mod candidates;
199
200use candidates::CandidateOutcome;
201
202impl std::fmt::Debug for ActivityDispatcher {
203    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        let mut debug = formatter.debug_struct("ActivityDispatcher");
205        debug.field("cluster_publisher", &self.cluster_publisher.is_some());
206        // Reported by PRESENCE: a delivery object has no useful debug form, and
207        // its ABSENCE is exactly the fact an operator reading a "no liminal
208        // delivery wired" refusal needs to confirm.
209        #[cfg(feature = "liminal-transport")]
210        debug.field("liminal_delivery", &self.liminal_delivery.is_some());
211        debug.finish_non_exhaustive()
212    }
213}
214
215impl ActivityDispatcher {
216    /// Build a dispatcher over the shared worker registry.
217    #[must_use]
218    pub fn new(registry: ConnectedWorkerRegistry) -> Self {
219        Self {
220            registry,
221            drain_state: DrainState::default(),
222            completion_fences: CompletionFences::default(),
223            queue_declarations: QueueDeclarationSource::default(),
224            queue_service_state: QueueServiceState::default(),
225            queue_service_config: QueueServiceConfig::default(),
226            cluster_publisher: None,
227            delivery_gate: crate::worker::outbox_dispatcher::DeliveryGate::default(),
228            #[cfg(feature = "liminal-transport")]
229            liminal_delivery: None,
230            lease_recorder: LeaseRecorderSeam::default(),
231            heartbeat_tracker: None,
232        }
233    }
234
235    /// Share the deployment's liveness tracker, so a dispatch this path places
236    /// is counted against the worker's advertised capacity exactly as a
237    /// bridge-seam dispatch is.
238    ///
239    /// Required for the capacity contract to hold on this path: the tracker's
240    /// in-flight map IS the count selection reads.
241    #[must_use]
242    pub fn with_heartbeat_tracker(
243        mut self,
244        heartbeat_tracker: crate::worker::heartbeat::HeartbeatTracker,
245    ) -> Self {
246        self.heartbeat_tracker = Some(heartbeat_tracker);
247        self
248    }
249
250    /// Share the lease-record seam, so an accepted delivery on this path is
251    /// attributed through the same recorder (and counted on the same ledger)
252    /// as one on the engine-seam bridge.
253    #[must_use]
254    pub fn with_lease_recorder(mut self, lease_recorder: LeaseRecorderSeam) -> Self {
255        self.lease_recorder = lease_recorder;
256        self
257    }
258
259    /// Share the deployment's delivery gate, so a dispatch waiting on a blocking
260    /// transport abandons promptly when the server begins draining.
261    #[must_use]
262    pub fn with_delivery_gate(
263        mut self,
264        delivery_gate: crate::worker::outbox_dispatcher::DeliveryGate,
265    ) -> Self {
266        self.delivery_gate = delivery_gate;
267        self
268    }
269
270    /// Install the liminal delivery arm, so a liminal-registered worker selected
271    /// by this dispatcher is SERVED over its own transport rather than
272    /// deregistered for lacking a gRPC sender (#52).
273    #[cfg(feature = "liminal-transport")]
274    #[must_use]
275    pub fn with_liminal_delivery(
276        mut self,
277        liminal_delivery: std::sync::Arc<dyn WorkerTaskDelivery>,
278    ) -> Self {
279        self.liminal_delivery = Some(liminal_delivery);
280        self
281    }
282
283    /// Share the deployment-global cluster-event publisher so a dispatch
284    /// parked with no availability deadline on this leg is announced on the
285    /// operator's real-time channel, not only in the log (#266 T4).
286    #[must_use]
287    pub fn with_cluster_publisher(
288        mut self,
289        cluster_publisher: crate::cluster_publisher::ClusterEventPublisher,
290    ) -> Self {
291        self.cluster_publisher = Some(cluster_publisher);
292        self
293    }
294
295    /// Share the queue-service seams so a park on this path reaches the same
296    /// `GET /queues/unserved` and `describe` surfaces the direct path feeds.
297    #[must_use]
298    pub fn with_queue_service(
299        mut self,
300        declarations: QueueDeclarationSource,
301        state: QueueServiceState,
302        config: QueueServiceConfig,
303    ) -> Self {
304        self.queue_declarations = declarations;
305        self.queue_service_state = state;
306        self.queue_service_config = config;
307        self
308    }
309
310    /// Share the server drain gate.
311    #[must_use]
312    pub fn with_drain_state(mut self, drain_state: DrainState) -> Self {
313        self.drain_state = drain_state;
314        self
315    }
316
317    /// Share the completion-generation registry used by result ingestion.
318    #[must_use]
319    pub fn with_completion_fences(mut self, completion_fences: CompletionFences) -> Self {
320        self.completion_fences = completion_fences;
321        self
322    }
323
324    /// Push a scheduled activity to a matching worker.
325    ///
326    /// # Errors
327    ///
328    /// Returns a typed dispatch error if no worker is available or the selected
329    /// stream is closed; returns lock poison if registry access cannot be trusted.
330    pub async fn dispatch(&self, activity: &ScheduledActivity) -> Result<(), ServerError> {
331        let span = info_span!(
332            "activity_dispatch",
333            operation = "activity_dispatch",
334            namespace = %activity.namespace,
335            task_queue = %activity.task_queue,
336            node = activity.node.as_deref(),
337            workflow_id = %activity.workflow_id,
338            activity_id = %activity.activity_id,
339            activity_type = %activity.activity_type,
340            worker_id = tracing::field::Empty,
341        );
342        let span_fields = span.clone();
343
344        async {
345            self.dispatch_to_node(activity, activity.node.as_deref(), &span_fields)
346                .await
347        }
348        .instrument(span)
349        .await
350        .inspect_err(|error| {
351            log_dispatch_error("activity_dispatch", activity, error);
352        })
353    }
354
355    /// Dispatch `activity` preferring workers on one of the `preferred` node
356    /// labels, spilling to ANY live worker when none of the preferred labels has a
357    /// live worker (Control-Plane Phase 2, P2-P3 — the `Prefer{L}` soft spill).
358    ///
359    /// This is consulted ONLY for an UNPINNED activity (`activity.node == None`):
360    /// a per-activity authored pin always wins and is dispatched through
361    /// [`Self::dispatch`] unchanged. The recorded row's `node` is NEVER mutated —
362    /// preference is a pure dispatch-time worker-selection optimization in this
363    /// non-replayed path, exactly like the existing round-robin, so replay is
364    /// untouched (CP-Phase-2 §2.4).
365    ///
366    /// The prefer-then-spill tier sequence is derived ONCE, from the shared
367    /// [`preferred_node_order`](crate::worker::preferred_node_order). There is
368    /// now only one walk to derive it for: since #52 R4 this dispatcher selects
369    /// for BOTH transports and each chosen worker is served over the one it
370    /// registered on, so "prefer labelled worker, spill to any" has a single
371    /// meaning by construction rather than by two implementations agreeing:
372    ///
373    /// Tier 1..N: for each preferred label (deterministic set order) try a
374    /// NON-WAITING `workers_for(node = Some(label))` and dispatch to the first
375    /// live worker found. Tier N+1 (spill): if no preferred label has a live
376    /// worker, fall back to [`Self::dispatch`] with the activity's own (unpinned)
377    /// node, so the wait-for-worker backstop and round-robin behave exactly as
378    /// today. An empty `preferred` set is the spill case immediately.
379    ///
380    /// # Errors
381    ///
382    /// As [`Self::dispatch`].
383    pub async fn dispatch_preferring(
384        &self,
385        activity: &ScheduledActivity,
386        preferred: &std::collections::BTreeSet<String>,
387    ) -> Result<(), ServerError> {
388        // Reconstruct the shared tier order from the preferred labels so gRPC and
389        // liminal consult ONE prefer-then-spill implementation.
390        let tiers = crate::worker::preferred_node_order(&aion_store::NamespacePlacement::Prefer {
391            nodes: preferred.clone(),
392        });
393        self.dispatch_over_tiers(activity, &tiers).await
394    }
395
396    /// Dispatch `activity` REQUIRING a worker whose advertised node is one of the
397    /// `required` labels, WAITING when none is live and NEVER spilling to a
398    /// node=`None` any-worker dispatch (Control-Plane Phase 2, P2-I1 — the
399    /// `Pinned{L}` hard pin). This is the opposite of [`Self::dispatch_preferring`]:
400    /// a `Prefer` set appends a `None` spill tier; a `Pinned` set has NO `None`
401    /// tier and instead holds on the wait-for-worker backstop until an L-labelled
402    /// worker registers.
403    ///
404    /// Consulted ONLY for an UNPINNED activity (`activity.node == None`): a
405    /// per-activity authored pin always wins and dispatches through
406    /// [`Self::dispatch`] unchanged. The recorded row's `node` is NEVER mutated —
407    /// the required set is a pure dispatch-time worker-selection input in this
408    /// non-replayed path, so replay is untouched (CP-Phase-2 §2.4).
409    ///
410    /// Each retry tries every required label (deterministic [`BTreeSet`] order) via
411    /// a NON-WAITING `workers_for(node = Some(label))` and delivers to the first
412    /// live worker found, preserving the round-robin exactly like
413    /// [`Self::dispatch_to_node`]. When no required label has a live worker across
414    /// the whole set, it awaits the [`WorkerArrival`](crate::worker::registry::WorkerArrival)
415    /// it subscribed to BEFORE walking the set
416    /// and retries — the same isolation-stall a per-activity `Some(N)` pin already
417    /// exhibits. An EMPTY required set can never be satisfied by any labelled
418    /// worker, so it stalls (isolation > availability); the caller sets a non-empty
419    /// `Pinned{L}` for a live pin.
420    ///
421    /// # Errors
422    ///
423    /// As [`Self::dispatch`].
424    pub async fn dispatch_requiring(
425        &self,
426        activity: &ScheduledActivity,
427        required: &std::collections::BTreeSet<String>,
428    ) -> Result<(), ServerError> {
429        let span = info_span!(
430            "activity_dispatch",
431            operation = "activity_dispatch_requiring",
432            namespace = %activity.namespace,
433            task_queue = %activity.task_queue,
434            workflow_id = %activity.workflow_id,
435            activity_id = %activity.activity_id,
436            activity_type = %activity.activity_type,
437            worker_id = tracing::field::Empty,
438        );
439        let span_fields = span.clone();
440        async {
441            loop {
442                // SUBSCRIBE BEFORE YOU LOOK. Taken here, at the top of the
443                // iteration, so every registration and every published verdict
444                // that fires while the required set below is being walked is
445                // retained by the park at the bottom. A subscription taken at
446                // the park instead would fire its `notify_waiters` into an
447                // empty waiter list and store nothing — see
448                // [`WorkerArrival`](crate::worker::registry::WorkerArrival).
449                let arrival = self.registry.worker_arrival();
450                // Whether any required label's pool was BUSY rather than absent.
451                let mut any_pool_busy = false;
452                for label in required {
453                    self.drain_state
454                        .ensure_accepting(&activity.namespace, &activity.activity_type)?;
455                    let candidates = self.registry.workers_for(
456                        &activity.namespace,
457                        &activity.task_queue,
458                        &activity.activity_type,
459                        Some(label.as_str()),
460                    )?;
461                    // An EMPTY list is the ordinary shape of "busy" here, not a
462                    // rare one: `workers_for` already excludes a worker at its
463                    // advertised capacity, so a saturated node produces no
464                    // candidates at all rather than candidates this pass skips.
465                    // The census is what can still tell "busy" from "absent",
466                    // and it is the same source `dispatch_to_node` classifies
467                    // its own miss from.
468                    if candidates.is_empty() {
469                        if self.pool_is_busy(activity, Some(label.as_str()))? {
470                            any_pool_busy = true;
471                        }
472                        continue;
473                    }
474                    match self
475                        .send_to_candidates(activity, candidates, &span_fields)
476                        .await?
477                    {
478                        CandidateOutcome::Delivered => return Ok(()),
479                        // The narrower race: the list was non-empty and every
480                        // candidate filled up between the read and the push.
481                        CandidateOutcome::AllFull => any_pool_busy = true,
482                        CandidateOutcome::NoLiveStream => {}
483                        // Terminal: never parked, never walked further. The
484                        // outbox dead-letters it on this observation and the
485                        // engine seam reports it terminally.
486                        CandidateOutcome::Unservable { reason } => {
487                            return Err(ServerError::worker_dispatch_unservable(
488                                activity.task_queue.clone(),
489                                reason,
490                            ));
491                        }
492                    }
493                }
494                // ONE BEHAVIOUR FOR "POOL BUSY" ON BOTH PATHS. An outbox row does
495                // not park here for the same reason it does not park in
496                // `dispatch_to_node`: it holds a durable claim, and waiting keeps
497                // that claim held while its own attempt budget, backoff and
498                // dead-letter never run. Waiting on a BUSY pool is the worst
499                // version of that — the pool is healthy and will free a slot, so
500                // the row would sit claimed for as long as the fleet is busy
501                // rather than being re-armed attempt-neutrally by the dispatcher
502                // that owns it.
503                //
504                // Absence is different and is left alone: no worker on a required
505                // node is a fleet condition this loop is entitled to wait for,
506                // and the hard-pin invariant below is why.
507                if any_pool_busy && let DispatchOrigin::OutboxRow { .. } = &activity.origin {
508                    return Err(ServerError::worker_busy(
509                        activity.task_queue.clone(),
510                        format!(
511                            "every worker on a required node for activity `{}` in task queue \
512                             `{}` is at its advertised concurrency; the row is re-queued without \
513                             spending an attempt",
514                            activity.activity_type, activity.task_queue,
515                        ),
516                    ));
517                }
518                // No required label had a live worker this pass. WAIT for a worker
519                // to register, then retry the WHOLE required set — never fall back
520                // to a node=None any-worker dispatch (the hard-pin invariant).
521                tracing::info!(
522                    namespace = %activity.namespace,
523                    task_queue = %activity.task_queue,
524                    activity_type = %activity.activity_type,
525                    workflow_id = %activity.workflow_id,
526                    activity_id = %activity.activity_id,
527                    "no worker on a required (Pinned) node; waiting — will NOT spill to any-node"
528                );
529                arrival.await;
530            }
531        }
532        .instrument(span)
533        .await
534        .inspect_err(|error| {
535            log_dispatch_error("activity_dispatch_requiring", activity, error);
536        })
537    }
538
539    /// Whether this address has compatible workers that are all BUSY — at their
540    /// advertised concurrency, or not yet having announced one — rather than
541    /// absent.
542    ///
543    /// Read from the census, which counts REGISTERED compatible workers without
544    /// the eligibility filter, so it can tell an empty pool from a full one.
545    /// Selection cannot: `workers_for` returns the same empty list for both.
546    ///
547    /// # Errors
548    ///
549    /// Returns the registry's own error.
550    fn pool_is_busy(
551        &self,
552        activity: &ScheduledActivity,
553        node: Option<&str>,
554    ) -> Result<bool, ServerError> {
555        let census = self.registry.pool_census(
556            &activity.namespace,
557            &activity.task_queue,
558            &activity.activity_type,
559            node,
560        )?;
561        Ok(census.eligible_compatible_workers == 0
562            && (census.compatible_workers_at_capacity
563                + census.compatible_workers_capacity_unannounced)
564                > 0)
565    }
566
567    /// Dispatch `activity` over an ordered `tiers` sequence of node filters, each
568    /// a `Some(label)` preference or the final `None` spill (the shared
569    /// [`preferred_node_order`](crate::worker::preferred_node_order) output). The
570    /// first non-spill tier with a live worker wins via a NON-WAITING
571    /// `workers_for`; the `None` spill tier falls back to the waiting
572    /// [`Self::dispatch_to_node`] so the wait-for-worker backstop and round-robin
573    /// behave exactly as today.
574    ///
575    /// # Errors
576    ///
577    /// As [`Self::dispatch`].
578    async fn dispatch_over_tiers(
579        &self,
580        activity: &ScheduledActivity,
581        tiers: &[Option<String>],
582    ) -> Result<(), ServerError> {
583        let span = info_span!(
584            "activity_dispatch",
585            operation = "activity_dispatch_preferring",
586            namespace = %activity.namespace,
587            task_queue = %activity.task_queue,
588            workflow_id = %activity.workflow_id,
589            activity_id = %activity.activity_id,
590            activity_type = %activity.activity_type,
591            worker_id = tracing::field::Empty,
592        );
593        let span_fields = span.clone();
594        async {
595            for tier in tiers {
596                let Some(label) = tier else {
597                    // The `None` spill tier: fall back to the waiting unpinned
598                    // dispatch (wait-for-worker backstop + round-robin).
599                    return self
600                        .dispatch_to_node(activity, activity.node.as_deref(), &span_fields)
601                        .await;
602                };
603                self.drain_state
604                    .ensure_accepting(&activity.namespace, &activity.activity_type)?;
605                let candidates = self.registry.workers_for(
606                    &activity.namespace,
607                    &activity.task_queue,
608                    &activity.activity_type,
609                    Some(label.as_str()),
610                )?;
611                if self
612                    .send_to_candidates(activity, candidates, &span_fields)
613                    .await?
614                    .is_delivered()
615                {
616                    return Ok(());
617                }
618            }
619            // An empty tier list (never produced by `preferred_node_order`, which
620            // always appends the spill) still degrades to the unpinned dispatch.
621            self.dispatch_to_node(activity, activity.node.as_deref(), &span_fields)
622                .await
623        }
624        .instrument(span)
625        .await
626        .inspect_err(|error| {
627            log_dispatch_error("activity_dispatch_preferring", activity, error);
628        })
629    }
630
631    /// The waiting dispatch core: select a worker for `node` (waiting for one to
632    /// register when none is live, exactly as before), then push the task.
633    async fn dispatch_to_node(
634        &self,
635        activity: &ScheduledActivity,
636        node: Option<&str>,
637        span_fields: &tracing::Span,
638    ) -> Result<(), ServerError> {
639        // The wait is unbounded, deliberately and unchanged: bounding a dispatch
640        // to an unserved queue is a semantics decision that is the operator's,
641        // and inventing one here would refuse work nobody asked to have refused.
642        // What changes is that the park is now VISIBLE. This loop used to emit
643        // one `info!` and block, so a permanently parked row had no state to
644        // query, `dispatch_parked` read false while it was in fact parked
645        // forever, and — because `dispatch` never returns — the outbox row sat
646        // `claimed` where dead-letter and redrive could not see it either.
647        let address = ServiceAddress {
648            namespace: activity.namespace.clone(),
649            task_queue: activity.task_queue.clone(),
650            activity_type: activity.activity_type.clone(),
651            node: node.map(ToOwned::to_owned),
652        };
653        let wait = ServiceWait {
654            registry: &self.registry,
655            declarations: &self.queue_declarations,
656            config: &self.queue_service_config,
657            state: &self.queue_service_state,
658            address: &address,
659            workflow_id: &activity.workflow_id,
660            activity_id: &activity.activity_id,
661            publisher: self.cluster_publisher.as_ref(),
662        };
663        let policy = self
664            .queue_service_config
665            .policy_for(&activity.namespace, &activity.task_queue);
666        let started_at = std::time::Instant::now();
667        let mut reported: Option<QueueServiceReason> = None;
668        let workers = loop {
669            // SUBSCRIBE BEFORE YOU LOOK, and before the census inside
670            // `observe_selection_miss` too. Everything that fires from here to
671            // the park at the bottom of this iteration — a registration, a
672            // published reachability verdict — is retained by that park. Taking
673            // the subscription at the park instead is the defect: both wake
674            // sources are `Notify::notify_waiters`, which stores no permit, so a
675            // wake that landed during the selection below would have fired into
676            // an empty waiter list. See
677            // [`WorkerArrival`](crate::worker::registry::WorkerArrival).
678            let arrival = self.registry.worker_arrival();
679            self.drain_state
680                .ensure_accepting(&activity.namespace, &activity.activity_type)
681                .inspect_err(|_| clear_selection_miss(&wait))?;
682            let candidates = self
683                .registry
684                .workers_for(
685                    &activity.namespace,
686                    &activity.task_queue,
687                    &activity.activity_type,
688                    node,
689                )
690                .inspect_err(|_| clear_selection_miss(&wait))?;
691            if !candidates.is_empty() {
692                if let Some(reason) = reported {
693                    tracing::info!(
694                        namespace = %activity.namespace,
695                        task_queue = %activity.task_queue,
696                        activity_type = %activity.activity_type,
697                        workflow_id = %activity.workflow_id,
698                        activity_id = %activity.activity_id,
699                        queue_service_reason = reason.as_str(),
700                        "queue service restored; the parked dispatch has a worker"
701                    );
702                }
703                clear_selection_miss(&wait);
704                break candidates;
705            }
706            match observe_selection_miss(&wait, policy, None, started_at.elapsed(), reported) {
707                // The census and selection disagree, and there are two ways that
708                // happens. A worker arrived between the two lock acquisitions —
709                // or every compatible worker is published dispatch-ineligible,
710                // because `pool_census` deliberately counts REGISTERED
711                // node-matched workers with no eligibility filter (#197 R3, so
712                // `classify` can tell an empty pool from an excluded one) while
713                // selection counts eligible ones. Neither has a state worth
714                // announcing.
715                //
716                // The wait below answers both, and the MECHANISM is `arrival`,
717                // not the notification: `arrival` was subscribed at the top of
718                // this iteration, before `workers_for` and before the census
719                // inside `observe_selection_miss`, so the very registration that
720                // opened the first case — which has ALREADY fired by the time
721                // control reaches here — is retained rather than lost, and so is
722                // a verdict published in the same window. Awaiting a freshly
723                // constructed wait here instead would park this dispatch holding
724                // positive census evidence of a live worker, with nothing left to
725                // wake it: on `OutboxTransport::Grpc` no liveness probe runs and
726                // no verdict is ever published, so the only other wake is some
727                // unrelated worker registering elsewhere in the registry.
728                //
729                // Re-selecting at once instead of parking would spin this loop
730                // hot — no park, no sleep, no WARN — for as long as the exclusion
731                // lasts, and a pool of one freshly registered worker is
732                // all-ineligible until it has served its opening probation.
733                Ok(None) => {}
734                Ok(Some(observed)) => reported = Some(observed.reason),
735                Err(refusal) => {
736                    clear_selection_miss(&wait);
737                    return Err(ServerError::worker_dispatch(
738                        activity.namespace.clone(),
739                        activity.activity_type.clone(),
740                        refusal.reason_string(),
741                    ));
742                }
743            }
744            // 🔴 AN OUTBOX ROW DOES NOT PARK HERE. It already has a mechanism
745            // for "nobody can serve this yet" — its own attempt budget, backoff
746            // and dead-letter — and that mechanism only runs if this call
747            // RETURNS. Parking instead holds the row `claimed` forever, spends
748            // no attempts, never dead-letters, and leaves the workflow
749            // reporting `Running` for a fan-out member that will never be
750            // delivered. Two mechanisms for one job, and the silent one wins.
751            //
752            // An ENGINE-seam dispatch is the opposite case and keeps the park:
753            // the run itself is blocked on this call, so there is nothing to
754            // hand back to, and parking visibly is the honest outcome.
755            //
756            // MEASURED, not assumed. `dead_letter_is_genuine_and_loud` runs on
757            // both arms; before this fix the gRPC arm had never dead-lettered
758            // an undeliverable row in any released version, and the liminal arm
759            // stopped when #52 R4 replaced its own dispatcher with this one.
760            if let DispatchOrigin::OutboxRow { .. } = &activity.origin {
761                let refusal = Self::outbox_row_selection_refusal(activity, reported);
762                // THE UNSERVED RECORD SURVIVES A BUSY RETURN, and is withdrawn
763                // only when the queue is actually served again.
764                //
765                // A busy pool re-arms attempt-neutrally, so this return is the
766                // start of a wait, not the end of one — the queue IS
767                // unserved-by-capacity for as long as that stays true, and the
768                // census already knows it by name (`POLLERS_AT_CAPACITY`,
769                // `POLLERS_CAPACITY_UNANNOUNCED`). Clearing it here made
770                // `/queues/unserved` flicker on and off once per re-arm, so the
771                // one place an operator can see WHY their rows are waiting
772                // showed nothing most of the time. The reason is served from the
773                // census rather than written onto the row: no new column, no
774                // second copy of a fact the census already holds.
775                //
776                // Every other refusal still clears, because those are terminal
777                // for this pass: the row goes back to its own attempt budget and
778                // this dispatch is over.
779                if !refusal.is_worker_busy() {
780                    clear_selection_miss(&wait);
781                }
782                return Err(refusal);
783            }
784            arrival.await;
785        };
786        match self
787            .send_to_candidates(activity, workers, span_fields)
788            .await?
789        {
790            CandidateOutcome::Delivered => Ok(()),
791            // Every candidate was BUSY, not gone. Attempt-neutral for the same
792            // reason an empty candidate list is when the pool is at capacity:
793            // this dispatch is queued behind real work, and pricing it as a
794            // delivery failure is how a busy pool dead-letters.
795            CandidateOutcome::AllFull => Err(ServerError::worker_busy(
796                activity.task_queue.clone(),
797                format!(
798                    "every worker serving activity `{}` in task queue `{}` had already taken \
799                     the work it advertised it would run at once",
800                    activity.activity_type, activity.task_queue,
801                ),
802            )),
803            CandidateOutcome::NoLiveStream => Err(ServerError::worker_dispatch(
804                activity.namespace.clone(),
805                activity.activity_type.clone(),
806                format!(
807                    "all matching worker streams in task queue {} closed before task could be \
808                     delivered",
809                    activity.task_queue
810                ),
811            )),
812            // Terminal by class: the outbox dispatcher dead-letters this on its
813            // first observation instead of spending its attempt budget on it.
814            CandidateOutcome::Unservable { reason } => Err(
815                ServerError::worker_dispatch_unservable(activity.task_queue.clone(), reason),
816            ),
817        }
818    }
819
820    /// Hand one task to one already-chosen worker over the transport that worker
821    /// registered on.
822    ///
823    /// The only place transport is decided, and it is decided by the worker
824    /// rather than by a server-wide key — which is the whole of #52 R1.
825    async fn deliver_to(
826        &self,
827        worker: &crate::worker::registry::WorkerHandle,
828        task: &ProtoActivityTask,
829        intent: &SharedDeliveryIntent,
830        accepted: &dyn DeliveryAccepted,
831    ) -> TaskDelivery {
832        match worker.delivery() {
833            WorkerDelivery::Grpc(_) => {
834                GrpcTaskDelivery
835                    .deliver(worker, task, intent, accepted)
836                    .await
837            }
838            #[cfg(feature = "liminal-transport")]
839            WorkerDelivery::Liminal(_) => {
840                let Some(delivery) = self.liminal_delivery.as_ref() else {
841                    // 🔴 The worker is ALIVE and correctly registered; the
842                    // SERVER is missing its wiring. Reporting this as
843                    // unreachable would deregister a healthy worker for a
844                    // configuration fault — the exact shape of the defect this
845                    // change removes. The dispatch fails loudly and the caller's
846                    // retry path runs, which is what a wiring bug deserves.
847                    return TaskDelivery::failed(
848                        "worker is delivered over liminal but this server has no liminal delivery \
849                         wired into its activity dispatcher",
850                    );
851                };
852                delivery.deliver(worker, task, intent, accepted).await
853            }
854        }
855    }
856}
857
858fn log_dispatch_error(operation: &'static str, activity: &ScheduledActivity, error: &ServerError) {
859    let fields = error.trace_fields();
860    tracing::error!(
861        operation,
862        namespace = %activity.namespace,
863        task_queue = %activity.task_queue,
864        node = activity.node.as_deref(),
865        workflow_id = %activity.workflow_id,
866        activity_id = %activity.activity_id,
867        activity_type = %activity.activity_type,
868        error_type = %fields.error_type,
869        store_error_type = fields.store_error_type,
870        reason = %fields.reason,
871        "activity dispatch failed"
872    );
873}
874
875/// Decoded activity outcome reported by a worker.
876#[derive(Clone, Debug, Eq, PartialEq)]
877pub enum ActivityCompletionOutcome {
878    /// Activity completed successfully with an output payload.
879    Succeeded(Payload),
880    /// Activity failed, preserving retryability classification for the engine.
881    Failed(ActivityError),
882    /// The worker was lost BEFORE the activity reported any result — a
883    /// TRANSPORT-domain loss, not an activity failure.
884    ///
885    /// A distinct variant rather than a `Failed` wearing a retryable kind,
886    /// because the two are different failure domains and were being conflated:
887    /// a synthesized `retryable:worker ... lost` was delivered verbatim as a
888    /// TERMINAL failure whenever the activity carried no authored retry policy,
889    /// so every infrastructure death read as a red action. The classification
890    /// and the transport's own re-dispatch budget live in
891    /// [`transport_loss`](crate::worker::transport_loss).
892    WorkerLost {
893        /// The worker that died holding this activity.
894        worker_id: crate::worker::registry::WorkerId,
895    },
896    /// The worker HANDED THE TASK BACK because it had no free execution slot —
897    /// a TRANSPORT-domain non-start, like [`Self::WorkerLost`], and for the
898    /// same reason: the activity never ran.
899    ///
900    /// A distinct variant rather than a `WorkerLost` wearing a different
901    /// message, because nothing is lost and nobody died: the worker is
902    /// connected, healthy, and working, and the operator reading this must not
903    /// be told a worker went away. What the two share is the ENGINE-facing
904    /// consequence — re-dispatch, attempt-neutral, nothing recorded against the
905    /// action's budget — so both are classified through the transport domain.
906    ///
907    /// The server should not be able to produce this: it knows every worker's
908    /// advertised capacity and does not select one that has reached it. It
909    /// exists because the two accountings CAN drift — a reconnect race, a
910    /// redelivery joining an outstanding generation — and a drifted count must
911    /// not become a false failure.
912    Refused {
913        /// The worker that declined the dispatch.
914        worker_id: crate::worker::registry::WorkerId,
915        /// The worker's own words for why it declined.
916        reason: String,
917    },
918}
919
920/// Correlated activity completion handed to the engine-owned activity contract.
921#[derive(Clone, Debug, Eq, PartialEq)]
922pub struct ActivityCompletion {
923    /// Owning workflow id.
924    pub workflow_id: WorkflowId,
925    /// Correlating activity id.
926    pub activity_id: ActivityId,
927    /// Concrete workflow run echoed by the worker, when known.
928    pub run_id: Option<RunId>,
929    /// Opaque execution generation echoed from the dispatched task.
930    pub completion_token: CompletionToken,
931    /// Worker-reported outcome.
932    pub outcome: ActivityCompletionOutcome,
933}
934
935impl TryFrom<ProtoActivityResult> for ActivityCompletion {
936    type Error = ServerError;
937
938    fn try_from(value: ProtoActivityResult) -> Result<Self, Self::Error> {
939        let workflow_id = value
940            .workflow_id
941            .ok_or_else(|| wire_error("activity result workflow id is missing"))
942            .and_then(|id| WorkflowId::try_from(id).map_err(ServerError::from))?;
943        let activity_id = value
944            .activity_id
945            .ok_or_else(|| wire_error("activity result activity id is missing"))
946            .map(ActivityId::from)?;
947        let run_id = value
948            .run_id
949            .ok_or_else(|| wire_error("activity result run id is missing"))
950            .and_then(|id| RunId::try_from(id).map_err(ServerError::from))?;
951        let completion_token =
952            CompletionToken::from_wire(&workflow_id, &activity_id, value.completion_token)?;
953        let outcome = match value.outcome {
954            Some(proto_activity_result::Outcome::Result(payload)) => {
955                ActivityCompletionOutcome::Succeeded(
956                    Payload::try_from(payload).map_err(ServerError::from)?,
957                )
958            }
959            Some(proto_activity_result::Outcome::Error(error)) => {
960                ActivityCompletionOutcome::Failed(
961                    ActivityError::try_from(error).map_err(ServerError::from)?,
962                )
963            }
964            None => return Err(wire_error("activity result outcome is missing")),
965        };
966
967        Ok(Self {
968            workflow_id,
969            activity_id,
970            run_id: Some(run_id),
971            completion_token,
972            outcome,
973        })
974    }
975}
976
977/// Engine-owned activity completion contract used by the worker endpoint.
978pub trait ActivityCompletionSink {
979    /// Feed one worker-reported result into the engine activity contract.
980    ///
981    /// # Errors
982    ///
983    /// Returns [`ServerError`] when the engine rejects or cannot record the completion.
984    fn complete_activity(&self, completion: ActivityCompletion) -> Result<(), ServerError>;
985
986    /// Park one in-flight dispatch for restart recovery during a graceful
987    /// drain (#207): resolve the LOCAL waiter with the ephemeral parked
988    /// sentinel and nothing else.
989    ///
990    /// Parking is the anti-completion — it writes nothing durable, delivers
991    /// nothing to workflow code, and never crosses the SDK wire. It exists so a
992    /// drain leaves the durable log at exactly the dangling
993    /// `ActivityScheduled`/`ActivityStarted` a kill -9 would leave (the proven
994    /// re-dispatchable state) while still unblocking the blocking dispatcher
995    /// thread, so process exit is never wedged on tokio's blocking pool. A
996    /// dispatch with no matching waiter (already resolved) is a no-op — a park
997    /// must never be routed as an outbox failure delivery.
998    ///
999    /// # Errors
1000    ///
1001    /// Returns [`ServerError`] when sink state cannot be trusted.
1002    fn park_activity(
1003        &self,
1004        workflow_id: &WorkflowId,
1005        activity_id: &ActivityId,
1006    ) -> Result<(), ServerError>;
1007}
1008
1009/// Decode and hand a worker result to the engine-owned activity completion sink.
1010///
1011/// # Errors
1012///
1013/// Returns [`ServerError`] for malformed wire results or sink failures.
1014pub fn handle_activity_result(
1015    sink: &impl ActivityCompletionSink,
1016    result: ProtoActivityResult,
1017) -> Result<(), ServerError> {
1018    sink.complete_activity(ActivityCompletion::try_from(result)?)
1019}
1020
1021fn wire_error(message: &'static str) -> ServerError {
1022    ServerError::Wire {
1023        wire: WireError::backend(message),
1024    }
1025}
1026
1027/// The refusal an outbox row receives when nothing can serve it yet.
1028///
1029/// Carries the queue-service classification when one was reached, so the
1030/// outbox's own retry log and the eventual dead letter say WHY rather than
1031/// only that a dispatch failed. `None` is the census/selection disagreement —
1032/// a worker arriving between two lock acquisitions, or a pool whose workers are
1033/// all serving their opening probation — which is transient by construction and
1034/// is exactly what the outbox's backoff is for.
1035fn unservable_outbox_row_reason(reported: Option<QueueServiceReason>, task_queue: &str) -> String {
1036    match reported {
1037        Some(reason) => format!(
1038            "no worker can currently serve task queue {task_queue} ({}); the row is returned to \
1039             the outbox so its retry, backoff and dead-letter apply",
1040            reason.as_str()
1041        ),
1042        None => format!(
1043            "no worker is currently eligible for task queue {task_queue}; the row is returned to \
1044             the outbox so its retry, backoff and dead-letter apply"
1045        ),
1046    }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use std::sync::Mutex;
1052
1053    // Production code here no longer pushes a WorkerMessage itself — the
1054    // delivery seam owns the push — but these tests still build one to drive a
1055    // fake worker stream.
1056    use crate::worker::registry::WorkerMessage;
1057
1058    use aion_core::{ActivityErrorKind, ContentType};
1059    use aion_proto::{ProtoActivityError, ProtoActivityErrorKind};
1060    use serde_json::json;
1061    use uuid::Uuid;
1062
1063    use crate::worker::queue_service::declarations::{QueueDeclaration, QueueDeclarations};
1064    use crate::worker::registry::{ConnectedWorkerRegistry, WorkerRegistration};
1065
1066    use super::*;
1067
1068    fn workflow_id() -> WorkflowId {
1069        WorkflowId::new(Uuid::nil())
1070    }
1071
1072    fn activity_id() -> ActivityId {
1073        ActivityId::from_sequence_position(42)
1074    }
1075
1076    fn payload(value: &serde_json::Value) -> Result<Payload, Box<dyn std::error::Error>> {
1077        Ok(Payload::from_json(value)?)
1078    }
1079
1080    #[tokio::test]
1081    async fn dispatch_pushes_activity_task_with_correlation()
1082    -> Result<(), Box<dyn std::error::Error>> {
1083        let registry = ConnectedWorkerRegistry::default();
1084        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1085        let activity_types = [String::from("charge-card")];
1086        let registration = registry.register(
1087            "tenant-a",
1088            activity_types.iter(),
1089            tx,
1090            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1091        )?;
1092        let dispatcher = ActivityDispatcher::new(registry.clone());
1093        let input = payload(&json!({"amount": 1200}))?;
1094        let scheduled = ScheduledActivity {
1095            namespace: String::from("tenant-a"),
1096            task_queue: String::from("default"),
1097            activity_type: String::from("charge-card"),
1098            node: None,
1099            workflow_id: workflow_id(),
1100            activity_id: activity_id(),
1101            run_id: Some(RunId::new_v4()),
1102            input: input.clone(),
1103            attempt: 1,
1104            labels: std::collections::BTreeMap::new(),
1105            origin: DispatchOrigin::Engine,
1106        };
1107
1108        dispatcher.dispatch(&scheduled).await?;
1109        let message = rx.recv().await.ok_or("expected pushed activity task")?;
1110        let WorkerMessage::ActivityTask(task) = message else {
1111            return Err("expected activity task message".into());
1112        };
1113
1114        assert_eq!(task.workflow_id, Some(ProtoWorkflowId::from(workflow_id())));
1115        assert_eq!(task.activity_id, Some(ProtoActivityId::from(activity_id())));
1116        assert_eq!(task.activity_type, "charge-card");
1117        assert_eq!(task.input, Some(ProtoPayload::from(input)));
1118        assert_eq!(task.attempt, 1, "wire task must carry the stamped attempt");
1119
1120        registration.deregister()?;
1121        Ok(())
1122    }
1123
1124    #[tokio::test]
1125    async fn dispatch_waits_for_worker_then_delivers() -> Result<(), Box<dyn std::error::Error>> {
1126        let registry = ConnectedWorkerRegistry::default();
1127        let dispatcher = ActivityDispatcher::new(registry.clone());
1128        let scheduled = ScheduledActivity {
1129            namespace: String::from("tenant-a"),
1130            task_queue: String::from("default"),
1131            activity_type: String::from("charge-card"),
1132            node: None,
1133            workflow_id: workflow_id(),
1134            activity_id: activity_id(),
1135            run_id: Some(RunId::new_v4()),
1136            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1137            attempt: 1,
1138            labels: std::collections::BTreeMap::new(),
1139            origin: DispatchOrigin::Engine,
1140        };
1141
1142        let dispatch_handle = tokio::spawn({
1143            let dispatcher = dispatcher.clone();
1144            let scheduled = scheduled.clone();
1145            async move { dispatcher.dispatch(&scheduled).await }
1146        });
1147
1148        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1149        assert!(!dispatch_handle.is_finished(), "dispatch should be waiting");
1150
1151        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1152        let activity_types = [String::from("charge-card")];
1153        let _registration = registry.register(
1154            "tenant-a",
1155            activity_types.iter(),
1156            tx,
1157            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1158        )?;
1159
1160        dispatch_handle.await??;
1161        assert!(rx.recv().await.is_some());
1162        Ok(())
1163    }
1164
1165    /// T9's seam: a real [`QueueDeclarations`] reader that places ONE worker
1166    /// registration at the moment it is consulted.
1167    ///
1168    /// This is public production API used for its purpose, not a test hook:
1169    /// `QueueDeclarationSource::install` is the same seam the boot path,
1170    /// `run.rs` and the NIF bridge each install their own reader through, and
1171    /// `declaration_for` is the trait's one synchronous method. No
1172    /// `#[cfg(test)]` hook exists anywhere in the production path this drives.
1173    ///
1174    /// Why it opens the window exactly: `observe_selection_miss` takes the
1175    /// `pool_census` snapshot FIRST and asks the declaration reader SECOND, so
1176    /// a registration placed here lands after the census that will be
1177    /// classified with it and before the park at the bottom of the loop. That
1178    /// is the interleaving the flight-1 judge could not force — a worker
1179    /// arriving between a dispatch's registry read and its park — reproduced
1180    /// deterministically, with the registration's real `notify_waiters` firing
1181    /// at its real site.
1182    struct RegisterInsideTheSelectionWindow {
1183        registry: ConnectedWorkerRegistry,
1184        activity_types: Vec<String>,
1185        delivery: tokio::sync::mpsc::Sender<WorkerMessage>,
1186        /// The single registration this reader places, kept alive here because
1187        /// dropping a `WorkerRegistration` deregisters the worker. Read by the
1188        /// test afterwards, so a registration that FAILED can never be mistaken
1189        /// for a wake that was lost.
1190        placed: std::sync::OnceLock<Result<WorkerRegistration, ServerError>>,
1191    }
1192
1193    impl QueueDeclarations for RegisterInsideTheSelectionWindow {
1194        fn declaration_for(&self, _task_queue: &str) -> QueueDeclaration {
1195            // Exactly one registration, however many iterations consult this
1196            // reader: `get_or_init` runs its closure once for the cell's life.
1197            // A second registration would give the loop a second wake and the
1198            // test would stop proving anything about the first.
1199            let placed = self.placed.get_or_init(|| {
1200                self.registry.register(
1201                    "tenant-a",
1202                    self.activity_types.iter(),
1203                    self.delivery.clone(),
1204                    crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1205                )
1206            });
1207            if let Err(error) = placed {
1208                tracing::error!(%error, "T9 seam could not place its worker in the window");
1209            }
1210            // Never `NotDeclared`: that refuses structurally before the park is
1211            // ever reached and would prove nothing about the wake.
1212            QueueDeclaration::Declared
1213        }
1214    }
1215
1216    /// T9 — a registration that lands after the loop's census snapshot and
1217    /// before its park is delivered WITHOUT any second event.
1218    ///
1219    /// This is the flight-1 judge's finding driven through the real production
1220    /// loop. The judge could describe the interleaving but not force it: it
1221    /// needs a registration inside the window between `dispatch_to_node`'s
1222    /// census and its park. The [`RegisterInsideTheSelectionWindow`] reader
1223    /// above forces it exactly, through public production API.
1224    ///
1225    /// What each tree does:
1226    ///
1227    /// - **Base** — the park constructs its wait AFTER the registration's
1228    ///   `notify_waiters` has already fired into an empty waiter list. Nothing
1229    ///   else registers, no reachability verdict is published (this dispatcher
1230    ///   has no liveness probe, exactly as `OutboxTransport::Grpc` has none),
1231    ///   and no second event of any kind exists. The dispatch stays `Pending`
1232    ///   forever, holding positive census evidence of a live worker.
1233    /// - **Fixed** — the subscription taken at the top of that same iteration
1234    ///   retains the wake, the park returns at once, the loop re-selects,
1235    ///   `workers_for` finds the worker, and the task is delivered.
1236    ///
1237    /// Polled by hand with a no-op waker, so the base's failure is an ASSERTION
1238    /// on `Poll::Pending` rather than a hang under a clock: one poll drives the
1239    /// whole loop body synchronously through selection, the census, the seam's
1240    /// registration and the park, and — on the fixed tree — straight on through
1241    /// the second iteration's `send_to_candidates`, whose `mpsc` send takes a
1242    /// permit that is free. No runtime, no timeout, no sleep anywhere in this
1243    /// test.
1244    ///
1245    /// The names it touches — `ActivityDispatcher::new`, `with_queue_service`,
1246    /// `QueueDeclarationSource::install`, `dispatch_to_node`, `register` — all
1247    /// exist unchanged at the base, so this test compiles on both trees and its
1248    /// red survives full reversal of the production hunks.
1249    #[test]
1250    fn a_registration_inside_the_selection_window_is_delivered_without_a_second_event()
1251    -> Result<(), Box<dyn std::error::Error>> {
1252        use std::future::Future;
1253        use std::task::{Context, Poll, Waker};
1254
1255        let registry = ConnectedWorkerRegistry::default();
1256        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1257        let seam = std::sync::Arc::new(RegisterInsideTheSelectionWindow {
1258            registry: registry.clone(),
1259            activity_types: vec![String::from("charge-card")],
1260            delivery: tx,
1261            placed: std::sync::OnceLock::new(),
1262        });
1263        let declarations = QueueDeclarationSource::default();
1264        declarations.install(seam.clone());
1265        let dispatcher = ActivityDispatcher::new(registry.clone()).with_queue_service(
1266            declarations,
1267            QueueServiceState::default(),
1268            QueueServiceConfig::default(),
1269        );
1270        let scheduled = ScheduledActivity {
1271            namespace: String::from("tenant-a"),
1272            task_queue: String::from("default"),
1273            activity_type: String::from("charge-card"),
1274            node: None,
1275            workflow_id: workflow_id(),
1276            activity_id: activity_id(),
1277            run_id: Some(RunId::new_v4()),
1278            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1279            attempt: 1,
1280            labels: std::collections::BTreeMap::new(),
1281            origin: DispatchOrigin::Engine,
1282        };
1283
1284        // The pool is empty before the dispatch: the delivery below cannot be
1285        // explained by a worker that was already there when the loop looked.
1286        assert!(
1287            registry
1288                .workers_for("tenant-a", "default", "charge-card", None)?
1289                .is_empty(),
1290            "the window is only a window if selection misses on the first pass"
1291        );
1292
1293        let span = tracing::Span::none();
1294        let mut dispatch = std::pin::pin!(dispatcher.dispatch_to_node(&scheduled, None, &span));
1295        let mut context = Context::from_waker(Waker::noop());
1296        let polled = dispatch.as_mut().poll(&mut context);
1297
1298        // Read the seam's own record BEFORE judging the poll, so a registration
1299        // that failed outright is reported as itself rather than as a lost wake.
1300        match seam.placed.get() {
1301            Some(Ok(_)) => {}
1302            Some(Err(error)) => {
1303                return Err(format!("the seam's registration failed: {error}").into());
1304            }
1305            None => {
1306                return Err(
1307                    "the seam was never consulted: the loop did not reach the census, \
1308                                so this test proved nothing about the park"
1309                        .into(),
1310                );
1311            }
1312        }
1313
1314        assert!(
1315            matches!(polled, Poll::Ready(Ok(()))),
1316            "a registration that landed between the census and the park must be RETAINED: the \
1317             loop holds a subscription taken before it looked, so it re-selects and delivers \
1318             without any second event. Pending here is the finding — a dispatch parked past its \
1319             own wake, with no probe, no verdict and no other registration left to free it."
1320        );
1321
1322        let message = rx.try_recv()?;
1323        let WorkerMessage::ActivityTask(task) = message else {
1324            return Err("expected the activity task to reach the window's worker".into());
1325        };
1326        assert_eq!(task.activity_type, "charge-card");
1327        Ok(())
1328    }
1329
1330    /// The eligible-candidate derivation made `workers_for` eligibility-filtered,
1331    /// which means this loop can now see an EMPTY candidate list while
1332    /// `pool_census` still reads the address as served: the census counts
1333    /// REGISTERED node-matched workers with no eligibility filter (#197 R3), so
1334    /// an all-ineligible pool produces exactly that disagreement and `classify`
1335    /// returns `None`. Treating that as the registration race it used to be —
1336    /// re-selecting at once — would spin this loop hot: no park, no sleep, no
1337    /// WARN, for as long as the exclusion lasts. A pool of one freshly registered
1338    /// worker is all-ineligible until it has served its opening probation, so
1339    /// this is routine rather than exotic.
1340    ///
1341    /// The loop parks instead, and the park wakes on a published reachability
1342    /// verdict as well as on a registration — the excluded worker is ALREADY
1343    /// registered, so a park that only woke on registrations would sleep through
1344    /// its recovery. No worker registers anywhere in this test; the verdict is
1345    /// the only thing that changes.
1346    ///
1347    /// A hot spin cannot pass this: the dispatch runs on this test's own
1348    /// current-thread runtime, so a loop that never awaits would never yield and
1349    /// the restoring publication below would never be scheduled at all.
1350    #[tokio::test]
1351    async fn a_dispatch_to_an_all_ineligible_pool_parks_until_eligibility_returns()
1352    -> Result<(), Box<dyn std::error::Error>> {
1353        let registry = ConnectedWorkerRegistry::default();
1354        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1355        let activity_types = [String::from("charge-card")];
1356        let registration = registry.register(
1357            "tenant-a",
1358            activity_types.iter(),
1359            tx,
1360            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1361        )?;
1362        let worker_id = registration
1363            .worker_id()
1364            .ok_or("registration assigned no worker id")?;
1365        // The verdict a probe round publishes for a worker still serving its
1366        // opening probation: registered, alive, and not yet dispatch-eligible.
1367        // The CAUSE is the point of this fixture — an opening probation clears
1368        // itself, which is why parking silently through it is correct and why
1369        // this test asserts a park rather than a published reason. Its sibling
1370        // below covers the exclusion that does NOT clear.
1371        registry.set_dispatch_ineligible(
1372            [(
1373                worker_id,
1374                crate::worker::heartbeat::DispatchExclusion::OpeningProbation { answers: 0 },
1375            )]
1376            .into_iter()
1377            .collect(),
1378        )?;
1379
1380        let dispatcher = ActivityDispatcher::new(registry.clone());
1381        let scheduled = ScheduledActivity {
1382            namespace: String::from("tenant-a"),
1383            task_queue: String::from("default"),
1384            activity_type: String::from("charge-card"),
1385            node: None,
1386            workflow_id: workflow_id(),
1387            activity_id: activity_id(),
1388            run_id: Some(RunId::new_v4()),
1389            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1390            attempt: 1,
1391            labels: std::collections::BTreeMap::new(),
1392            origin: DispatchOrigin::Engine,
1393        };
1394        let dispatch_handle = tokio::spawn({
1395            let dispatcher = dispatcher.clone();
1396            let scheduled = scheduled.clone();
1397            async move { dispatcher.dispatch(&scheduled).await }
1398        });
1399
1400        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1401        assert!(
1402            !dispatch_handle.is_finished(),
1403            "a worker the server cannot reach must not take the dispatch"
1404        );
1405
1406        // The probation is served: the next round republishes an empty exclusion
1407        // set. Nothing registers.
1408        registry.set_dispatch_ineligible(std::collections::BTreeMap::new())?;
1409
1410        dispatch_handle.await??;
1411        assert!(
1412            rx.recv().await.is_some(),
1413            "the parked dispatch delivers as soon as the pool has an eligible worker"
1414        );
1415
1416        registration.deregister()?;
1417        Ok(())
1418    }
1419
1420    /// 🔴 An OUTBOX ROW is REFUSED when nothing can serve it, never parked.
1421    ///
1422    /// # What breaks without this
1423    ///
1424    /// The row carries its own lifecycle — attempt budget, backoff, dead-letter
1425    /// — and every part of it runs only if this call RETURNS. A parked dispatch
1426    /// holds the row `claimed` indefinitely, spends no attempts, never dead
1427    /// letters, and leaves the workflow reporting `Running` for a fan-out member
1428    /// that will never arrive. Two mechanisms for one job, and the silent one
1429    /// wins.
1430    ///
1431    /// # Why this is not a style preference
1432    ///
1433    /// Measured on both transport arms, before and after #52 R4, by
1434    /// `dead_letter_is_genuine_and_loud`: a gRPC server had NEVER dead-lettered
1435    /// an undeliverable fan-out row in any released version, and the liminal arm
1436    /// — which did, through a dispatcher of its own — stopped when R4 replaced
1437    /// that dispatcher with this one. This test is the unit-level twin of that
1438    /// pin, and it is paired with the engine-origin park test below: the two
1439    /// differ ONLY in the declared origin, which is the whole claim.
1440    #[tokio::test]
1441    async fn an_outbox_row_is_refused_rather_than_parked_so_dead_letter_stays_reachable()
1442    -> Result<(), Box<dyn std::error::Error>> {
1443        let registry = ConnectedWorkerRegistry::default();
1444        let dispatcher = ActivityDispatcher::new(registry);
1445        let mut scheduled = scheduled_unpinned();
1446        scheduled.origin = DispatchOrigin::OutboxRow {
1447            dispatch_key: String::from("row-key"),
1448        };
1449
1450        // No worker is registered at all, so the engine-origin twin of this
1451        // dispatch would park here forever.
1452        let refused = tokio::time::timeout(
1453            std::time::Duration::from_secs(5),
1454            dispatcher.dispatch(&scheduled),
1455        )
1456        .await
1457        .map_err(|_| {
1458            "an outbox row must be REFUSED, not parked: it is still parked five seconds later, \
1459             which is the shape that holds the row claimed and never dead-letters"
1460        })?;
1461
1462        let Err(error) = refused else {
1463            return Err("a dispatch with no worker must not report success".into());
1464        };
1465        let message = error.to_string();
1466        assert!(
1467            message.contains("returned to the outbox"),
1468            "the refusal must say the row goes back to the machinery that owns its lifecycle, so \
1469             an operator reading a dead letter can tell this from a delivery failure; got: \
1470             {message}"
1471        );
1472        Ok(())
1473    }
1474
1475    /// 🔴 ITEM B's PROOF AT THE DISPATCH LEVEL: the exclusion CAUSE decides
1476    /// whether a park says anything.
1477    ///
1478    /// Two dispatches set up identically — one worker, registered, serving the
1479    /// activity, excluded from dispatch — differing ONLY in why it is excluded.
1480    /// The probation case must park in silence, because it clears itself within
1481    /// seconds and announcing it would fire on every healthy connect. The
1482    /// reachability case must park with a published reason, because it does NOT
1483    /// clear and a row waiting on it waits forever.
1484    ///
1485    /// # What this caught
1486    ///
1487    /// The published verdict used to be a flat `BTreeSet<WorkerId>`, so the
1488    /// registry could not tell the two apart, and the census counted compatible
1489    /// workers without an eligibility filter — so an all-excluded pool
1490    /// classified as SERVED, `classify` returned `None`, and the dispatch
1491    /// parked with nothing published at all. `DispatchExclusion` already
1492    /// carried the distinction and the prober already had the value; it was
1493    /// discarded one line before it became useful.
1494    ///
1495    /// Asserting either case alone would prove nothing — each passes on a
1496    /// constant. The pair is the test.
1497    #[tokio::test]
1498    async fn a_park_says_why_only_when_the_exclusion_does_not_clear_itself()
1499    -> Result<(), Box<dyn std::error::Error>> {
1500        async fn park_reason_for(
1501            exclusion: crate::worker::heartbeat::DispatchExclusion,
1502        ) -> Result<Option<QueueServiceReason>, Box<dyn std::error::Error>> {
1503            let registry = ConnectedWorkerRegistry::default();
1504            let state = QueueServiceState::default();
1505            let dispatcher = ActivityDispatcher::new(registry.clone()).with_queue_service(
1506                QueueDeclarationSource::default(),
1507                state.clone(),
1508                QueueServiceConfig::default(),
1509            );
1510            let activity_types = [String::from("charge-card")];
1511            let (tx, _rx) = tokio::sync::mpsc::channel(1);
1512            let registration = registry.register(
1513                "tenant-a",
1514                activity_types.iter(),
1515                tx,
1516                crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1517            )?;
1518            let worker_id = registration
1519                .worker_id()
1520                .ok_or("registration assigned no worker id")?;
1521            // The ONLY difference between the two runs of this body.
1522            registry.set_dispatch_ineligible([(worker_id, exclusion)].into_iter().collect())?;
1523
1524            let scheduled = ScheduledActivity {
1525                namespace: String::from("tenant-a"),
1526                task_queue: String::from("default"),
1527                activity_type: String::from("charge-card"),
1528                node: None,
1529                workflow_id: workflow_id(),
1530                activity_id: activity_id(),
1531                run_id: Some(RunId::new_v4()),
1532                input: Payload::new(ContentType::Json, b"{}".to_vec()),
1533                attempt: 1,
1534                labels: std::collections::BTreeMap::new(),
1535                origin: DispatchOrigin::Engine,
1536            };
1537            assert!(
1538                state.unserved()?.is_empty(),
1539                "precondition: nothing is published before the dispatch, so a reason found \
1540                 below was published BY it"
1541            );
1542
1543            let handle = tokio::spawn(async move { dispatcher.dispatch(&scheduled).await });
1544            // Give the dispatch time to reach its park and publish. The
1545            // dispatch parks either way — what is under test is whether it says
1546            // anything while parked, not whether it proceeds — so this waits
1547            // rather than racing the publication.
1548            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1549            assert!(
1550                !handle.is_finished(),
1551                "a pool with no dispatchable worker must not resolve the dispatch"
1552            );
1553
1554            let unserved = state.unserved()?;
1555            let reason = unserved.first().map(|entry| entry.reason);
1556            handle.abort();
1557            Ok(reason)
1558        }
1559
1560        let probation = park_reason_for(
1561            crate::worker::heartbeat::DispatchExclusion::OpeningProbation { answers: 1 },
1562        )
1563        .await?;
1564        let unreachable =
1565            park_reason_for(crate::worker::heartbeat::DispatchExclusion::ReachabilityLost).await?;
1566
1567        assert_eq!(
1568            probation, None,
1569            "an opening probation clears itself in seconds; publishing it would put every \
1570             healthy worker's first moments on the unserved list"
1571        );
1572        assert_eq!(
1573            unreachable,
1574            Some(QueueServiceReason::PollersUnreachable),
1575            "a pool that has LOST reachability does not recover on its own, so a dispatch \
1576             parked on it must be queryable with a reason rather than waiting in silence"
1577        );
1578        assert_ne!(
1579            probation, unreachable,
1580            "🔴 the cause must be what decides; identical pools differing only in the exclusion \
1581             cause must not produce the same published state"
1582        );
1583        Ok(())
1584    }
1585
1586    /// The park on this leg must be VISIBLE — queryable, not merely logged.
1587    ///
1588    /// This loop predates the queue-service taxonomy and never adopted it, so a
1589    /// dispatch parked here published no state at all: `GET /queues/unserved`
1590    /// and `describe`'s `unserved` list both read empty while a row sat parked
1591    /// forever, and because `dispatch` never returns, the outbox row stayed
1592    /// `claimed` where dead-letter and redrive could not see it either. Three
1593    /// surfaces, all reading "nothing to see".
1594    ///
1595    /// The wait is deliberately still unbounded FOR AN ENGINE-SEAM DISPATCH,
1596    /// which this is: the run itself is blocked on the call, so there is nobody
1597    /// to hand the work back to and bounding it is the operator's decision
1598    /// rather than this function's. An OUTBOX ROW is the opposite case and no
1599    /// longer reaches this park at all — see
1600    /// `an_outbox_row_is_refused_rather_than_parked_so_dead_letter_stays_reachable`.
1601    #[tokio::test]
1602    async fn a_dispatch_with_no_worker_publishes_its_park_and_clears_on_arrival()
1603    -> Result<(), Box<dyn std::error::Error>> {
1604        let registry = ConnectedWorkerRegistry::default();
1605        let state = QueueServiceState::default();
1606        let dispatcher = ActivityDispatcher::new(registry.clone()).with_queue_service(
1607            QueueDeclarationSource::default(),
1608            state.clone(),
1609            QueueServiceConfig::default(),
1610        );
1611        let scheduled = ScheduledActivity {
1612            namespace: String::from("tenant-a"),
1613            task_queue: String::from("default"),
1614            activity_type: String::from("charge-card"),
1615            node: None,
1616            workflow_id: workflow_id(),
1617            activity_id: activity_id(),
1618            run_id: Some(RunId::new_v4()),
1619            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1620            attempt: 1,
1621            labels: std::collections::BTreeMap::new(),
1622            origin: DispatchOrigin::Engine,
1623        };
1624
1625        // Nothing is parked before the dispatch: the assertion below would pass
1626        // vacuously against a state that reported everything as unserved.
1627        assert!(
1628            state.unserved()?.is_empty(),
1629            "no dispatch has been made yet"
1630        );
1631
1632        // CONTROL ARM, built for this promotion. A dispatcher that does NOT
1633        // share the queue-service seams behaves exactly as this loop did before
1634        // the change: it parks, and the shared state learns nothing. Running it
1635        // first proves the assertion below detects the ABSENCE of publishing
1636        // rather than passing on any state at all.
1637        let unwired = ActivityDispatcher::new(registry.clone());
1638        let unwired_handle = tokio::spawn({
1639            let scheduled = scheduled.clone();
1640            async move { unwired.dispatch(&scheduled).await }
1641        });
1642        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1643        assert!(
1644            state.unserved()?.is_empty(),
1645            "an unshared dispatcher must publish nothing HERE — that is the \
1646             defect this test exists to catch, reproduced on purpose"
1647        );
1648        unwired_handle.abort();
1649
1650        let dispatch_handle = tokio::spawn({
1651            let dispatcher = dispatcher.clone();
1652            let scheduled = scheduled.clone();
1653            async move { dispatcher.dispatch(&scheduled).await }
1654        });
1655        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1656        assert!(!dispatch_handle.is_finished(), "dispatch should be waiting");
1657
1658        let unserved = state.unserved()?;
1659        assert_eq!(
1660            unserved.len(),
1661            1,
1662            "the parked dispatch must be queryable, not just logged: {unserved:?}"
1663        );
1664        assert_eq!(unserved[0].key.task_queue, "default");
1665        assert_eq!(
1666            unserved[0].reason,
1667            QueueServiceReason::NoLivePollers,
1668            "an empty pool must be classified, not reported as a bare miss"
1669        );
1670        assert_eq!(
1671            state.parked_on_queue("default")?,
1672            1,
1673            "the run parked on the queue must be attributable to the queue"
1674        );
1675
1676        // A worker arrives: the dispatch completes AND the state clears, so an
1677        // operator is not left reading a park that has already resolved.
1678        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1679        let activity_types = [String::from("charge-card")];
1680        let _registration = registry.register(
1681            "tenant-a",
1682            activity_types.iter(),
1683            tx,
1684            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1685        )?;
1686
1687        dispatch_handle.await??;
1688        assert!(rx.recv().await.is_some(), "the task must be delivered");
1689        assert!(
1690            state.unserved()?.is_empty(),
1691            "a served dispatch must not be left published as unserved: {:?}",
1692            state.unserved()?
1693        );
1694        Ok(())
1695    }
1696
1697    #[tokio::test]
1698    async fn dispatch_skips_closed_worker_and_uses_next_match()
1699    -> Result<(), Box<dyn std::error::Error>> {
1700        let registry = ConnectedWorkerRegistry::default();
1701        let (closed_tx, closed_rx) = tokio::sync::mpsc::channel(1);
1702        let (live_tx, mut live_rx) = tokio::sync::mpsc::channel(1);
1703        let activity_types = [String::from("charge-card")];
1704        let closed_registration = registry.register(
1705            "tenant-a",
1706            activity_types.iter(),
1707            closed_tx,
1708            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1709        )?;
1710        let live_registration = registry.register(
1711            "tenant-a",
1712            activity_types.iter(),
1713            live_tx,
1714            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
1715        )?;
1716        drop(closed_rx);
1717
1718        let dispatcher = ActivityDispatcher::new(registry.clone());
1719        let scheduled = ScheduledActivity {
1720            namespace: String::from("tenant-a"),
1721            task_queue: String::from("default"),
1722            activity_type: String::from("charge-card"),
1723            node: None,
1724            workflow_id: workflow_id(),
1725            activity_id: activity_id(),
1726            run_id: Some(RunId::new_v4()),
1727            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1728            attempt: 1,
1729            labels: std::collections::BTreeMap::new(),
1730            origin: DispatchOrigin::Engine,
1731        };
1732
1733        dispatcher.dispatch(&scheduled).await?;
1734
1735        assert!(live_rx.recv().await.is_some());
1736        assert_eq!(
1737            registry
1738                .workers_for("tenant-a", "default", "charge-card", None)?
1739                .len(),
1740            1
1741        );
1742
1743        closed_registration.deregister()?;
1744        live_registration.deregister()?;
1745        Ok(())
1746    }
1747
1748    /// The unservable class SURVIVES the delivery boundary and ENDS the walk.
1749    ///
1750    /// A frame liminal proved larger than the connection's
1751    /// whole outbound buffer came back from the liminal arm as a plain
1752    /// `DeliveryFailed` string, the candidate walk read it as "this worker did
1753    /// not place it, try the next", the pass resolved as the retryable dispatch
1754    /// class, and the outbox re-pushed it once per attempt in its budget —
1755    /// recording one lease per push for a step that could never run. Every
1756    /// connection this server holds is built by the one supervisor with the
1757    /// one bound, so a second candidate refuses identically.
1758    ///
1759    /// The pin: with TWO eligible liminal workers, an unservable delivery is
1760    /// returned as [`ServerError::WorkerDispatchUnservable`] (the class the
1761    /// outbox dead-letters on first observation), the door was asked exactly
1762    /// ONCE, and both registrations stand — an unservable frame is not evidence
1763    /// that any worker is gone.
1764    #[cfg(feature = "liminal-transport")]
1765    #[tokio::test]
1766    async fn an_unservable_delivery_ends_the_walk_at_the_first_candidate_and_keeps_every_worker()
1767    -> Result<(), Box<dyn std::error::Error>> {
1768        use std::sync::Arc;
1769        use std::sync::atomic::{AtomicUsize, Ordering};
1770
1771        use crate::worker::delivery_intent::SharedDeliveryIntent;
1772        use crate::worker::registry::WorkerHandle;
1773        use crate::worker::task_delivery::{DeliveryAccepted, TaskDelivery, WorkerTaskDelivery};
1774
1775        const REFUSAL: &str = "the dispatch frame is 5089012 bytes and this worker connection's \
1776                               whole outbound buffer is 1048576 bytes";
1777
1778        /// A liminal door that refuses every frame as unservable and counts
1779        /// how many times it was asked.
1780        struct RefusingDoor {
1781            pushes: AtomicUsize,
1782        }
1783
1784        #[async_trait::async_trait]
1785        impl WorkerTaskDelivery for RefusingDoor {
1786            async fn deliver(
1787                &self,
1788                _worker: &WorkerHandle,
1789                _task: &ProtoActivityTask,
1790                _intent: &SharedDeliveryIntent,
1791                _accepted: &dyn DeliveryAccepted,
1792            ) -> TaskDelivery {
1793                self.pushes.fetch_add(1, Ordering::SeqCst);
1794                TaskDelivery::unservable(REFUSAL)
1795            }
1796        }
1797
1798        let registry = ConnectedWorkerRegistry::default();
1799        let delivery_gate = crate::worker::outbox_dispatcher::DeliveryGate::default();
1800        let door = Arc::new(RefusingDoor {
1801            pushes: AtomicUsize::new(0),
1802        });
1803        let door_as_delivery: Arc<dyn WorkerTaskDelivery> = door.clone();
1804        let dispatcher = ActivityDispatcher::new(registry.clone())
1805            .with_delivery_gate(delivery_gate.clone())
1806            .with_liminal_delivery(door_as_delivery);
1807
1808        let (worker_ids, registrations) = register_two_liminal_workers(&registry)?;
1809        assert_eq!(
1810            registry
1811                .workers_for("tenant-a", "default", "charge-card", None)?
1812                .len(),
1813            2,
1814            "both liminal workers must be eligible candidates before the push"
1815        );
1816
1817        let mut scheduled = scheduled_unpinned();
1818        let dispatch_key = String::from("wf:0");
1819        scheduled.origin = DispatchOrigin::OutboxRow {
1820            dispatch_key: dispatch_key.clone(),
1821        };
1822        let _claim = delivery_gate
1823            .begin(&dispatch_key)
1824            .ok_or("the row's claim must be acquirable")?;
1825
1826        let refusal = dispatcher
1827            .dispatch(&scheduled)
1828            .await
1829            .err()
1830            .ok_or("an unservable frame must not be reported as delivered")?;
1831        assert!(
1832            refusal.is_worker_dispatch_unservable(),
1833            "an unservable delivery must resolve as the UNSERVABLE class, which the outbox \
1834             dead-letters on first observation; got {refusal}"
1835        );
1836        assert!(
1837            !refusal.is_worker_busy() && !refusal.is_worker_connection_lost(),
1838            "unservable is neither busy nor lost: {refusal}"
1839        );
1840        assert!(
1841            refusal.to_string().contains("1048576"),
1842            "the transport's own reason must reach the dispatcher: {refusal}"
1843        );
1844        assert_eq!(
1845            door.pushes.load(Ordering::SeqCst),
1846            1,
1847            "the walk must end at the first candidate: every connection shares the one bound, \
1848             and each further push would record one more lease for a step that cannot run"
1849        );
1850        for worker_id in worker_ids {
1851            assert!(
1852                registry.worker_by_id(worker_id)?.is_some(),
1853                "an unservable frame is not evidence that a worker is gone; its registration \
1854                 must stand"
1855            );
1856        }
1857        drop(registrations);
1858        Ok(())
1859    }
1860
1861    /// Register two liminal workers on `tenant-a`/`default` for `charge-card`,
1862    /// each with one slot, and hand back their ids beside the registration
1863    /// guards. The guards deregister on drop, so the caller holds them for as
1864    /// long as the workers must stand: a guard that fell at the end of a loop
1865    /// body is a worker that left before the push.
1866    #[cfg(feature = "liminal-transport")]
1867    fn register_two_liminal_workers(
1868        registry: &ConnectedWorkerRegistry,
1869    ) -> Result<
1870        (
1871            Vec<crate::worker::registry::WorkerId>,
1872            Vec<crate::worker::registry::WorkerRegistration>,
1873        ),
1874        Box<dyn std::error::Error>,
1875    > {
1876        use aion_core::InterventionCapabilities;
1877
1878        use crate::worker::liminal_transport::LiminalWorkerDelivery;
1879        use crate::worker::registry::{RegistrationOptions, WorkerDelivery};
1880
1881        let supervisor = liminal_server::server::connection::ConnectionSupervisor::new()?;
1882        let types = [String::from("charge-card")];
1883        let mut worker_ids = Vec::new();
1884        let mut registrations = Vec::new();
1885        for (pid, identity) in [(7, "liminal-1"), (8, "liminal-2")] {
1886            let registration = registry.register_delivery(
1887                [String::from("tenant-a")],
1888                String::from("default"),
1889                None,
1890                types.iter(),
1891                WorkerDelivery::Liminal(LiminalWorkerDelivery::new(supervisor.clone(), pid)),
1892                RegistrationOptions::identified(identity, 1)
1893                    .with_intervention_capabilities(InterventionCapabilities::none()),
1894            )?;
1895            worker_ids.push(
1896                registration
1897                    .worker_id()
1898                    .ok_or("a registration must assign a worker id")?,
1899            );
1900            registrations.push(registration);
1901        }
1902        Ok((worker_ids, registrations))
1903    }
1904
1905    fn scheduled_unpinned() -> ScheduledActivity {
1906        ScheduledActivity {
1907            namespace: String::from("tenant-a"),
1908            task_queue: String::from("default"),
1909            activity_type: String::from("charge-card"),
1910            // UNPINNED row: `node == None`, so placement (here a Pinned require) is
1911            // the worker-selection input — the row's own node is never set.
1912            node: None,
1913            workflow_id: workflow_id(),
1914            activity_id: activity_id(),
1915            run_id: Some(RunId::new_v4()),
1916            input: Payload::new(ContentType::Json, b"{}".to_vec()),
1917            attempt: 1,
1918            labels: std::collections::BTreeMap::new(),
1919            origin: DispatchOrigin::Engine,
1920        }
1921    }
1922
1923    /// HOLD-1: a delivered OUTBOX dispatch is COUNTED, so the next selection
1924    /// sees the slot it spent.
1925    ///
1926    /// This path had the capacity filter and neither half of the bookkeeping.
1927    /// `workers_for` read `in_flight`, but nothing on this path ever claimed a
1928    /// slot or handed one to the liveness tracker — `track_task` had exactly one
1929    /// production caller, on the bridge. So a fan of N legs at one worker all
1930    /// read a count nobody had claimed, all passed the filter, and all pushed;
1931    /// the surplus was refused, and refusals ride a BOUNDED transport-loss
1932    /// ledger, so a persistently over-pushed worker dead-lettered the overflow.
1933    ///
1934    /// Two facts, and the second is what the first is for: the delivery is
1935    /// tracked, and the count it leaves behind is what makes the NEXT dispatch
1936    /// to a one-slot worker come back busy instead of being pushed.
1937    #[tokio::test]
1938    async fn a_delivered_outbox_dispatch_is_tracked_and_spends_the_workers_slot()
1939    -> Result<(), Box<dyn std::error::Error>> {
1940        let registry = ConnectedWorkerRegistry::default();
1941        let tracker =
1942            crate::worker::heartbeat::HeartbeatTracker::new(std::time::Duration::from_secs(30));
1943        let delivery_gate = crate::worker::outbox_dispatcher::DeliveryGate::default();
1944        let dispatcher = ActivityDispatcher::new(registry.clone())
1945            .with_delivery_gate(delivery_gate.clone())
1946            .with_heartbeat_tracker(tracker.clone());
1947        let types = [String::from("charge-card")];
1948        let (tx, mut rx) = tokio::sync::mpsc::channel(4);
1949        let registration = registry.register("tenant-a", types.iter(), tx, 1)?;
1950        let worker_id = registration
1951            .worker_id()
1952            .ok_or("the registration must carry a worker id")?;
1953
1954        let mut scheduled = scheduled_unpinned();
1955        let dispatch_key = String::from("wf:0");
1956        scheduled.origin = DispatchOrigin::OutboxRow {
1957            dispatch_key: dispatch_key.clone(),
1958        };
1959        let claim = delivery_gate
1960            .begin(&dispatch_key)
1961            .ok_or("the row's claim must be acquirable")?;
1962
1963        dispatcher.dispatch(&scheduled).await?;
1964        assert!(rx.recv().await.is_some(), "the task must reach the worker");
1965
1966        assert!(
1967            tracker.is_tracked(worker_id, &scheduled.workflow_id, &scheduled.activity_id)?,
1968            "a delivered outbox dispatch must be handed to the liveness tracker; untracked, the \
1969             expiry sweep cannot see it and the capacity count never learns it happened"
1970        );
1971        assert_eq!(
1972            registry.in_flight_for_worker(worker_id)?,
1973            1,
1974            "the delivery must spend the worker's slot in the count selection reads"
1975        );
1976        assert!(
1977            registry
1978                .workers_for("tenant-a", "default", "charge-card", None)?
1979                .is_empty(),
1980            "and that count must take the worker out of the candidate set: this is the \
1981             over-push the outbox path had no protection against"
1982        );
1983
1984        // A SECOND row now finds the pool busy rather than being pushed at a
1985        // worker with no slot left — the consequence the count exists for.
1986        drop(claim);
1987        let second_key = String::from("wf:1");
1988        let mut second = scheduled_unpinned();
1989        second.origin = DispatchOrigin::OutboxRow {
1990            dispatch_key: second_key.clone(),
1991        };
1992        let _second_claim = delivery_gate
1993            .begin(&second_key)
1994            .ok_or("the second row's claim must be acquirable")?;
1995        let refusal = dispatcher
1996            .dispatch(&second)
1997            .await
1998            .err()
1999            .ok_or("a worker holding its only slot must not take a second dispatch")?;
2000        assert!(
2001            refusal.is_worker_busy(),
2002            "and it must come back attempt-neutral, not as a delivery failure: {refusal}"
2003        );
2004        Ok(())
2005    }
2006
2007    /// aion#204 item 3: the unserved record SURVIVES a busy return.
2008    ///
2009    /// A busy pool re-arms attempt-neutrally, so the busy return is the start of
2010    /// a wait rather than the end of one. Withdrawing the queue-service record
2011    /// on the way out made `/queues/unserved` flicker on and off once per
2012    /// re-arm, so the one place an operator can see WHY their rows are waiting
2013    /// showed nothing most of the time — while the ruling this implements
2014    /// promised them exactly that visibility.
2015    ///
2016    /// The clearing half is the control: a delivery must still withdraw it, or
2017    /// this test would pass for a record that is simply never cleared.
2018    #[tokio::test]
2019    async fn a_busy_pool_keeps_its_unserved_record_until_it_is_served()
2020    -> Result<(), Box<dyn std::error::Error>> {
2021        let registry = ConnectedWorkerRegistry::default();
2022        let queue_state = QueueServiceState::default();
2023        let delivery_gate = crate::worker::outbox_dispatcher::DeliveryGate::default();
2024        let dispatcher = ActivityDispatcher::new(registry.clone())
2025            .with_delivery_gate(delivery_gate.clone())
2026            .with_queue_service(
2027                QueueDeclarationSource::default(),
2028                queue_state.clone(),
2029                QueueServiceConfig::default(),
2030            );
2031        let types = [String::from("charge-card")];
2032        let (tx, mut rx) = tokio::sync::mpsc::channel(4);
2033        let registration = registry.register("tenant-a", types.iter(), tx, 1)?;
2034        let worker_id = registration
2035            .worker_id()
2036            .ok_or("the registration must carry a worker id")?;
2037
2038        let mut scheduled = scheduled_unpinned();
2039        let dispatch_key = String::from("wf:0");
2040        scheduled.origin = DispatchOrigin::OutboxRow {
2041            dispatch_key: dispatch_key.clone(),
2042        };
2043        let _claim = delivery_gate
2044            .begin(&dispatch_key)
2045            .ok_or("the row's claim must be acquirable")?;
2046
2047        let held = registry
2048            .reserve_worker(worker_id)?
2049            .ok_or("an idle worker must have a slot")?;
2050        let refusal = dispatcher
2051            .dispatch(&scheduled)
2052            .await
2053            .err()
2054            .ok_or("a full pool must not deliver")?;
2055        assert!(refusal.is_worker_busy(), "{refusal}");
2056
2057        let unserved = queue_state.unserved()?;
2058        assert_eq!(
2059            unserved.len(),
2060            1,
2061            "a busy pool must leave its unserved record standing: the queue IS \
2062             unserved-by-capacity for as long as that is true, and this is the only place an \
2063             operator can read the reason"
2064        );
2065        assert_eq!(
2066            unserved[0].reason,
2067            QueueServiceReason::PollersAtCapacity,
2068            "and it must carry the capacity reason by name"
2069        );
2070
2071        // THE CONTROL: a delivery withdraws it.
2072        drop(held);
2073        dispatcher.dispatch(&scheduled).await?;
2074        assert!(rx.recv().await.is_some());
2075        assert!(
2076            queue_state.unserved()?.is_empty(),
2077            "a served queue must withdraw its unserved record, or the assertion above would \
2078             hold for a record that is simply never cleared"
2079        );
2080        Ok(())
2081    }
2082
2083    /// aion#204 item 4: the HARD-PINNED path answers a busy pool the same way
2084    /// the unpinned path does.
2085    ///
2086    /// `dispatch_requiring` waits for a worker to register, which is right when
2087    /// no worker on a required node EXISTS. It was doing the same thing when the
2088    /// required node's workers were merely BUSY — and for an outbox row that
2089    /// means sitting on a durable claim while the row's own attempt budget,
2090    /// backoff and dead-letter never run, which is precisely what its sibling's
2091    /// comment forbids. A busy pool is going to free a slot; the row belongs
2092    /// back with the dispatcher that owns it.
2093    #[tokio::test]
2094    async fn a_pinned_outbox_row_against_a_busy_node_is_busy_not_parked()
2095    -> Result<(), Box<dyn std::error::Error>> {
2096        let registry = ConnectedWorkerRegistry::default();
2097        let delivery_gate = crate::worker::outbox_dispatcher::DeliveryGate::default();
2098        let dispatcher =
2099            ActivityDispatcher::new(registry.clone()).with_delivery_gate(delivery_gate.clone());
2100        let types = [String::from("charge-card")];
2101        let (tx, _rx) = tokio::sync::mpsc::channel(4);
2102        let registration = registry.register_namespaces(
2103            [String::from("tenant-a")],
2104            "default",
2105            Some(String::from("n1")),
2106            types.iter(),
2107            tx,
2108            1,
2109        )?;
2110        let worker_id = registration
2111            .worker_id()
2112            .ok_or("the registration must carry a worker id")?;
2113
2114        let mut scheduled = scheduled_unpinned();
2115        let dispatch_key = String::from("wf:pinned");
2116        scheduled.origin = DispatchOrigin::OutboxRow {
2117            dispatch_key: dispatch_key.clone(),
2118        };
2119        let _claim = delivery_gate
2120            .begin(&dispatch_key)
2121            .ok_or("the row's claim must be acquirable")?;
2122
2123        let _held = registry
2124            .reserve_worker(worker_id)?
2125            .ok_or("an idle worker must have a slot")?;
2126
2127        // Bounded, because the defect this pins is a WAIT: before the fix this
2128        // call parked on `arrival` and never returned, so a plain `.await` here
2129        // would hang the suite rather than fail it.
2130        let refusal = tokio::time::timeout(
2131            std::time::Duration::from_secs(5),
2132            dispatcher.dispatch_requiring(&scheduled, &required(&["n1"])),
2133        )
2134        .await
2135        .map_err(|_| {
2136            "the pinned path parked on a busy pool instead of returning; the row stays claimed \
2137             and its attempt budget never runs"
2138        })?
2139        .err()
2140        .ok_or("a full pinned pool must not deliver")?;
2141        assert!(
2142            refusal.is_worker_busy(),
2143            "the pinned path must answer a busy pool exactly as the unpinned path does, so the \
2144             outbox re-arms it attempt-neutrally: {refusal}"
2145        );
2146        Ok(())
2147    }
2148
2149    /// HOLD-2: a BUSY pool must not cost an outbox row one of its attempts.
2150    ///
2151    /// `workers_for` excludes a worker at its advertised capacity, so an empty
2152    /// candidate list stopped meaning "nobody serves this" and started also
2153    /// meaning "everybody who serves this is busy". Returned as the unservable
2154    /// reason, the second case spent an attempt, backed the row off, and
2155    /// dead-lettered it if the pool stayed busy across the budget — work
2156    /// discarded because a worker was working, which is the harm this landing
2157    /// exists to remove.
2158    ///
2159    /// The busy case now resolves as `WorkerBusy`, the class the outbox
2160    /// dispatcher already re-arms attempt-neutrally, and the release half is the
2161    /// vacuity control: once a slot frees the same row is served, so the refusal
2162    /// above was capacity and not an unservable pool.
2163    #[tokio::test]
2164    async fn an_outbox_row_against_a_full_pool_is_busy_not_unservable()
2165    -> Result<(), Box<dyn std::error::Error>> {
2166        let registry = ConnectedWorkerRegistry::default();
2167        // The row's claim gate is SHARED with the dispatcher and held open for
2168        // the whole test: an outbox row's delivery re-asks whether its claim
2169        // still stands, and a dispatcher holding a private gate answers
2170        // "released" for every key — which would abandon the delivery as a
2171        // gate failure and never reach the capacity question at all.
2172        let delivery_gate = crate::worker::outbox_dispatcher::DeliveryGate::default();
2173        let dispatcher =
2174            ActivityDispatcher::new(registry.clone()).with_delivery_gate(delivery_gate.clone());
2175        let types = [String::from("charge-card")];
2176        let (tx, mut rx) = tokio::sync::mpsc::channel(4);
2177        let registration = registry.register("tenant-a", types.iter(), tx, 1)?;
2178        let worker_id = registration
2179            .worker_id()
2180            .ok_or("the registration must carry a worker id")?;
2181
2182        let mut scheduled = scheduled_unpinned();
2183        let dispatch_key = String::from("wf:0");
2184        scheduled.origin = DispatchOrigin::OutboxRow {
2185            dispatch_key: dispatch_key.clone(),
2186        };
2187        let _claim = delivery_gate
2188            .begin(&dispatch_key)
2189            .ok_or("the row's claim must be acquirable")?;
2190
2191        // FILL the worker's one slot, exactly as a delivered dispatch does.
2192        let held = registry
2193            .reserve_worker(worker_id)?
2194            .ok_or("an idle worker must have a slot")?;
2195        assert_eq!(
2196            registry.in_flight_for_worker(worker_id)?,
2197            1,
2198            "the held reservation must be visible to selection"
2199        );
2200        assert!(
2201            registry
2202                .workers_for("tenant-a", "default", "charge-card", None)?
2203                .is_empty(),
2204            "a worker holding its only slot must not be an eligible candidate"
2205        );
2206
2207        let refusal = dispatcher
2208            .dispatch(&scheduled)
2209            .await
2210            .err()
2211            .ok_or("a full pool must not deliver")?;
2212        assert!(
2213            refusal.is_worker_busy(),
2214            "a busy pool must resolve as WorkerBusy, which the outbox dispatcher re-arms without \
2215             spending an attempt; got {refusal}"
2216        );
2217        assert!(
2218            refusal.to_string().contains("advertised concurrency"),
2219            "the refusal must say WHY the row is waiting, so an operator reads 'queued behind \
2220             real work' rather than only that it failed: {refusal}"
2221        );
2222
2223        // THE CONTROL. Free the slot and the same row is served — so the
2224        // refusal above was capacity, not a pool nothing could serve.
2225        drop(held);
2226        dispatcher
2227            .dispatch(&scheduled)
2228            .await
2229            .map_err(|error| format!("the freed slot must serve the row: {error}"))?;
2230        assert!(
2231            rx.recv().await.is_some(),
2232            "once a slot frees the row must be delivered to the same worker"
2233        );
2234        Ok(())
2235    }
2236
2237    fn required(labels: &[&str]) -> std::collections::BTreeSet<String> {
2238        labels.iter().map(|l| (*l).to_owned()).collect()
2239    }
2240
2241    /// P2-I1 gRPC hard-pin: an unpinned row in a `Pinned{n1}` namespace WAITS when
2242    /// no `n1` worker is live and NEVER spills to a live any-node worker — the
2243    /// opposite of `Prefer`. This test would FAIL under the old fall-through (which
2244    /// dispatched `Pinned` to any worker).
2245    #[tokio::test]
2246    async fn dispatch_requiring_waits_and_never_spills_to_a_wrong_node_worker()
2247    -> Result<(), Box<dyn std::error::Error>> {
2248        let registry = ConnectedWorkerRegistry::default();
2249        let dispatcher = ActivityDispatcher::new(registry.clone());
2250        let scheduled = scheduled_unpinned();
2251        let types = [String::from("charge-card")];
2252
2253        // A LIVE worker on the WRONG node (n2) — a Prefer would spill to it; a
2254        // Pinned{n1} must NOT.
2255        let (wrong_tx, mut wrong_rx) = tokio::sync::mpsc::channel(1);
2256        let _wrong = registry.register_namespaces(
2257            [String::from("tenant-a")],
2258            "default",
2259            Some(String::from("n2")),
2260            types.iter(),
2261            wrong_tx,
2262            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2263        )?;
2264
2265        let handle = tokio::spawn({
2266            let dispatcher = dispatcher.clone();
2267            let scheduled = scheduled.clone();
2268            async move {
2269                dispatcher
2270                    .dispatch_requiring(&scheduled, &required(&["n1"]))
2271                    .await
2272            }
2273        });
2274
2275        // The wrong-node worker is idle and live, yet dispatch must still be waiting.
2276        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2277        assert!(
2278            !handle.is_finished(),
2279            "Pinned{{n1}} must WAIT rather than spill to the live n2 worker"
2280        );
2281        assert!(
2282            wrong_rx.try_recv().is_err(),
2283            "the wrong-node (n2) worker must never receive the task"
2284        );
2285
2286        // Bring up the REQUIRED n1 worker: the wait resolves onto it.
2287        let (right_tx, mut right_rx) = tokio::sync::mpsc::channel(1);
2288        let _right = registry.register_namespaces(
2289            [String::from("tenant-a")],
2290            "default",
2291            Some(String::from("n1")),
2292            types.iter(),
2293            right_tx,
2294            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2295        )?;
2296
2297        handle.await??;
2298        assert!(
2299            right_rx.recv().await.is_some(),
2300            "the required n1 worker receives the task once live"
2301        );
2302        assert!(
2303            wrong_rx.try_recv().is_err(),
2304            "the wrong-node worker still never received it"
2305        );
2306        Ok(())
2307    }
2308
2309    /// P2-I1 determinism: the row's authored `node` stays `None` through a Pinned
2310    /// dispatch — placement is a pure selection input, never written back.
2311    #[tokio::test]
2312    async fn dispatch_requiring_never_mutates_the_rows_node()
2313    -> Result<(), Box<dyn std::error::Error>> {
2314        let registry = ConnectedWorkerRegistry::default();
2315        let dispatcher = ActivityDispatcher::new(registry.clone());
2316        let scheduled = scheduled_unpinned();
2317        assert_eq!(scheduled.node, None, "precondition: the row is unpinned");
2318        let types = [String::from("charge-card")];
2319        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
2320        let _right = registry.register_namespaces(
2321            [String::from("tenant-a")],
2322            "default",
2323            Some(String::from("n1")),
2324            types.iter(),
2325            tx,
2326            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2327        )?;
2328
2329        dispatcher
2330            .dispatch_requiring(&scheduled, &required(&["n1"]))
2331            .await?;
2332
2333        assert!(rx.recv().await.is_some(), "the n1 worker received the task");
2334        assert_eq!(
2335            scheduled.node, None,
2336            "the row's authored node MUST remain None through a Pinned dispatch \
2337             (the determinism invariant, CP-Phase-2 §2.4)"
2338        );
2339        Ok(())
2340    }
2341
2342    #[derive(Default)]
2343    struct RecordingSink {
2344        completions: Mutex<Vec<ActivityCompletion>>,
2345    }
2346
2347    impl ActivityCompletionSink for RecordingSink {
2348        fn complete_activity(&self, completion: ActivityCompletion) -> Result<(), ServerError> {
2349            self.completions
2350                .lock()
2351                .map_err(|_| ServerError::lock_poisoned("recording completion sink"))?
2352                .push(completion);
2353            Ok(())
2354        }
2355
2356        fn park_activity(
2357            &self,
2358            _workflow_id: &WorkflowId,
2359            _activity_id: &ActivityId,
2360        ) -> Result<(), ServerError> {
2361            Err(ServerError::worker_dispatch(
2362                "",
2363                "",
2364                "result-handoff tests never park a dispatch",
2365            ))
2366        }
2367    }
2368
2369    #[test]
2370    fn successful_activity_result_calls_completion_sink() -> Result<(), Box<dyn std::error::Error>>
2371    {
2372        let sink = RecordingSink::default();
2373        let output = payload(&json!({"ok": true}))?;
2374        let result = ProtoActivityResult {
2375            workflow_id: Some(ProtoWorkflowId::from(workflow_id())),
2376            activity_id: Some(ProtoActivityId::from(activity_id())),
2377            run_id: Some(ProtoRunId::from(RunId::new_v4())),
2378            completion_token: String::from("generation-1"),
2379            outcome: Some(proto_activity_result::Outcome::Result(ProtoPayload::from(
2380                output.clone(),
2381            ))),
2382        };
2383
2384        handle_activity_result(&sink, result)?;
2385        let completions = sink
2386            .completions
2387            .lock()
2388            .map_err(|_| ServerError::lock_poisoned("recording completion sink"))?;
2389
2390        assert_eq!(completions.len(), 1);
2391        assert_eq!(completions[0].workflow_id, workflow_id());
2392        assert_eq!(completions[0].activity_id, activity_id());
2393        assert_eq!(
2394            completions[0].outcome,
2395            ActivityCompletionOutcome::Succeeded(output)
2396        );
2397        Ok(())
2398    }
2399
2400    #[test]
2401    fn failed_activity_result_preserves_error_classification()
2402    -> Result<(), Box<dyn std::error::Error>> {
2403        let sink = RecordingSink::default();
2404        let error = ProtoActivityError {
2405            kind: ProtoActivityErrorKind::Retryable as i32,
2406            message: String::from("temporary outage"),
2407            details: Some(ProtoPayload::from(payload(
2408                &json!({"retry_after_ms": 500}),
2409            )?)),
2410        };
2411        let result = ProtoActivityResult {
2412            workflow_id: Some(ProtoWorkflowId::from(workflow_id())),
2413            activity_id: Some(ProtoActivityId::from(activity_id())),
2414            run_id: Some(ProtoRunId::from(RunId::new_v4())),
2415            completion_token: String::from("generation-1"),
2416            outcome: Some(proto_activity_result::Outcome::Error(error)),
2417        };
2418
2419        handle_activity_result(&sink, result)?;
2420        let completions = sink
2421            .completions
2422            .lock()
2423            .map_err(|_| ServerError::lock_poisoned("recording completion sink"))?;
2424
2425        assert_eq!(completions.len(), 1);
2426        match &completions[0].outcome {
2427            ActivityCompletionOutcome::Failed(error) => {
2428                assert_eq!(error.kind, ActivityErrorKind::Retryable);
2429                assert!(error.is_retryable());
2430            }
2431            other => return Err(format!("expected failed outcome, got {other:?}").into()),
2432        }
2433        Ok(())
2434    }
2435}