Skip to main content

aion_server/worker/
bridge.rs

1//! NIF bridge dispatcher that routes `run_activity` calls to connected workers.
2//!
3//! `WorkerActivityDispatcher` implements `aion::ActivityDispatcher` so the
4//! engine's activity NIFs can synchronously dispatch to a remote worker and
5//! block until the result comes back.
6//!
7//! # Threading contract
8//!
9//! The engine invokes [`aion::ActivityDispatcher::dispatch`] from two kinds of
10//! threads: beamr scheduler threads (concurrency combinators) and spawned
11//! tokio tasks (the two-phase `dispatch_activity` completion task). The task
12//! send uses `try_send()` (non-blocking channel push) and the response wait
13//! blocks on `std::sync::mpsc::Receiver::recv`.
14//!
15//! Blocking is harmless on a beamr thread, but on a tokio runtime worker it
16//! must be wrapped in `tokio::task::block_in_place`: the `try_send` wakes the
17//! per-worker gRPC stream forwarder task, and tokio schedules a task woken
18//! from task context into the *current* worker's LIFO slot, which no other
19//! runtime worker can steal. Without the `block_in_place` core handoff the
20//! forwarder sits trapped in that slot while this thread blocks, so the queued
21//! `ActivityTask` is never flushed to the worker even though the worker is
22//! healthy. `block_in_place` moves the worker's scheduler core (LIFO slot
23//! included) to another thread before the wait begins, so dispatch-to-delivery
24//! stays in the millisecond range and the runtime keeps full parallelism.
25//!
26//! # Wait termination
27//!
28//! The engine imposes no activity timeout of its own: agent-style activities
29//! legitimately run for over an hour, so the completion wait is unbounded.
30//! The blocking `recv` terminates on exactly one of:
31//!
32//! - **Completion** — the worker reports a result and the stream handler
33//!   delivers it through [`ActivityCompletionSink::complete_activity`].
34//! - **Worker loss** — the worker's gRPC stream ends (process death,
35//!   disconnect, expired token); the stream teardown sweeps the worker's
36//!   in-flight tasks through the same sink as every other TRANSPORT loss
37//!   ([`HeartbeatTracker::fail_disconnected_worker`]). A liminal-delivered
38//!   worker's loss is observed by its reply router thread instead (the
39//!   correlated-reply awaiter wakes the moment the connection closes) and
40//!   resolves the dispatch with the same transport-loss class. Both are
41//!   classified by [`transport_loss`](crate::worker::transport_loss): `lost:`
42//!   while the transport still has budget to deliver the activity (the engine
43//!   re-dispatches the SAME attempt, recording nothing), `transport-exhausted:`
44//!   once that budget is spent. Neither ever wears the action's retry
45//!   vocabulary — the activity never ran.
46//! - **Graceful-drain park (#207)** — during a drain, a worker stream ending
47//!   (or the drain-timeout backstop) PARKS the worker's in-flight tasks
48//!   through [`ActivityCompletionSink::park_activity`]: the waiter resolves
49//!   with the ephemeral parked sentinel
50//!   ([`aion::PARKED_ACTIVITY_REASON`]), nothing is recorded or delivered,
51//!   and restart recovery re-dispatches the dangling ordinal — kill -9
52//!   convergence.
53//! - **Drain timeout at shutdown** — the shutdown coordinator parks all
54//!   remaining in-flight tasks through the sink
55//!   (`HeartbeatTracker::park_all_in_flight_workers`).
56//! - **Channel teardown** — every sender for the pending entry is dropped
57//!   (a cleanup path removed the entry without completing it); surfaced as
58//!   a channel-closed dispatch error, never a hang.
59//!
60//! An activity's duration is bounded only by the workflow's own
61//! `timeout_seconds` and by worker liveness — never by an engine constant.
62
63use std::collections::BTreeMap;
64use std::sync::{Arc, OnceLock};
65use std::time::{Duration, Instant};
66
67use aion::{ActivityDispatch, ActivityDispatcher, LeaseSignal};
68use aion_core::{ActivityId, RunId, WorkflowId};
69use aion_proto::{ProtoActivityId, ProtoActivityTask, ProtoPayload, ProtoWorkflowId};
70use dashmap::DashMap;
71use dashmap::mapref::entry::Entry;
72
73use super::dispatch::{ActivityCompletion, ActivityCompletionSink};
74use super::envelope::{CompletionFences, CompletionToken, idempotency_key};
75use super::heartbeat::{HeartbeatTracker, InFlightActivity};
76use super::queue_service::{
77    DeliveryRefusal, ExpiredClock, PARK_POLL_INTERVAL, PoolCensus, QueueDeclarationSource,
78    QueueServiceConfig, QueueServiceReason, QueueServiceState, SelectionRefusal, ServiceAddress,
79    ServiceWait, WorkerUnavailable, deliver_within_schedule_to_start, select_worker_or_refuse,
80};
81use super::registry::{
82    ConnectedWorkerRegistry, DispatchReservation, WorkerArrival, WorkerDelivery, WorkerHandle,
83    WorkerId, WorkerMessage,
84};
85use crate::error::ServerError;
86use crate::shutdown::DrainState;
87
88mod completion_outcome;
89use tracing::info_span;
90
91type SyncSender = std::sync::mpsc::SyncSender<Result<String, String>>;
92
93/// One live dispatch's waiter slot: the channel that unblocks its
94/// `await_activity_result`, and the attempt it was dispatched at.
95///
96/// The attempt travels with the sender so resolution paths owned by ONE
97/// dispatch (`deliver_result`, `cleanup_activity`) remove the entry only while
98/// it is still that dispatch's own — after a higher attempt takes the site
99/// over (aion#195), the superseded dispatch's teardown must not evict the live
100/// waiter that replaced it. Whole-site resolutions (a fence-accepted
101/// completion, a park) stay unconditional: they retire the site, whoever
102/// holds it.
103struct PendingWaiter {
104    sender: SyncSender,
105    attempt: u32,
106}
107
108/// The reason a superseded waiter is resolved with when a higher attempt takes
109/// over its execution site (aion#195).
110///
111/// Worn by the DROPPED dispatch future of an attempt whose per-attempt bound
112/// already failed it, so it normally reaches nobody. It still wears the
113/// `retryable:` prefix deliberately: if a race ever surfaces it to the retry
114/// loop, a retry is the harmless reading (the fence refuses duplicates), where
115/// a terminal reading would repeat the exact defect this exists to close.
116fn superseded_site_reason(new_attempt: u32) -> String {
117    format!(
118        "retryable: superseded by attempt {new_attempt} at this execution site — the prior \
119         attempt reached its bound, its completion token is fence-refused, and its worker \
120         has been asked to stop"
121    )
122}
123type SyncReceiver = std::sync::mpsc::Receiver<Result<String, String>>;
124
125/// Execution-scoped key for an in-flight activity dispatch.
126///
127/// The engine seam ([`ActivityDispatch`]) carries the *real* workflow id and
128/// the *real* per-workflow activity ordinal recorded in history, so this pair
129/// uniquely and stably identifies one execution. Keying by bare [`ActivityId`]
130/// would be unsafe across server restarts — a stale result re-reported from a
131/// worker's previous session could complete a *different* post-restart
132/// dispatch reusing the same ordinal — but pairing it with the real workflow
133/// id closes that race: two different workflow executions never share a
134/// workflow id, so a stale `(workflow_id, activity_id)` from a previous server
135/// life can only ever match the exact execution it belongs to.
136///
137/// The wire (`ActivityResult`) carries both ids, plus an attempt discriminator
138/// (`ActivityTask.attempt`). The pending key stays attempt-free for now: a
139/// retry re-dispatches under the same `(workflow_id, activity_id)` and the
140/// outstanding entry is the one awaiting completion. Redelivery bookkeeping
141/// can widen this key with the wire attempt later — no protocol change needed.
142type PendingActivityKey = (WorkflowId, ActivityId);
143
144/// Routes an unmatched durable-outbox completion into the live workflow.
145///
146/// When the outbox is ON a worker completion can arrive at the sink with no
147/// pending oneshot (the dispatch was non-blocking fan-out, or the original
148/// waiter was lost). Rather than dropping it, [`PendingActivities::complete`]
149/// hands it to this callback, which resolves the workflow to its live engine
150/// process and delivers the terminal into its mailbox. The callback is only
151/// installed when the outbox is enabled, so flag-off the unmatched branch
152/// stays a silent drop.
153pub trait OutboxDeliveryCallback: Send + Sync {
154    /// Deliver a successful completion to the live workflow.
155    ///
156    /// Returns `Ok(true)` when delivered to a live workflow and `Ok(false)`
157    /// when no run is currently live (the expected stale-completion case that
158    /// recovery re-arms).
159    ///
160    /// # Errors
161    ///
162    /// Returns [`ServerError`] when the engine rejects the delivery.
163    fn deliver_completion(
164        &self,
165        workflow_id: &WorkflowId,
166        activity_id: &ActivityId,
167        run_id: Option<&RunId>,
168        result: String,
169    ) -> Result<bool, ServerError>;
170
171    /// Deliver a failure to the live workflow. Same `bool`/error contract as
172    /// [`Self::deliver_completion`].
173    ///
174    /// # Errors
175    ///
176    /// Returns [`ServerError`] when the engine rejects the delivery.
177    fn deliver_failure(
178        &self,
179        workflow_id: &WorkflowId,
180        activity_id: &ActivityId,
181        run_id: Option<&RunId>,
182        reason: String,
183    ) -> Result<bool, ServerError>;
184}
185
186/// Tracks in-flight activity dispatches waiting for worker results.
187///
188/// When the server's worker stream handler receives an `ActivityResult`, it
189/// calls [`complete_activity`](ActivityCompletionSink::complete_activity) to
190/// deliver the result to the blocked NIF thread. Entries are keyed by
191/// [`PendingActivityKey`] so a stale result from a previous server life can
192/// never be matched to a different execution (#59).
193///
194/// Clones share both the pending map and the outbox-delivery callback through
195/// `Arc`, so [`set_outbox_delivery`](Self::set_outbox_delivery) called once on
196/// any clone after construction is visible to the clone the dispatcher holds.
197#[derive(Clone)]
198pub struct PendingActivities {
199    pending: Arc<DashMap<PendingActivityKey, PendingWaiter>>,
200    completion_fences: CompletionFences,
201    outbox_delivery: Arc<OnceLock<Arc<dyn OutboxDeliveryCallback>>>,
202    /// Where an accepted delivery's lease is recorded (WA-010 R3), shared by
203    /// the bridge and every outbox dispatcher built over these seams so both
204    /// paths attribute through one recorder and count on one ledger.
205    lease_recorder: super::lease_record::LeaseRecorderSeam,
206    /// The transport's OWN re-dispatch budget for activities whose worker died
207    /// before reporting (see [`transport_loss`](crate::worker::transport_loss)).
208    /// Shared across clones, so every loss for one execution site lands in one
209    /// budget. There is no default: the window is required at construction, so
210    /// a zero budget cannot be inherited by omission.
211    transport_losses: super::transport_loss::TransportLossLedger,
212}
213
214impl std::fmt::Debug for PendingActivities {
215    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        formatter
217            .debug_struct("PendingActivities")
218            .field("pending", &self.pending.len())
219            .field("completion_fences", &self.completion_fences)
220            .field(
221                "outbox_delivery_installed",
222                &self.outbox_delivery.get().is_some(),
223            )
224            .field("lease_recorder", &self.lease_recorder)
225            .field("transport_losses", &self.transport_losses)
226            .finish()
227    }
228}
229
230impl PendingActivities {
231    /// Register the waiter for one dispatch, taking over the execution site
232    /// from a superseded lower attempt when one still holds it (aion#195).
233    ///
234    /// The third return names the attempt that was superseded, so the caller
235    /// can walk the release ladder for it (ask its worker to stop, untrack its
236    /// heartbeat task). The fence issue above already superseded the holder's
237    /// completion tokens — a takeover changes who may still answer, never what
238    /// would have been accepted.
239    fn insert(
240        &self,
241        workflow_id: WorkflowId,
242        run_id: &RunId,
243        activity_id: ActivityId,
244        attempt: u32,
245    ) -> Result<(CompletionToken, SyncReceiver, Option<u32>), ServerError> {
246        let completion_token =
247            self.completion_fences
248                .issue(&workflow_id, run_id, &activity_id, attempt)?;
249        let (tx, rx) = std::sync::mpsc::sync_channel(1);
250        let key = (workflow_id, activity_id);
251        match self.pending.entry(key) {
252            Entry::Vacant(entry) => {
253                entry.insert(PendingWaiter {
254                    sender: tx,
255                    attempt,
256                });
257                Ok((completion_token, rx, None))
258            }
259            Entry::Occupied(mut entry) => {
260                let held_attempt = entry.get().attempt;
261                if attempt > held_attempt {
262                    // The holder is a corpse: issuing this attempt's token
263                    // superseded its generation, so nothing it answers can be
264                    // accepted. Waking it with the superseded sentinel lets
265                    // its blocked dispatcher thread finish; its attempt-aware
266                    // teardown cannot evict this replacement.
267                    let superseded = entry.insert(PendingWaiter {
268                        sender: tx,
269                        attempt,
270                    });
271                    let _ = superseded.sender.send(Err(superseded_site_reason(attempt)));
272                    Ok((completion_token, rx, Some(held_attempt)))
273                } else {
274                    let (workflow_id, activity_id) = entry.key();
275                    Err(ServerError::PendingActivityCollision {
276                        workflow_id: workflow_id.clone(),
277                        activity_id: activity_id.clone(),
278                        held_attempt,
279                        incoming_attempt: attempt,
280                    })
281                }
282            }
283        }
284    }
285
286    /// Share the generation registry with non-blocking outbox dispatch.
287    #[must_use]
288    pub fn completion_fences(&self) -> CompletionFences {
289        self.completion_fences.clone()
290    }
291
292    /// Test seam: register a pending waiter exactly as a live dispatch does,
293    /// so crate-internal tests outside this module (the stream-teardown
294    /// park-vs-fail pins) can observe how a sweep resolves it.
295    #[cfg(test)]
296    pub(crate) fn insert_for_test(
297        &self,
298        workflow_id: WorkflowId,
299        run_id: &RunId,
300        activity_id: ActivityId,
301        attempt: u32,
302    ) -> Result<(CompletionToken, SyncReceiver, Option<u32>), ServerError> {
303        self.insert(workflow_id, run_id, activity_id, attempt)
304    }
305
306    /// Install the unmatched-completion delivery callback (idempotent).
307    ///
308    /// Set once, after construction, when the durable outbox is enabled. A
309    /// second set is ignored and logged: the callback is process-wide and must
310    /// not silently change identity.
311    pub fn set_outbox_delivery(&self, callback: Arc<dyn OutboxDeliveryCallback>) {
312        if self.outbox_delivery.set(callback).is_err() {
313            tracing::warn!("outbox delivery callback already installed; ignoring duplicate set");
314        }
315    }
316
317    /// Complete a pending dispatch, or route an unmatched completion to the
318    /// outbox delivery callback when one is installed.
319    ///
320    /// A matched entry delivers to its waiting oneshot exactly as before. An
321    /// unmatched completion is dropped silently when no callback is installed
322    /// (outbox OFF — byte-identical to the prior behaviour); with a callback
323    /// installed (outbox ON) it is routed into the live workflow's mailbox.
324    fn complete(
325        &self,
326        workflow_id: &WorkflowId,
327        activity_id: &ActivityId,
328        run_id: Option<&RunId>,
329        result: Result<String, String>,
330    ) -> bool {
331        // Take and drop the DashMap guard before any callback runs: the engine
332        // delivery the callback invokes must never execute under a shard lock.
333        let matched = self
334            .pending
335            .remove(&(workflow_id.clone(), activity_id.clone()));
336        if let Some((_, waiter)) = matched {
337            return waiter.sender.send(result).is_ok();
338        }
339        let Some(callback) = self.outbox_delivery.get() else {
340            // Outbox OFF: silent drop, byte-identical to the prior behaviour.
341            return false;
342        };
343        let outcome = match result {
344            Ok(payload) => callback.deliver_completion(workflow_id, activity_id, run_id, payload),
345            Err(reason) => callback.deliver_failure(workflow_id, activity_id, run_id, reason),
346        };
347        match outcome {
348            Ok(true) => true,
349            Ok(false) => {
350                // Not live: the expected stale-completion case recovery re-arms.
351                tracing::debug!(
352                    workflow_id = %workflow_id,
353                    activity_id = %activity_id,
354                    "unmatched outbox completion for a workflow that is not currently live; \
355                     recovery will re-arm it"
356                );
357                false
358            }
359            Err(error) => {
360                tracing::warn!(
361                    workflow_id = %workflow_id,
362                    activity_id = %activity_id,
363                    %error,
364                    "failed to deliver unmatched outbox completion to the live workflow"
365                );
366                false
367            }
368        }
369    }
370
371    fn complete_fenced_after_accept(
372        &self,
373        workflow_id: &WorkflowId,
374        activity_id: &ActivityId,
375        run_id: Option<&RunId>,
376        completion_token: &CompletionToken,
377        result: Result<String, String>,
378        after_accept: impl FnOnce() -> Result<bool, ServerError>,
379    ) -> Result<bool, ServerError> {
380        // The acceptance hands back the generation it consumed — every token
381        // outstanding for this attempt, not just the one submitted — so the two
382        // restore arms below put back exactly what was taken rather than
383        // re-deriving it and inventing a narrower generation.
384        let accepted = self
385            .completion_fences
386            .accept(workflow_id, activity_id, completion_token)
387            .inspect_err(|error| {
388                tracing::warn!(
389                    workflow_id = %workflow_id,
390                    activity_id = %activity_id,
391                    %error,
392                    "activity completion rejected by execution-generation fence"
393                );
394            })?;
395        let should_publish = match after_accept() {
396            Ok(should_publish) => should_publish,
397            Err(error) => {
398                if let Err(restore_error) =
399                    self.completion_fences
400                        .restore_if_absent(workflow_id, activity_id, &accepted)
401                {
402                    tracing::error!(%restore_error, "failed to restore completion generation after settlement failure");
403                }
404                return Err(error);
405            }
406        };
407        if !should_publish {
408            // A non-publishing settlement means ANOTHER path owns this
409            // dispatch's resolution (the lost-worker sweep, typically). That
410            // path will present the same token to `accept` — so the
411            // generation this call consumed must be restored, exactly as on
412            // the failure arm, or the true resolver is refused with
413            // `NoCurrentGeneration` and the waiter is never resolved.
414            if let Err(restore_error) =
415                self.completion_fences
416                    .restore_if_absent(workflow_id, activity_id, &accepted)
417            {
418                tracing::error!(%restore_error, "failed to restore completion generation after a non-publishing settlement");
419            }
420            return Ok(false);
421        }
422        // A resolution OUTSIDE the transport domain — a real result, or a real
423        // action failure — retires this execution site's transport-loss budget,
424        // so a run that survived one blip does not carry it forward. A
425        // transport-domain resolution deliberately does not: the running budget
426        // is exactly what bounds a flapping link.
427        let transport_domain = result
428            .as_ref()
429            .err()
430            .is_some_and(|reason| super::transport_loss::is_transport_domain_reason(reason));
431        if !transport_domain
432            && let Err(error) = self.transport_losses.clear(workflow_id, activity_id)
433        {
434            tracing::warn!(
435                workflow_id = %workflow_id,
436                activity_id = %activity_id,
437                %error,
438                "failed to retire the transport-loss budget for a resolved activity"
439            );
440        }
441        Ok(self.complete(workflow_id, activity_id, run_id, result))
442    }
443
444    /// Build a sink whose transport-loss ledger derives its budget from the
445    /// operator's heartbeat window (see
446    /// [`transport_loss`](crate::worker::transport_loss)).
447    ///
448    /// The window is a CONSTRUCTOR parameter rather than a builder step, and
449    /// that is the point: there is no way to obtain a `PendingActivities`
450    /// without naming the window, so a wiring cannot supply everything else and
451    /// silently inherit a zero budget. The previous shape —
452    /// `default().with_heartbeat_window(w)` — was two calls where the second was
453    /// omissible, and nothing in the test suite could tell the wired world from
454    /// the unwired one.
455    #[must_use]
456    pub fn new(heartbeat_window: std::time::Duration) -> Self {
457        Self {
458            pending: Arc::default(),
459            completion_fences: CompletionFences::default(),
460            outbox_delivery: Arc::default(),
461            lease_recorder: super::lease_record::LeaseRecorderSeam::default(),
462            transport_losses: super::transport_loss::TransportLossLedger::new(heartbeat_window),
463        }
464    }
465
466    /// The shared lease-record seam, for an outbox dispatcher built over the
467    /// same pending set to record through.
468    #[must_use]
469    pub fn lease_recorder(&self) -> super::lease_record::LeaseRecorderSeam {
470        self.lease_recorder.clone()
471    }
472
473    /// Install the lease recorder once the engine exists; a duplicate install
474    /// keeps the first and is logged by the seam.
475    pub fn set_lease_recorder(
476        &self,
477        recorder: Arc<dyn super::lease_record::ActivityLeaseRecorder>,
478        metrics: Option<crate::observability::Metrics>,
479    ) {
480        self.lease_recorder.install(recorder, metrics);
481    }
482}
483
484impl ActivityCompletionSink for PendingActivities {
485    /// Park one in-flight dispatch for restart recovery (#207): resolve the
486    /// matched waiter with the ephemeral parked sentinel, and nothing else.
487    ///
488    /// This resolution is MANDATORY, not an optimization: the default
489    /// `ActivityDispatcher::dispatch_async` runs the dispatcher's blocking
490    /// `std::sync::mpsc::recv()` on tokio's blocking pool, and tokio `Runtime`
491    /// drop joins blocking threads — an unresolved waiter would wedge process
492    /// exit indefinitely. An unmatched dispatch (already resolved by another
493    /// path) is a no-op: a park is NEVER routed to the outbox delivery
494    /// callback, because it is not a failure and must never reach a workflow.
495    fn park_activity(
496        &self,
497        workflow_id: &WorkflowId,
498        activity_id: &ActivityId,
499    ) -> Result<(), ServerError> {
500        // A park retires the execution site, so it takes the whole current
501        // generation — including a redelivery's sibling token, whose worker is
502        // being parked for the same restart.
503        self.completion_fences
504            .revoke_current(workflow_id, activity_id)?;
505        let matched = self
506            .pending
507            .remove(&(workflow_id.clone(), activity_id.clone()));
508        if let Some((_, waiter)) = matched {
509            // A send failure means the waiter side already dropped its
510            // receiver (the dispatch is being cleaned up concurrently) —
511            // benign: there is no thread left to unblock.
512            let _ = waiter
513                .sender
514                .send(Err(aion::PARKED_ACTIVITY_REASON.to_owned()));
515        }
516        Ok(())
517    }
518
519    fn complete_activity(&self, completion: ActivityCompletion) -> Result<(), ServerError> {
520        self.complete_activity_after_accept(completion, || Ok(()))
521    }
522}
523
524/// Dispatcher that routes `run_activity` NIF calls to connected workers.
525///
526/// Synchronous interface — uses `try_send` for the task channel and
527/// `std::sync::mpsc::Receiver::recv` for the response. Callers on a
528/// multi-thread tokio runtime are detected and moved into
529/// `tokio::task::block_in_place` so the blocking wait never starves the
530/// runtime tasks that flush the worker stream (see the module docs).
531pub struct WorkerActivityDispatcher {
532    registry: ConnectedWorkerRegistry,
533    namespace: String,
534    pending: PendingActivities,
535    heartbeat_tracker: HeartbeatTracker,
536    drain_state: DrainState,
537    tokio_handle: Option<tokio::runtime::Handle>,
538    /// NOI-6 attempt→owner back-index. When installed, each liminal-delivered
539    /// dispatch binds its `(workflow, activity, attempt)` to the owning worker
540    /// for the dispatch's lifetime, so the intervention router (and the ops
541    /// console's live-attempts enumeration) can see and target it. `None`
542    /// (isolated tests) binds nothing.
543    attempt_owners: Option<super::intervention::AttemptOwnerIndex>,
544    /// R1 service policies and the two service clocks. Default: `strict` with
545    /// no clocks — refuse the structurally unservable, wait (loudly) for
546    /// everything else, and invent no deadline the operator never wrote.
547    queue_service: QueueServiceConfig,
548    /// Deployed queue declarations, installed once the engine exists. Answers
549    /// `Unknown` until then, and `Unknown` never refuses anything.
550    queue_declarations: QueueDeclarationSource,
551    /// Live unserved-queue state a parked dispatch publishes itself into.
552    queue_state: QueueServiceState,
553    /// Cluster-event publisher an unbounded park announces itself on (#266
554    /// T4). `None` (isolated tests) loses only the pushed echo; the WARN and
555    /// the queryable state above cannot be switched off by wiring.
556    cluster_publisher: Option<crate::cluster_publisher::ClusterEventPublisher>,
557}
558
559impl std::fmt::Debug for WorkerActivityDispatcher {
560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561        f.debug_struct("WorkerActivityDispatcher")
562            .field("namespace", &self.namespace)
563            .finish_non_exhaustive()
564    }
565}
566
567impl WorkerActivityDispatcher {
568    /// Build a dispatcher for the given namespace, worker registry, and
569    /// liveness tracker.
570    ///
571    /// The tracker must be the same instance the worker stream handler and
572    /// shutdown coordinator share: the unbounded completion wait relies on
573    /// stream teardown sweeping this tracker's in-flight entries to fail
574    /// dispatches whose worker was lost.
575    #[must_use]
576    pub fn new(
577        registry: ConnectedWorkerRegistry,
578        namespace: impl Into<String>,
579        heartbeat_tracker: HeartbeatTracker,
580    ) -> Self {
581        Self {
582            registry,
583            namespace: namespace.into(),
584            // Derived from the tracker this constructor is already handed,
585            // which already carries the operator's window: the dispatcher
586            // cannot be given a heartbeat window and a mismatched budget.
587            pending: PendingActivities::new(heartbeat_tracker.heartbeat_window()),
588            heartbeat_tracker,
589            drain_state: DrainState::default(),
590            tokio_handle: None,
591            attempt_owners: None,
592            queue_service: QueueServiceConfig::default(),
593            queue_declarations: QueueDeclarationSource::default(),
594            queue_state: QueueServiceState::default(),
595            cluster_publisher: None,
596        }
597    }
598
599    /// Share the deployment-global cluster-event publisher so a dispatch
600    /// parked with no availability deadline is announced on the operator's
601    /// real-time channel, not only in the log (#266 T4).
602    #[must_use]
603    pub fn with_cluster_publisher(
604        mut self,
605        cluster_publisher: crate::cluster_publisher::ClusterEventPublisher,
606    ) -> Self {
607        self.cluster_publisher = Some(cluster_publisher);
608        self
609    }
610
611    /// Share the operator's R1 queue-service settings: the default policy, the
612    /// written per-queue `durable_pending` opt-ins, and the two clocks.
613    #[must_use]
614    pub fn with_queue_service(mut self, queue_service: QueueServiceConfig) -> Self {
615        self.queue_service = queue_service;
616        self
617    }
618
619    /// Share the queue-declaration source the boot path fills in once the
620    /// engine exists (the handle is cloneable; installing on any clone is
621    /// visible here).
622    #[must_use]
623    pub fn with_queue_declarations(mut self, queue_declarations: QueueDeclarationSource) -> Self {
624        self.queue_declarations = queue_declarations;
625        self
626    }
627
628    /// Share the queue-service state so the server can read which addresses are
629    /// unserved and which runs are parked on them.
630    #[must_use]
631    pub fn with_queue_state(mut self, queue_state: QueueServiceState) -> Self {
632        self.queue_state = queue_state;
633        self
634    }
635
636    /// Share the server's NOI-6 attempt→owner back-index so liminal-delivered
637    /// dispatches are visible (and targetable) to the intervention router for
638    /// exactly as long as they are in flight. The production boot passes
639    /// `ServerState`'s index — the SAME instance `intervenable_attempts` and
640    /// `intervene` read — or the console's live-attempts list stays empty for
641    /// every bridge-dispatched agent step.
642    #[must_use]
643    pub fn with_attempt_owners(
644        mut self,
645        attempt_owners: super::intervention::AttemptOwnerIndex,
646    ) -> Self {
647        self.attempt_owners = Some(attempt_owners);
648        self
649    }
650
651    /// The completion sink this dispatcher classifies worker deaths through.
652    ///
653    /// Exposed so the transport-loss budget a wiring actually ends up with is
654    /// observable, rather than being a property only prose asserts.
655    #[must_use]
656    pub const fn pending(&self) -> &PendingActivities {
657        &self.pending
658    }
659
660    /// Share a caller-supplied pending-activities tracker.
661    #[must_use]
662    pub fn with_pending(mut self, pending: PendingActivities) -> Self {
663        self.pending = pending;
664        self
665    }
666
667    /// Share the server drain gate.
668    #[must_use]
669    pub fn with_drain_state(mut self, drain_state: DrainState) -> Self {
670        self.drain_state = drain_state;
671        self
672    }
673
674    /// Share the server runtime handle for sync history writes from dirty NIF threads.
675    #[must_use]
676    pub fn with_tokio_handle(mut self, tokio_handle: tokio::runtime::Handle) -> Self {
677        self.tokio_handle = Some(tokio_handle);
678        self
679    }
680}
681
682impl WorkerActivityDispatcher {
683    /// The drain gate for one dispatch, and the single place a draining server's
684    /// refusal is turned into a reason the engine acts on.
685    ///
686    /// The reason handed back is the #207 parked sentinel
687    /// ([`aion::PARKED_ACTIVITY_REASON`]), NOT the gate's own message. A
688    /// dispatch the drain refused never reached a worker and never ran, which is
689    /// precisely the state that sentinel was minted for: record nothing, deliver
690    /// nothing, and let post-restart replay re-dispatch the dangling ordinal —
691    /// the kill -9 convergence `shutdown::ShutdownOutcome::Parked` documents.
692    ///
693    /// Both alternatives are wrong here, and neither is a style preference:
694    ///
695    /// - the *unprefixed* gate message this used to return is classified as a
696    ///   terminal action failure, so a routine deploy records `ActivityFailed`
697    ///   (and, with no authored retry, `WorkflowFailed`) for work nobody
698    ///   attempted — the synthesized failure #207 exists to abolish;
699    /// - the TRANSPORT-loss class (`lost:`) is attempt-neutral but re-dispatches
700    ///   the SAME attempt live and in-process, which on a draining server means
701    ///   refuse → re-dispatch → refuse, spinning against the very gate that
702    ///   refused it while the process is trying to exit.
703    ///
704    /// The operator-facing detail is not lost: the gate's own message is what is
705    /// logged here. Only the engine-facing classification is the sentinel.
706    fn ensure_accepting(
707        &self,
708        namespace: &str,
709        activity_type: &str,
710        workflow_id: &WorkflowId,
711        activity_id: &ActivityId,
712        worker_id: Option<WorkerId>,
713    ) -> Result<(), String> {
714        self.drain_state
715            .ensure_accepting(namespace, activity_type)
716            .map_err(|error| {
717                log_worker_error(
718                    "WorkerDispatch",
719                    namespace,
720                    activity_type,
721                    workflow_id,
722                    activity_id,
723                    worker_id,
724                    &error.to_string(),
725                );
726                aion::PARKED_ACTIVITY_REASON.to_owned()
727            })
728    }
729
730    /// Select a worker for the address, or refuse with a typed R1 reason.
731    ///
732    /// The wait itself lives in [`super::queue_service::wait`]: every selection
733    /// miss is classified against the deployed contract records and the live
734    /// poller census, published to the queue-service state, and stated at WARN.
735    /// This method owns only the two things the seam cannot delegate — the
736    /// drain gate and the runtime plumbing of one bounded park.
737    ///
738    /// The [`DispatchReservation`] returned beside the worker is the chosen
739    /// worker's capacity slot, already claimed. The caller must hold it until
740    /// the heartbeat tracker has taken the count over; dropping it early
741    /// re-opens the window in which a simultaneous dispatch re-picks a worker
742    /// this one has already filled.
743    fn select_worker_or_wait(
744        &self,
745        address: &ServiceAddress,
746        workflow_id: &WorkflowId,
747        activity_id: &ActivityId,
748    ) -> Result<(WorkerHandle, DispatchReservation), String> {
749        let wait = ServiceWait {
750            registry: &self.registry,
751            declarations: &self.queue_declarations,
752            config: &self.queue_service,
753            state: &self.queue_state,
754            address,
755            workflow_id,
756            activity_id,
757            publisher: self.cluster_publisher.as_ref(),
758        };
759        let mut accepting = || {
760            self.ensure_accepting(
761                &address.namespace,
762                &address.activity_type,
763                workflow_id,
764                activity_id,
765                None,
766            )
767        };
768        let mut park = |arrival: WorkerArrival, budget: Option<Duration>| {
769            self.park_for_worker(arrival, budget);
770        };
771        select_worker_or_refuse(&wait, &mut accepting, &mut park).map_err(|refusal| {
772            let reason = refusal.reason_string();
773            // The drain gate already logged its own refusal at its own site;
774            // logging it twice would double-count the incident.
775            if !matches!(refusal, SelectionRefusal::NotAccepting { .. }) {
776                let error_type = match refusal {
777                    SelectionRefusal::Unavailable(_) => "WorkerUnavailable",
778                    _ => "WorkerRegistry",
779                };
780                log_worker_error(
781                    error_type,
782                    &address.namespace,
783                    &address.activity_type,
784                    workflow_id,
785                    activity_id,
786                    None,
787                    &reason,
788                );
789            }
790            reason
791        })
792    }
793
794    /// Wait once on the `arrival` subscription — bounded by `budget` when one is
795    /// given, and bounded in BOTH arms by the server's own shutdown.
796    ///
797    /// `arrival` was taken by [`select_worker_or_refuse`] BEFORE the selection
798    /// that missed, which is why this method is handed one rather than taking
799    /// its own. Subscribing here would be too late: the registry's wake sources
800    /// store no permit, so a worker that registered during that selection would
801    /// already have fired into an empty waiter list and this park would sleep
802    /// through it.
803    ///
804    /// The arrival wait races the drain latch, and that race is the whole point.
805    /// Without it a `None` budget — the default, because no schedule-to-start
806    /// deadline is configured unless an operator writes one — parked this
807    /// dispatch on a `spawn_blocking` thread with nothing left that could ever
808    /// wake it: the drain gate is consulted once per selection iteration, and
809    /// the iteration was stuck inside this wait. Tokio's runtime `Drop` joins
810    /// the blocking pool, so `aion server` could not exit even after `main`
811    /// returned an exit code — it kept the store's writer lock and blocked the
812    /// next deploy into the same data directory. The captured stack of a real
813    /// one is `docs/evidence/samples/aion-server-hang-after-drain-15202.sample`.
814    /// The `Some(budget)` arm carried the same defect at the scale of the
815    /// budget: exit was delayed by up to the whole availability deadline.
816    ///
817    /// Racing the latch is the fix rather than capping the wait, because the
818    /// wait is legitimately unbounded while the server runs: a queue no worker
819    /// serves yet is a fleet condition an operator resolves by starting a
820    /// worker, and a deadline invented here would refuse work nobody asked to
821    /// have refused.
822    ///
823    /// Waking on the latch decides nothing by itself. The selection loop
824    /// re-selects, misses, and consults the SAME drain gate it always
825    /// consulted — [`Self::ensure_accepting`] — which is where the refusal and
826    /// its classification are produced. This seam gains no second opinion about
827    /// draining; it only stops being unwakeable.
828    fn park_for_worker(&self, arrival: WorkerArrival, budget: Option<Duration>) {
829        let handle = self
830            .tokio_handle
831            .clone()
832            .or_else(|| tokio::runtime::Handle::try_current().ok());
833        let Some(handle) = handle else {
834            // No runtime in reach (a plain OS thread in an isolated test):
835            // sleep the seam's park interval, clamped to the remaining budget.
836            // Bounded by construction, so the loop re-consults the drain gate
837            // within one interval and needs no signal to race.
838            //
839            // `arrival` is DROPPED unawaited here, deliberately: there is no
840            // executor to await it on. That is safe only because this arm is
841            // bounded by construction — the loop re-selects within one interval
842            // whether or not any wake ever fires — and it is the one place the
843            // `park` contract permits discarding the subscription. Its `Drop`
844            // unlinks the waiter it enabled.
845            std::thread::sleep(
846                budget.map_or(PARK_POLL_INTERVAL, |budget| budget.min(PARK_POLL_INTERVAL)),
847            );
848            return;
849        };
850        handle.block_on(async {
851            let raced = async {
852                tokio::select! {
853                    () = arrival => {}
854                    () = self.drain_state.wait_for_drain() => {}
855                }
856            };
857            match budget {
858                None => raced.await,
859                Some(budget) => {
860                    // Timeout is not failure here: the caller re-classifies and
861                    // decides whether the clock has run out.
862                    drop(tokio::time::timeout(budget, raced).await);
863                }
864            }
865        });
866    }
867
868    fn track_worker_task(
869        &self,
870        worker_id: WorkerId,
871        activity_type: &str,
872        workflow_id: &WorkflowId,
873        activity_id: &ActivityId,
874        attempt: u32,
875        // The token this dispatch is fenced by and the capacity slot it holds,
876        // together because they are handed over together: the tracker commits
877        // the slot in the same call that records the token.
878        lease: (CompletionToken, DispatchReservation),
879    ) -> Result<(), String> {
880        let (completion_token, reservation) = lease;
881        self.heartbeat_tracker
882            .track_task(
883                worker_id,
884                InFlightActivity {
885                    workflow_id: workflow_id.clone(),
886                    activity_id: activity_id.clone(),
887                    // The engine-provided attempt this delivery carries — the
888                    // same one stamped on the wire task and bound into the
889                    // NOI-6 attempt→owner index, so the tracker, the wire, and
890                    // the owner index all name one attempt.
891                    attempt,
892                    completion_token,
893                },
894                Instant::now(),
895                &self.registry,
896                // HANDED OVER in one act: the tracker commits this slot and
897                // skips its own increment, so the count never reads higher than
898                // the work this worker holds. Two acts — track, then release the
899                // reservation — left a window in which a concurrent selection
900                // was refused a slot that was free.
901                Some(reservation),
902            )
903            .map_err(|error| {
904                let reason = error.to_string();
905                log_worker_error(
906                    "WorkerHeartbeatTracker",
907                    &self.namespace,
908                    activity_type,
909                    workflow_id,
910                    activity_id,
911                    Some(worker_id),
912                    &reason,
913                );
914                reason
915            })
916    }
917
918    fn cleanup_activity(
919        &self,
920        worker_id: WorkerId,
921        workflow_id: &WorkflowId,
922        activity_id: &ActivityId,
923        attempt: u32,
924        completion_token: &CompletionToken,
925    ) {
926        // Own-attempt-only, like the token revocation below: a dispatch may
927        // tear down only the waiter slot it installed. After a higher attempt
928        // takes the site over (aion#195) this entry is no longer this
929        // dispatch's to remove.
930        self.pending
931            .pending
932            .remove_if(&(workflow_id.clone(), activity_id.clone()), |_, waiter| {
933                waiter.attempt == attempt
934            });
935        // Own-token-only: an undelivered task withdraws the authorization it
936        // was given and never the sibling a concurrently redelivered dispatch
937        // handed to a worker that is still executing.
938        if let Err(error) =
939            self.pending
940                .completion_fences
941                .revoke(workflow_id, activity_id, completion_token)
942        {
943            tracing::error!(
944                workflow_id = %workflow_id,
945                activity_id = %activity_id,
946                %error,
947                "failed to revoke undelivered activity generation"
948            );
949        }
950        let _ = self.heartbeat_tracker.complete_task(
951            worker_id,
952            workflow_id,
953            activity_id,
954            &self.registry,
955        );
956        self.drain_state.notify_activity_drained();
957    }
958
959    /// Deliver one dispatched task to the selected worker over ITS transport.
960    ///
961    /// The bridge is transport-agnostic at this seam: selection
962    /// ([`Self::select_worker_or_wait`]) already treats every registry member
963    /// identically, and this match delivers on whichever [`WorkerDelivery`] leg
964    /// the worker registered with — the gRPC stream `mpsc` push, or the liminal
965    /// server-push on the worker's existing connection. Both legs resolve
966    /// through the SAME pending map, so `await_activity_result` is oblivious to
967    /// the transport.
968    fn send_activity_task(
969        &self,
970        worker: &WorkerHandle,
971        task: ProtoActivityTask,
972        address: &ServiceAddress,
973        handoff: &super::lease_record::LeaseHandoff,
974    ) -> Result<(), String> {
975        let workflow_id = &handoff.key().workflow_id;
976        let activity_id = &handoff.key().activity_id;
977        let completion_token = handoff.token();
978        match worker.delivery() {
979            WorkerDelivery::Grpc(sender) => {
980                let worker_id = worker.id();
981                let mut accepting = || {
982                    self.ensure_accepting(
983                        &address.namespace,
984                        &address.activity_type,
985                        workflow_id,
986                        activity_id,
987                        Some(worker_id),
988                    )
989                };
990                let handed_over = deliver_within_schedule_to_start(
991                    sender,
992                    WorkerMessage::ActivityTask(Box::new(task)),
993                    self.queue_service.schedule_to_start_timeout,
994                    &mut accepting,
995                );
996                let Err(refusal) = handed_over else {
997                    // The stream took the frame: the worker holds the attempt
998                    // from here, and the lease is recorded before this call
999                    // returns to wait on the result (WA-010 R3).
1000                    handoff.accepted_blocking(self.tokio_handle.as_ref());
1001                    return Ok(());
1002                };
1003                self.cleanup_activity(
1004                    worker_id,
1005                    workflow_id,
1006                    activity_id,
1007                    handoff.key().attempt,
1008                    completion_token,
1009                );
1010                let (error_type, reason) = self.hand_off_failure(&refusal, address);
1011                if !matches!(refusal, DeliveryRefusal::NotAccepting { .. }) {
1012                    log_worker_error(
1013                        error_type,
1014                        &address.namespace,
1015                        &address.activity_type,
1016                        workflow_id,
1017                        activity_id,
1018                        Some(worker_id),
1019                        &reason,
1020                    );
1021                }
1022                Err(reason)
1023            }
1024            // The liminal leg has no bounded intake queue to saturate: the push
1025            // either enqueues on the worker's live connection or fails
1026            // synchronously because that connection is already gone. There is
1027            // therefore no schedule-to-start window to apply here, and reporting
1028            // `SATURATED` for a push failure would name a condition that did not
1029            // happen.
1030            #[cfg(feature = "liminal-transport")]
1031            WorkerDelivery::Liminal(delivery) => self.send_liminal_activity_task(
1032                worker.id(),
1033                delivery,
1034                task,
1035                &address.activity_type,
1036                handoff,
1037            ),
1038        }
1039    }
1040
1041    /// Render one hand-off refusal into its log class and failure reason.
1042    ///
1043    /// `SATURATED` is the only one that becomes a typed [`WorkerUnavailable`]:
1044    /// a compatible worker WAS live and would not take the task inside the
1045    /// schedule-to-start clock. A full intake with no clock configured, and a
1046    /// closed transport, keep the failure they always had.
1047    fn hand_off_failure(
1048        &self,
1049        refusal: &DeliveryRefusal,
1050        address: &ServiceAddress,
1051    ) -> (&'static str, String) {
1052        match refusal {
1053            DeliveryRefusal::Saturated { waited } => {
1054                // A census failure must not swallow the refusal: report the
1055                // saturation with an empty census and say why it is empty.
1056                let census = self
1057                    .registry
1058                    .pool_census(
1059                        &address.namespace,
1060                        &address.task_queue,
1061                        &address.activity_type,
1062                        address.node.as_deref(),
1063                    )
1064                    .unwrap_or_else(|error| {
1065                        tracing::error!(
1066                            namespace = %address.namespace,
1067                            task_queue = %address.task_queue,
1068                            activity_type = %address.activity_type,
1069                            %error,
1070                            "poller census failed while reporting a saturated queue; \
1071                             the refusal carries an empty census"
1072                        );
1073                        PoolCensus::default()
1074                    });
1075                let unavailable = WorkerUnavailable {
1076                    reason: QueueServiceReason::Saturated,
1077                    clock: Some(ExpiredClock::ScheduleToStart),
1078                    waited: *waited,
1079                    address: address.clone(),
1080                    census,
1081                };
1082                ("WorkerUnavailable", unavailable.reason_string())
1083            }
1084            DeliveryRefusal::Full => (
1085                "WorkerChannelClosed",
1086                "worker task channel full or closed: no available capacity".to_owned(),
1087            ),
1088            DeliveryRefusal::Closed => (
1089                "WorkerChannelClosed",
1090                "worker task channel full or closed: channel closed".to_owned(),
1091            ),
1092            DeliveryRefusal::NotAccepting { reason } => ("WorkerDispatch", reason.clone()),
1093        }
1094    }
1095
1096    /// Deliver one dispatched task to a liminal-connected worker: push the SAME
1097    /// wire frame the outbox liminal path pushes (a
1098    /// [`DispatchRequest`](super::liminal_transport::DispatchRequest) — the
1099    /// worker's serve loop cannot tell a bridge dispatch from an outbox row) and
1100    /// hand the correlated-reply awaiter to a dedicated router thread that
1101    /// resolves this dispatch's pending entry exactly like a gRPC completion.
1102    ///
1103    /// The wire carries the SAME engine-provided `attempt` and `labels` the gRPC
1104    /// arm's `ActivityTask` carries (a retry over liminal executes with the real
1105    /// attempt, not a re-stamped first delivery), plus the server's heartbeat
1106    /// window so the worker's automatic liveness pump keeps this TRACKED
1107    /// dispatch alive under the #176 expiry sweeper. It carries the engine's
1108    /// concrete run identity, generation proof, and stable effect key exactly
1109    /// like the gRPC bridge task.
1110    ///
1111    /// A successful push also binds the attempt into the NOI-6 attempt→owner
1112    /// back-index (when installed) with the SAME `(workflow, activity, attempt)`
1113    /// key the worker stamps its intervention session with, exactly as the
1114    /// outbox liminal arm binds each row dispatch — so the ops console can
1115    /// enumerate this live attempt and route interventions to its worker. The
1116    /// binding is released when the reply router exits (reply, abandonment, or
1117    /// disconnect — every path). The gRPC arm carries no bind because the agent
1118    /// harness seam exists only on the liminal worker transport.
1119    #[cfg(feature = "liminal-transport")]
1120    fn send_liminal_activity_task(
1121        &self,
1122        worker_id: WorkerId,
1123        delivery: &super::liminal_transport::LiminalWorkerDelivery,
1124        task: ProtoActivityTask,
1125        activity_type: &str,
1126        handoff: &super::lease_record::LeaseHandoff,
1127    ) -> Result<(), String> {
1128        let workflow_id = &handoff.key().workflow_id;
1129        let activity_id = &handoff.key().activity_id;
1130        let completion_token =
1131            CompletionToken::from_wire(workflow_id, activity_id, task.completion_token.clone())
1132                .map_err(|error| error.to_string())?;
1133        let heartbeat_window_ms =
1134            u64::try_from(self.heartbeat_tracker.heartbeat_window().as_millis())
1135                .unwrap_or(u64::MAX);
1136        let attempt = task.attempt;
1137        // The run is resolved BEFORE the dispatch is built, because everything
1138        // downstream is keyed on it: the worker refuses a run-less dispatch
1139        // envelope outright, the attempt-owner binding below needs it to name a
1140        // generation, and the attempt's transcript is written under it. Refusing
1141        // here reports the fault against the dispatch that has it, rather than
1142        // shipping a frame whose only possible outcome is the worker's rejection.
1143        let run_id = task
1144            .run_id
1145            .map(RunId::try_from)
1146            .transpose()
1147            .map_err(|error| error.to_string())?
1148            .ok_or_else(|| {
1149                "activity task run id is missing; refusing to dispatch an unidentified run"
1150                    .to_owned()
1151            })?;
1152        let request = super::liminal_transport::DispatchRequest {
1153            activity_type: activity_type.to_owned(),
1154            workflow_id: workflow_id.clone(),
1155            ordinal: activity_id.sequence_position(),
1156            run_id: Some(run_id.clone()),
1157            attempt,
1158            completion_token: task.completion_token,
1159            idempotency_key: task.idempotency_key,
1160            labels: task.labels.into_iter().collect(),
1161            heartbeat_window_ms,
1162            input: task.input.map(|payload| payload.bytes).unwrap_or_default(),
1163        };
1164        // A push-enqueue failure means the worker's connection was already gone
1165        // at push time — the same synchronous-failure contract as a closed gRPC
1166        // stream channel above.
1167        let awaiter = match delivery.push_dispatch(&request) {
1168            Ok(awaiter) => awaiter,
1169            Err(error) => {
1170                let reason = format!("worker liminal push failed: {error}");
1171                self.cleanup_activity(
1172                    worker_id,
1173                    workflow_id,
1174                    activity_id,
1175                    attempt,
1176                    &completion_token,
1177                );
1178                log_worker_error(
1179                    "WorkerChannelClosed",
1180                    &self.namespace,
1181                    activity_type,
1182                    workflow_id,
1183                    activity_id,
1184                    Some(worker_id),
1185                    &reason,
1186                );
1187                return Err(reason);
1188            }
1189        };
1190        // NOI-6: the attempt is live on `worker_id` from this push until the
1191        // router resolves it — bind it for exactly that window (the guard is
1192        // dropped when the router thread exits).
1193        // The push was acknowledged: the worker holds the attempt, and the
1194        // lease is recorded before the reply router is even spawned (WA-010
1195        // R3) — the reply IS the completion, so nothing may wait for it first.
1196        handoff.accepted_blocking(self.tokio_handle.as_ref());
1197        let owner_binding = self.attempt_owners.as_ref().map(|owners| {
1198            super::liminal_transport::AttemptOwnerGuard::bind(
1199                owners.clone(),
1200                super::intervention::AttemptKey::new(
1201                    workflow_id.clone(),
1202                    run_id.clone(),
1203                    activity_id.clone(),
1204                    attempt,
1205                ),
1206                worker_id,
1207            )
1208        });
1209        self.spawn_liminal_reply_router(
1210            worker_id,
1211            awaiter,
1212            workflow_id,
1213            activity_id,
1214            &completion_token,
1215            owner_binding,
1216        );
1217        Ok(())
1218    }
1219
1220    /// Waits (on a dedicated router thread, bounded by the dispatch's own
1221    /// lifetime) for the worker's correlated
1222    /// [`DispatchResponse`](super::liminal_transport::DispatchResponse) and
1223    /// re-enters it through the SAME completion bookkeeping the gRPC inbound
1224    /// stream applies (`process_inbound` in `worker_grpc.rs`): clear the
1225    /// in-flight liveness entry, wake any drain waiter, then resolve the
1226    /// bridge's pending map — result, failure, and retryable classification
1227    /// identical (the worker encodes the `retryable:`/`terminal:` reason
1228    /// vocabulary on the wire). An unmatched (already-resolved) REAL reply
1229    /// routes through the outbox delivery callback exactly like a late gRPC
1230    /// result.
1231    ///
1232    /// The #176 heartbeat sweeper covers this dispatch exactly as it covers a
1233    /// gRPC one: the dispatch is tracked in the shared [`HeartbeatTracker`] and
1234    /// the worker's runtime pumps automatic liveness beats over the reserved
1235    /// liminal channel (`WORKER_LIVENESS_CHANNEL`), so a healthy worker running
1236    /// an over-window activity is never falsely expired while a wedged one
1237    /// still is. Prompt worker-DEATH detection additionally rides the
1238    /// connection itself — the awaiter wakes with the typed Disconnected error
1239    /// the moment the connection closes, resolving the SAME retryable
1240    /// lost-worker failure the gRPC stream-teardown sweep reports.
1241    ///
1242    /// Two structural guards mirror the gRPC arm's tracker gating:
1243    ///
1244    /// - A SYNTHESIZED failure (disconnect / receive fault) is delivered only
1245    ///   when this router's own `complete_task` actually retired the tracked
1246    ///   entry — the same "fail only still-tracked tasks" gate
1247    ///   `remove_worker_tasks` gives the gRPC sweeps — so a dispatch already
1248    ///   resolved elsewhere (expiry sweep, shutdown drain, deregistered
1249    ///   fast path) never has a spurious failure injected for an ordinal whose
1250    ///   retry may be live on another worker.
1251    /// - The wait itself ends one reply-poll after the tracked entry
1252    ///   disappears, so an abandoned dispatch never parks this thread for the
1253    ///   remaining life of the worker's connection. A real reply arriving
1254    ///   AFTER that exit is dropped (the resolving path owns the ordinal — its
1255    ///   retry re-executes); this is the one deliberate divergence from the
1256    ///   gRPC arm, whose shared stream task routes any late result to the
1257    ///   outbox callback, and it is the safer half of the trade because a
1258    ///   stale attempt's result can never resolve a newer attempt's entry.
1259    #[cfg(feature = "liminal-transport")]
1260    fn spawn_liminal_reply_router(
1261        &self,
1262        worker_id: WorkerId,
1263        awaiter: liminal_server::server::connection::PushReplyAwaiter,
1264        workflow_id: &WorkflowId,
1265        activity_id: &ActivityId,
1266        completion_token: &CompletionToken,
1267        owner_binding: Option<super::liminal_transport::AttemptOwnerGuard>,
1268    ) {
1269        let pending = self.pending.clone();
1270        let heartbeat_tracker = self.heartbeat_tracker.clone();
1271        let registry = self.registry.clone();
1272        let drain_state = self.drain_state.clone();
1273        let workflow_id = workflow_id.clone();
1274        let activity_id = activity_id.clone();
1275        let completion_token = completion_token.clone();
1276        std::thread::spawn(move || {
1277            // Owns the NOI-6 attempt binding for the dispatch's lifetime: it
1278            // drops (releasing the back-index entry) when this router exits,
1279            // on every path — reply, abandonment, disconnect, or panic.
1280            let _owner_binding = owner_binding;
1281            route_liminal_reply(
1282                &pending,
1283                &heartbeat_tracker,
1284                &registry,
1285                &drain_state,
1286                &awaiter,
1287                (worker_id, &workflow_id, &activity_id, &completion_token),
1288            );
1289        });
1290    }
1291
1292    /// Block until the dispatch terminates (see the module docs for the
1293    /// exhaustive termination list). The wait is deliberately unbounded:
1294    /// the engine imposes no activity timeout of its own.
1295    fn await_activity_result(
1296        &self,
1297        context: &ActivityDispatchContext<'_>,
1298        rx: &SyncReceiver,
1299    ) -> Result<String, String> {
1300        // Close the dispatch/disconnect race before blocking. A worker whose
1301        // stream tore down *before* this dispatch tracked its task was swept
1302        // without this entry, so nothing would ever deliver through `rx`.
1303        // `fail_lost_worker` deregisters before it collects tasks, and this
1304        // dispatch tracked its task before sending, so: if the worker is
1305        // still registered here, any later sweep is guaranteed to include
1306        // this task and unblock the `recv` below.
1307        match self.registry.is_registered(context.worker_id) {
1308            Ok(true) => {}
1309            Ok(false) => {
1310                // A sweep that did include this task may have delivered
1311                // already; prefer its verdict (or a genuine result that
1312                // raced the disconnect) over fabricating one.
1313                if let Ok(result) = rx.try_recv() {
1314                    return self.deliver_result(context, result);
1315                }
1316                self.cleanup_activity(
1317                    context.worker_id,
1318                    context.workflow_id,
1319                    context.activity_id,
1320                    context.attempt,
1321                    &context.completion_token,
1322                );
1323                // TRANSPORT domain, classified through the shared ledger: the
1324                // activity never executed, so the failure never wears the
1325                // action's retry vocabulary and is re-dispatched
1326                // attempt-neutrally until the transport's own budget is spent.
1327                let reason = self.pending.classify_worker_loss(
1328                    context.workflow_id,
1329                    context.activity_id,
1330                    context.worker_id,
1331                );
1332                log_worker_error(
1333                    "WorkerLost",
1334                    &self.namespace,
1335                    context.activity_type,
1336                    context.workflow_id,
1337                    context.activity_id,
1338                    Some(context.worker_id),
1339                    &reason,
1340                );
1341                return Err(reason);
1342            }
1343            Err(error) => {
1344                self.cleanup_activity(
1345                    context.worker_id,
1346                    context.workflow_id,
1347                    context.activity_id,
1348                    context.attempt,
1349                    &context.completion_token,
1350                );
1351                let reason = format!("worker registry inspection failed: {error}");
1352                log_worker_error(
1353                    "WorkerRegistry",
1354                    &self.namespace,
1355                    context.activity_type,
1356                    context.workflow_id,
1357                    context.activity_id,
1358                    Some(context.worker_id),
1359                    &reason,
1360                );
1361                return Err(reason);
1362            }
1363        }
1364        if let Ok(result) = rx.recv() {
1365            return self.deliver_result(context, result);
1366        }
1367        // Every sender was dropped without completing: a cleanup path
1368        // removed the pending entry. Surface it instead of hanging.
1369        self.cleanup_activity(
1370            context.worker_id,
1371            context.workflow_id,
1372            context.activity_id,
1373            context.attempt,
1374            &context.completion_token,
1375        );
1376        let reason = "activity response channel dropped".to_owned();
1377        log_worker_error(
1378            "WorkerChannelClosed",
1379            &self.namespace,
1380            context.activity_type,
1381            context.workflow_id,
1382            context.activity_id,
1383            Some(context.worker_id),
1384            &reason,
1385        );
1386        Err(reason)
1387    }
1388
1389    fn deliver_result(
1390        &self,
1391        context: &ActivityDispatchContext<'_>,
1392        result: Result<String, String>,
1393    ) -> Result<String, String> {
1394        // Own-attempt-only (aion#195): after a higher attempt takes this site
1395        // over, the superseded dispatch resolving here must not evict the live
1396        // waiter that replaced it.
1397        self.pending.pending.remove_if(
1398            &(context.workflow_id.clone(), context.activity_id.clone()),
1399            |_, waiter| waiter.attempt == context.attempt,
1400        );
1401        // A parked dispatch (#207) is not a failure: the server is draining and
1402        // restart recovery re-dispatches the ordinal. Info, never error — a
1403        // routine deploy must not emit an ActivityFailed log per in-flight
1404        // dispatch (the incident's alarm noise).
1405        if let Err(reason) = &result
1406            && aion::is_parked_reason(reason)
1407        {
1408            tracing::info!(
1409                operation = "activity_dispatch",
1410                namespace = %self.namespace,
1411                workflow_id = %context.workflow_id,
1412                activity_id = %context.activity_id,
1413                activity_type = context.activity_type,
1414                worker_id = ?context.worker_id,
1415                "activity parked for restart recovery"
1416            );
1417            return result;
1418        }
1419        log_activity_completion(context, result.is_ok());
1420        result.inspect_err(|reason| {
1421            log_worker_error(
1422                "ActivityFailed",
1423                &self.namespace,
1424                context.activity_type,
1425                context.workflow_id,
1426                context.activity_id,
1427                Some(context.worker_id),
1428                reason,
1429            );
1430        })
1431    }
1432}
1433
1434impl ActivityDispatcher for WorkerActivityDispatcher {
1435    fn dispatch(&self, request: ActivityDispatch) -> Result<String, String> {
1436        match tokio::runtime::Handle::try_current() {
1437            Ok(handle) => match handle.runtime_flavor() {
1438                tokio::runtime::RuntimeFlavor::MultiThread => {
1439                    // We are inside a tokio runtime (the engine spawns the
1440                    // sync dispatch onto its handle). Hand this worker's
1441                    // scheduler core to another thread before blocking so the
1442                    // stream forwarder woken by our `try_send` can actually
1443                    // run — otherwise it is trapped in this worker's
1444                    // non-stealable LIFO slot for as long as we block.
1445                    tokio::task::block_in_place(|| {
1446                        self.dispatch_blocking(request, &LeaseSignal::none())
1447                    })
1448                }
1449                flavor => Err(format!(
1450                    "activity dispatch blocks the calling thread until the worker responds; \
1451                     a {flavor:?} tokio runtime cannot host that wait because the worker \
1452                     stream forwarder shares its only executor thread and the task could \
1453                     never be delivered — run the engine on a multi-thread tokio runtime"
1454                )),
1455            },
1456            // No tokio context: a beamr scheduler thread or other plain OS
1457            // thread. Blocking here is the designed contract and cannot starve
1458            // the server runtime.
1459            Err(_) => self.dispatch_blocking(request, &LeaseSignal::none()),
1460        }
1461    }
1462
1463    /// The awaited entry, overridden so the LEASE reaches the engine's
1464    /// per-attempt timer.
1465    ///
1466    /// The default runs the synchronous entry above and fires the signal
1467    /// immediately, which is right for a dispatcher that has no lease step.
1468    /// THIS dispatcher has one, and it is separated from the call by an
1469    /// unbounded park: `dispatch_blocking` waits for a worker to exist before
1470    /// anything can be handed over. Firing at the call would put that park
1471    /// inside a bound written to describe execution — the defect — so the
1472    /// signal travels down to the handover instead and fires at the same
1473    /// instant the durable `ActivityLeased` record is made.
1474    ///
1475    /// `spawn_blocking` rather than the default's `dispatch` hop: the blocking
1476    /// pool is exactly the "no scheduler core to give up" condition the
1477    /// `block_in_place` branch above exists to manufacture, so going through it
1478    /// again would be a hop that arranges what is already true.
1479    fn dispatch_async(
1480        self: std::sync::Arc<Self>,
1481        request: ActivityDispatch,
1482        lease: LeaseSignal,
1483    ) -> futures::future::BoxFuture<'static, Result<String, String>> {
1484        Box::pin(async move {
1485            let blocking =
1486                tokio::task::spawn_blocking(move || self.dispatch_blocking(request, &lease));
1487            match blocking.await {
1488                Ok(result) => result,
1489                Err(join_error) => Err(format!("activity dispatch task failed: {join_error}")),
1490            }
1491        })
1492    }
1493}
1494
1495impl WorkerActivityDispatcher {
1496    /// Dispatch the activity and block the calling thread until the worker
1497    /// responds, the worker is declared lost, or the server drains (see the
1498    /// module docs for the exhaustive termination list).
1499    ///
1500    /// The request carries the *real* workflow and activity ids the engine
1501    /// recorded in history, so the worker logs, the pending-completion key,
1502    /// and the heartbeat tracker all correlate directly against the event
1503    /// store. `config` is forwarded by the engine seam but not yet consumed
1504    /// here (the retry executor that reads it is unbuilt).
1505    ///
1506    /// Must never run while the calling thread still owns a tokio scheduler
1507    /// core: the response can only arrive after the runtime's stream
1508    /// forwarder flushes the queued [`WorkerMessage::ActivityTask`] to the
1509    /// worker, so the thread blocking here must not be the one responsible
1510    /// for polling that forwarder. [`ActivityDispatcher::dispatch`] enforces
1511    /// this with `tokio::task::block_in_place`.
1512    fn dispatch_blocking(
1513        &self,
1514        request: ActivityDispatch,
1515        lease: &LeaseSignal,
1516    ) -> Result<String, String> {
1517        let ActivityDispatch {
1518            namespace,
1519            task_queue,
1520            // OPTIONAL within-pool node affinity (NODE-4): `Some(n)` pins this
1521            // dispatch to workers advertising node `n` (require semantics);
1522            // `None` is unpinned and reaches any worker in the pool.
1523            node,
1524            workflow_id,
1525            run_id,
1526            activity_id,
1527            name,
1528            input,
1529            config: _,
1530            attempt,
1531            labels,
1532            // R5 advisory class: engine-side only. The wire carries no such
1533            // field and a worker's behaviour is identical either way — the
1534            // class governs how the ENGINE treats the failure, never how the
1535            // work is done.
1536            advisory: _,
1537        } = request;
1538        let started_at = Instant::now();
1539        self.ensure_accepting(&namespace, &name, &workflow_id, &activity_id, None)?;
1540        let address = ServiceAddress {
1541            namespace: namespace.clone(),
1542            task_queue: task_queue.clone(),
1543            activity_type: name.clone(),
1544            node: node.clone(),
1545        };
1546        // `reservation` holds this worker's capacity slot from the moment it was
1547        // chosen. It is dropped only after `track_then_send` below has handed
1548        // the count to the heartbeat tracker; every `?` between here and there
1549        // releases it on the way out, which is why none of those arms mention
1550        // capacity.
1551        let (worker, reservation) =
1552            self.select_worker_or_wait(&address, &workflow_id, &activity_id)?;
1553        let worker_id = worker.id();
1554        let span = info_span!(
1555            "activity_dispatch",
1556            operation = "activity_dispatch",
1557            namespace = %namespace,
1558            task_queue = %task_queue,
1559            node = node.as_deref(),
1560            workflow_id = %workflow_id,
1561            activity_id = %activity_id,
1562            activity_type = %name,
1563            worker_id = ?worker_id,
1564        );
1565        let _span_guard = span.enter();
1566        self.ensure_accepting(
1567            &namespace,
1568            &name,
1569            &workflow_id,
1570            &activity_id,
1571            Some(worker_id),
1572        )?;
1573
1574        let (completion_token, rx, superseded_attempt) = self
1575            .pending
1576            .insert(workflow_id.clone(), &run_id, activity_id.clone(), attempt)
1577            .map_err(|error| error.to_string())?;
1578        if let Some(held_attempt) = superseded_attempt {
1579            // aion#195: this retry took the site over from a timed-out
1580            // attempt whose responder is still live. Walk the release ladder
1581            // for the corpse BEFORE this attempt's work begins: ask its
1582            // worker to stop, and untrack its heartbeat task so the expiry
1583            // sweep never presents its stale token mid-sweep.
1584            self.release_superseded_attempt(&workflow_id, &activity_id, held_attempt, attempt);
1585        }
1586        // The lease this delivery's acceptance records (WA-010 R3), armed on
1587        // the fences before the push; settled by the handoff whether the push
1588        // lands or not, so a completion never waits on a lease that will not
1589        // come.
1590        let handoff = super::lease_record::LeaseHandoff::arm(
1591            self.pending.lease_recorder(),
1592            super::lease_record::LeaseKey {
1593                workflow_id: workflow_id.clone(),
1594                run_id: run_id.clone(),
1595                activity_id: activity_id.clone(),
1596                attempt,
1597            },
1598            super::lease_record::attribution_for(&worker),
1599            self.pending.completion_fences(),
1600            completion_token.clone(),
1601            // The engine's per-attempt clock starts where the durable lease
1602            // record is made, not where this call began. Both are this
1603            // handoff's job, so it holds both — one anchor by construction,
1604            // rather than two sites that agree only while somebody keeps them
1605            // agreeing.
1606            lease.clone(),
1607        )
1608        .map_err(|error| error.to_string())?;
1609        let task = activity_task(
1610            &name,
1611            &input,
1612            (&workflow_id, &run_id, &activity_id),
1613            attempt,
1614            labels,
1615            &completion_token,
1616        );
1617        // The reservation goes INTO the tracking call: one act, no window in
1618        // which the count reads higher than the truth.
1619        self.track_then_send(
1620            (worker_id, &worker),
1621            task,
1622            &address,
1623            (&name, &workflow_id, &activity_id, attempt),
1624            (&completion_token, &handoff),
1625            reservation,
1626        )?;
1627        let context = ActivityDispatchContext {
1628            namespace: &namespace,
1629            activity_type: &name,
1630            worker_id,
1631            workflow_id: &workflow_id,
1632            activity_id: &activity_id,
1633            attempt,
1634            completion_token,
1635            started_at,
1636        };
1637        self.await_activity_result(&context, &rx)
1638    }
1639
1640    /// Track this dispatch's liveness entry, then push it to its worker.
1641    ///
1642    /// The two are one step because the ORDER between them is load-bearing and
1643    /// the failure path between them is too: the task is tracked BEFORE the
1644    /// push, so a sweep that fires while the push is in flight can always see
1645    /// the dispatch it has to fail; and a tracking failure must undo the
1646    /// arming this dispatch has already done rather than leave a generation
1647    /// armed for work that was never sent.
1648    fn track_then_send(
1649        &self,
1650        worker: (WorkerId, &WorkerHandle),
1651        task: ProtoActivityTask,
1652        address: &ServiceAddress,
1653        site: (&str, &WorkflowId, &ActivityId, u32),
1654        lease: (&CompletionToken, &super::lease_record::LeaseHandoff),
1655        reservation: DispatchReservation,
1656    ) -> Result<(), String> {
1657        let (worker_id, handle) = worker;
1658        let (name, workflow_id, activity_id, attempt) = site;
1659        let (completion_token, handoff) = lease;
1660        if let Err(error) = self.track_worker_task(
1661            worker_id,
1662            name,
1663            workflow_id,
1664            activity_id,
1665            attempt,
1666            (completion_token.clone(), reservation),
1667        ) {
1668            self.cleanup_activity(
1669                worker_id,
1670                workflow_id,
1671                activity_id,
1672                attempt,
1673                completion_token,
1674            );
1675            return Err(error);
1676        }
1677        self.send_activity_task(handle, task, address, handoff)
1678    }
1679
1680    /// Release the superseded attempt a takeover displaced (aion#195).
1681    ///
1682    /// Two acts, both best-effort and both reported: ask the worker still
1683    /// executing the corpse to stop (a request, not a guarantee — see
1684    /// [`super::activity_cancel`]), and untrack its heartbeat task so the
1685    /// expiry sweep never aborts mid-loop on the corpse's fence-refused stale
1686    /// token. Bookkeeping about the corpse must never fail the live retry, so
1687    /// every failure here lands in the log and nowhere else.
1688    fn release_superseded_attempt(
1689        &self,
1690        workflow_id: &WorkflowId,
1691        activity_id: &ActivityId,
1692        held_attempt: u32,
1693        new_attempt: u32,
1694    ) {
1695        let in_flight = match self.heartbeat_tracker.in_flight_for_workflow(workflow_id) {
1696            Ok(in_flight) => in_flight,
1697            Err(error) => {
1698                tracing::error!(
1699                    workflow_id = %workflow_id,
1700                    activity_id = %activity_id,
1701                    held_attempt,
1702                    new_attempt,
1703                    %error,
1704                    "superseded-attempt release could not read the in-flight tracker; \
1705                     the corpse's worker was not asked to stop"
1706                );
1707                return;
1708            }
1709        };
1710        let holders = in_flight
1711            .into_iter()
1712            .filter(|task| &task.activity_id == activity_id && task.attempt == held_attempt);
1713        let mut asked_anyone = false;
1714        for task in holders {
1715            asked_anyone = true;
1716            let delivery = match super::activity_cancel::ask_worker_to_stop(
1717                &self.registry,
1718                task.worker_id,
1719                workflow_id,
1720                activity_id,
1721            ) {
1722                Ok(delivery) => Some(delivery),
1723                Err(error) => {
1724                    // ASKED AND FAILED IS STILL SUPERSEDED. This used to
1725                    // `continue`, which skipped the `complete_task` below and
1726                    // leaked the corpse's capacity slot permanently — the
1727                    // registry count only comes down on completion or
1728                    // deregistration, so a worker whose stop request failed
1729                    // silently shrank by one slot for the life of its
1730                    // registration, and the expiry sweep kept a dispatch nobody
1731                    // would ever answer.
1732                    //
1733                    // The stop request is best-effort by construction (it is a
1734                    // REQUEST, not a guarantee — see `activity_cancel`). Whether
1735                    // it reached the worker changes nothing about the fact this
1736                    // attempt has been superseded, so the bookkeeping proceeds
1737                    // either way and the failed request is reported on its own.
1738                    //
1739                    // NOT COVERED BY A TEST, and worth saying why rather than
1740                    // leaving it to be discovered: `ask_worker_to_stop` returns
1741                    // `Ok` for every reachable outcome — worker gone, transport
1742                    // cannot carry, channel full — and reaches `Err` only when
1743                    // the registry lock is POISONED. Constructing that state
1744                    // means panicking inside the registry's mutex, which would
1745                    // poison it for the rest of the process. The leak this arm
1746                    // fixes was therefore real but confined to a poisoned-lock
1747                    // path; the correction is that the bookkeeping no longer
1748                    // depends on the request having succeeded.
1749                    tracing::warn!(
1750                        workflow_id = %workflow_id,
1751                        activity_id = %activity_id,
1752                        held_attempt,
1753                        worker_id = task.worker_id.value(),
1754                        %error,
1755                        "superseded-attempt release could not ask the holding worker to stop; \
1756                         untracking the corpse anyway so its capacity slot is not leaked"
1757                    );
1758                    None
1759                }
1760            };
1761            if let Err(error) = self.heartbeat_tracker.complete_task(
1762                task.worker_id,
1763                workflow_id,
1764                activity_id,
1765                &self.registry,
1766            ) {
1767                tracing::warn!(
1768                    workflow_id = %workflow_id,
1769                    activity_id = %activity_id,
1770                    held_attempt,
1771                    worker_id = task.worker_id.value(),
1772                    %error,
1773                    "superseded-attempt release could not untrack the corpse's heartbeat task"
1774                );
1775            }
1776            let Some(delivery) = delivery else {
1777                // The stop request itself failed and was reported above. The
1778                // corpse is untracked, its slot is back, and there is no
1779                // delivery outcome to describe.
1780                continue;
1781            };
1782            if delivery.was_requested() {
1783                tracing::info!(
1784                    workflow_id = %workflow_id,
1785                    activity_id = %activity_id,
1786                    held_attempt,
1787                    new_attempt,
1788                    worker_id = task.worker_id.value(),
1789                    "attempt takeover: asked the worker still executing the superseded \
1790                     attempt to stop"
1791                );
1792            } else {
1793                tracing::warn!(
1794                    workflow_id = %workflow_id,
1795                    activity_id = %activity_id,
1796                    held_attempt,
1797                    new_attempt,
1798                    worker_id = task.worker_id.value(),
1799                    outcome = ?delivery,
1800                    "attempt takeover: the superseded attempt's worker could NOT be asked \
1801                     to stop — its work runs on unwitnessed until it next reports, and its \
1802                     completion will be refused as a stale generation"
1803                );
1804            }
1805        }
1806        if !asked_anyone {
1807            tracing::info!(
1808                workflow_id = %workflow_id,
1809                activity_id = %activity_id,
1810                held_attempt,
1811                new_attempt,
1812                "attempt takeover: no tracked worker holds the superseded attempt \
1813                 (already untracked, or its transport records no heartbeat task); \
1814                 its completion token is fence-refused regardless"
1815            );
1816        }
1817    }
1818}
1819
1820/// One failure the reply router FABRICATED (a disconnect, an unservable
1821/// frame, or a receive fault) in the shape [`route_liminal_reply`] settles:
1822/// no run id, the token THIS push dispatched, the reason, and the
1823/// `synthesized` flag set so delivery is gated on the in-flight tracker.
1824#[cfg(feature = "liminal-transport")]
1825fn synthesized_failure(
1826    current_token: &CompletionToken,
1827    reason: String,
1828) -> (Option<RunId>, CompletionToken, Result<String, String>, bool) {
1829    (None, current_token.clone(), Err(reason), true)
1830}
1831
1832/// Body of one liminal reply-router thread (see
1833/// [`WorkerActivityDispatcher::spawn_liminal_reply_router`] for the contract).
1834///
1835/// Resolves by the key THIS push dispatched (the awaiter is already
1836/// correlation-scoped to it), never by the reply's echoed ids: a buggy echo
1837/// must not cross executions.
1838#[cfg(feature = "liminal-transport")]
1839fn route_liminal_reply(
1840    pending: &PendingActivities,
1841    heartbeat_tracker: &HeartbeatTracker,
1842    registry: &crate::worker::ConnectedWorkerRegistry,
1843    drain_state: &DrainState,
1844    awaiter: &liminal_server::server::connection::PushReplyAwaiter,
1845    execution: (WorkerId, &WorkflowId, &ActivityId, &CompletionToken),
1846) {
1847    let (worker_id, workflow_id, activity_id, current_token) = execution;
1848    // The wait re-arms only while this dispatch is still tracked in-flight, so
1849    // a dispatch resolved elsewhere (expiry sweep, shutdown drain, cleanup)
1850    // releases this thread within one reply poll instead of parking it for the
1851    // remaining life of the worker's connection.
1852    let waited = super::liminal_transport::receive_bridge_reply(awaiter, || {
1853        heartbeat_tracker
1854            .is_tracked(worker_id, workflow_id, activity_id)
1855            .unwrap_or(false)
1856    });
1857    // `synthesized` marks a failure this router FABRICATED (disconnect or
1858    // receive fault) as opposed to a real worker reply: only fabricated
1859    // failures are gated on the tracker below.
1860    let (run_id, submitted_token, outcome, synthesized) = match waited {
1861        Ok(Some(response)) => {
1862            let submitted_token = match CompletionToken::from_wire(
1863                workflow_id,
1864                activity_id,
1865                response.completion_token,
1866            ) {
1867                Ok(token) => token,
1868                Err(error) => {
1869                    tracing::warn!(
1870                        worker_id = ?worker_id,
1871                        workflow_id = %workflow_id,
1872                        activity_id = %activity_id,
1873                        %error,
1874                        "liminal activity completion omitted its generation proof"
1875                    );
1876                    return;
1877                }
1878            };
1879            (response.run_id, submitted_token, response.outcome, false)
1880        }
1881        Ok(None) => {
1882            tracing::debug!(
1883                worker_id = ?worker_id,
1884                workflow_id = %workflow_id,
1885                activity_id = %activity_id,
1886                "liminal dispatch resolved by another path; abandoning reply wait"
1887            );
1888            return;
1889        }
1890        // UNSERVABLE, before the lost arm: liminal proved the frame larger than
1891        // the connection's whole outbound buffer. The lost arm below would
1892        // classify it through the transport-loss ledger and re-dispatch it
1893        // attempt-neutrally, forever — the 846-lease fault. The engine reads any
1894        // non-`retryable:` prefix as settled (`nif_activity_retry::is_retryable_reason`),
1895        // so this outcome records one `ActivityFailed` and stops.
1896        Err(error) if error.is_worker_dispatch_unservable() => {
1897            synthesized_failure(current_token, format!("terminal:{error}"))
1898        }
1899        // TRANSPORT domain (the worker's connection closed before it replied):
1900        // classified through the shared ledger, never as an action failure.
1901        Err(error) if error.is_worker_connection_lost() => synthesized_failure(
1902            current_token,
1903            pending.classify_worker_loss(workflow_id, activity_id, worker_id),
1904        ),
1905        Err(error) => synthesized_failure(
1906            current_token,
1907            format!("retryable:worker liminal reply failed: {error}"),
1908        ),
1909    };
1910    // The gRPC sweeps fail only still-tracked tasks (`remove_worker_tasks`);
1911    // this is the same structural gate: `complete_task` reports whether THIS
1912    // call retired the tracked entry. A poisoned tracker fails open (deliver)
1913    // so the blocked dispatch thread is never left hanging on a broken lock.
1914    let after_accept = || {
1915        if synthesized {
1916            let was_tracked = clear_completed_task_tracking(
1917                heartbeat_tracker,
1918                registry,
1919                worker_id,
1920                workflow_id,
1921                activity_id,
1922            );
1923            if !was_tracked {
1924                // Another path already resolved this dispatch (and notified drain):
1925                // injecting the fabricated lost-worker failure now could reach a retry.
1926                tracing::debug!(
1927                    worker_id = ?worker_id,
1928                    workflow_id = %workflow_id,
1929                    activity_id = %activity_id,
1930                    "liminal dispatch already resolved; dropping synthesized lost-worker failure"
1931                );
1932                return Ok(false);
1933            }
1934        } else {
1935            // A real worker result: the tracking clear is bookkeeping, never
1936            // a gate. Fail open — withholding a received result over a
1937            // poisoned tracker would leave the dispatch waiter parked on an
1938            // unbounded receive with no recovery path, because the lost-worker
1939            // sweep reads the same poisoned lock.
1940            let _ = clear_completed_task_tracking(
1941                heartbeat_tracker,
1942                registry,
1943                worker_id,
1944                workflow_id,
1945                activity_id,
1946            );
1947        }
1948        drain_state.notify_activity_drained();
1949        Ok(true)
1950    };
1951    if let Err(error) = pending.complete_fenced_after_accept(
1952        workflow_id,
1953        activity_id,
1954        run_id.as_ref(),
1955        &submitted_token,
1956        outcome,
1957        after_accept,
1958    ) {
1959        tracing::warn!(
1960            worker_id = ?worker_id,
1961            workflow_id = %workflow_id,
1962            activity_id = %activity_id,
1963            %error,
1964            "liminal activity completion handoff rejected"
1965        );
1966    }
1967}
1968
1969/// Clears a completed activity's in-flight tracking entry, reporting whether
1970/// THIS call retired it. Fails open on a poisoned tracker — logged loudly and
1971/// treated as "was tracked" — because the clear is bookkeeping, never a gate:
1972/// withholding a completion over the poisoned lock would strand the dispatch
1973/// waiter on an unbounded receive, and the lost-worker sweep cannot recover it
1974/// (it reads the same lock).
1975pub(crate) fn clear_completed_task_tracking(
1976    heartbeat_tracker: &HeartbeatTracker,
1977    registry: &crate::worker::ConnectedWorkerRegistry,
1978    worker_id: WorkerId,
1979    workflow_id: &WorkflowId,
1980    activity_id: &ActivityId,
1981) -> bool {
1982    heartbeat_tracker
1983        .complete_task(worker_id, workflow_id, activity_id, registry)
1984        .unwrap_or_else(|error| {
1985            tracing::error!(
1986                worker_id = ?worker_id,
1987                workflow_id = %workflow_id,
1988                activity_id = %activity_id,
1989                %error,
1990                "failed to clear in-flight tracking for a completed activity; delivering anyway — a poisoned tracker must not withhold a result"
1991            );
1992            true
1993        })
1994}
1995
1996struct ActivityDispatchContext<'a> {
1997    namespace: &'a str,
1998    activity_type: &'a str,
1999    worker_id: WorkerId,
2000    workflow_id: &'a WorkflowId,
2001    activity_id: &'a ActivityId,
2002    /// Attempt this dispatch was delivered at — the identity its own-attempt
2003    /// teardown paths remove the waiter slot by (aion#195).
2004    attempt: u32,
2005    completion_token: CompletionToken,
2006    started_at: Instant,
2007}
2008
2009fn activity_task(
2010    activity_type: &str,
2011    input: &str,
2012    execution: (&WorkflowId, &RunId, &ActivityId),
2013    attempt: u32,
2014    labels: BTreeMap<String, String>,
2015    completion_token: &CompletionToken,
2016) -> ProtoActivityTask {
2017    let (workflow_id, run_id, activity_id) = execution;
2018    ProtoActivityTask {
2019        workflow_id: Some(ProtoWorkflowId::from(workflow_id.clone())),
2020        activity_id: Some(ProtoActivityId::from(activity_id.clone())),
2021        activity_type: activity_type.to_owned(),
2022        input: Some(ProtoPayload {
2023            content_type: String::from("application/json"),
2024            bytes: input.as_bytes().to_vec(),
2025        }),
2026        attempt,
2027        labels: labels.into_iter().collect(),
2028        run_id: Some(run_id.clone().into()),
2029        completion_token: completion_token.as_str().to_owned(),
2030        idempotency_key: idempotency_key(workflow_id, run_id, activity_id),
2031    }
2032}
2033
2034fn log_activity_completion(context: &ActivityDispatchContext<'_>, succeeded: bool) {
2035    let duration_ms = duration_ms(context.started_at.elapsed());
2036    tracing::info!(
2037        operation = "activity_complete",
2038        namespace = context.namespace,
2039        workflow_id = %context.workflow_id,
2040        activity_id = %context.activity_id,
2041        activity_type = context.activity_type,
2042        worker_id = ?context.worker_id,
2043        duration_ms,
2044        outcome = if succeeded { "succeeded" } else { "failed" },
2045        "activity completed"
2046    );
2047}
2048
2049fn duration_ms(duration: Duration) -> u64 {
2050    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2051}
2052
2053fn log_worker_error(
2054    error_type: &'static str,
2055    namespace: &str,
2056    activity_type: &str,
2057    workflow_id: &WorkflowId,
2058    activity_id: &ActivityId,
2059    worker_id: Option<super::registry::WorkerId>,
2060    reason: &str,
2061) {
2062    tracing::error!(
2063        operation = "activity_dispatch",
2064        namespace,
2065        workflow_id = %workflow_id,
2066        activity_id = %activity_id,
2067        activity_type,
2068        worker_id = ?worker_id,
2069        error_type,
2070        reason,
2071        "worker interaction failed"
2072    );
2073}
2074
2075#[cfg(test)]
2076mod tests {
2077    use std::sync::{
2078        Mutex,
2079        atomic::{AtomicBool, Ordering},
2080    };
2081
2082    use aion_core::{ActivityError, ActivityErrorKind, ContentType, Payload};
2083
2084    use crate::worker::dispatch::ActivityCompletionOutcome;
2085
2086    use super::*;
2087
2088    fn activity_id(pos: u64) -> ActivityId {
2089        ActivityId::from_sequence_position(pos)
2090    }
2091
2092    /// One delivery of attempt 1 for `(workflow_id, id)` in its own run — what
2093    /// every single-dispatch unit test below is. The run and the attempt are the
2094    /// two discriminators the fence separates a REDELIVERY from a retry and from
2095    /// a new execution generation with, so they are stated here rather than
2096    /// implied.
2097    fn insert_attempt_one(
2098        pending: &PendingActivities,
2099        workflow_id: &WorkflowId,
2100        id: &ActivityId,
2101    ) -> Result<(CompletionToken, SyncReceiver), ServerError> {
2102        let (token, rx, superseded) =
2103            pending.insert(workflow_id.clone(), &RunId::new_v4(), id.clone(), 1)?;
2104        assert!(
2105            superseded.is_none(),
2106            "a first insert must never report a superseded holder"
2107        );
2108        Ok((token, rx))
2109    }
2110
2111    #[test]
2112    fn pending_insert_and_complete_delivers_result() -> Result<(), ServerError> {
2113        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2114        let workflow_id = WorkflowId::new_v4();
2115        let id = activity_id(1);
2116        let rx = insert_attempt_one(&pending, &workflow_id, &id)?.1;
2117
2118        assert!(pending.complete(&workflow_id, &id, None, Ok("done".to_owned())));
2119        assert_eq!(
2120            rx.recv_timeout(Duration::from_millis(50)),
2121            Ok(Ok("done".to_owned()))
2122        );
2123        Ok(())
2124    }
2125
2126    /// #246 / #254 red: retrying an activity while the same worker still holds
2127    /// the prior attempt must never replace its responder silently. Today's
2128    /// attempt-free `DashMap::insert` drops the first sender, producing the
2129    /// terminal "activity response channel dropped" specimen.
2130    #[test]
2131    fn same_execution_collision_is_a_named_typed_refusal() -> Result<(), ServerError> {
2132        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2133        let workflow_id = WorkflowId::new_v4();
2134        let id = activity_id(2);
2135        // The SAME execution redelivered: same run, same attempt. The fence
2136        // treats that as a sibling authorization; the pending map still refuses
2137        // to replace a live responder.
2138        let run_id = RunId::new_v4();
2139        let _first = pending.insert(workflow_id.clone(), &run_id, id.clone(), 1)?;
2140
2141        let error = pending
2142            .insert(workflow_id, &run_id, id, 1)
2143            .err()
2144            .ok_or_else(|| {
2145                ServerError::worker_dispatch(
2146                    "test",
2147                    "same-worker-collision",
2148                    "a live responder collision was silently replaced",
2149                )
2150            })?;
2151
2152        assert!(
2153            error.to_string().contains("pending activity collision"),
2154            "{error}"
2155        );
2156        Ok(())
2157    }
2158
2159    /// aion#195 THE PIN: a HIGHER attempt arriving at a held execution site is
2160    /// a retry whose predecessor timed out — it takes the site over instead of
2161    /// dying on it. Before the fix, this insert returned the collision error
2162    /// and the workflow FAILED while the healthy responder ran on (workflow
2163    /// b38ecf80: attempt 2 collided with attempt 1's live responder one second
2164    /// after its backoff and the whole run was lost).
2165    #[test]
2166    fn a_higher_attempt_takes_over_a_stale_execution_site() -> Result<(), ServerError> {
2167        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2168        let workflow_id = WorkflowId::new_v4();
2169        let id = activity_id(3);
2170        let run_id = RunId::new_v4();
2171        let (_stale_token, stale_rx, none) =
2172            pending.insert(workflow_id.clone(), &run_id, id.clone(), 1)?;
2173        assert!(none.is_none());
2174
2175        let (fresh_token, fresh_rx, superseded) =
2176            pending.insert(workflow_id.clone(), &run_id, id.clone(), 2)?;
2177        assert_eq!(
2178            superseded,
2179            Some(1),
2180            "the takeover must name the attempt it displaced"
2181        );
2182
2183        // The superseded waiter is woken with the retryable sentinel, so its
2184        // blocked dispatcher thread finishes instead of hanging forever.
2185        let stale_resolution = stale_rx
2186            .recv_timeout(Duration::from_millis(50))
2187            .map_err(|_| {
2188                ServerError::worker_dispatch(
2189                    "test",
2190                    "takeover",
2191                    "the superseded waiter was never woken",
2192                )
2193            })?;
2194        let reason = match stale_resolution {
2195            Ok(delivered) => {
2196                return Err(ServerError::worker_dispatch(
2197                    "test",
2198                    "takeover",
2199                    format!("the superseded waiter resolved Ok({delivered}) instead of an error"),
2200                ));
2201            }
2202            Err(reason) => reason,
2203        };
2204        assert!(
2205            reason.starts_with("retryable:") && reason.contains("superseded by attempt 2"),
2206            "{reason}"
2207        );
2208
2209        // The site now belongs to attempt 2: a completion presenting attempt
2210        // 2's token delivers to attempt 2's waiter.
2211        pending.complete_activity(ActivityCompletion {
2212            workflow_id: workflow_id.clone(),
2213            activity_id: id.clone(),
2214            run_id: None,
2215            completion_token: fresh_token,
2216            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2217                ContentType::Json,
2218                br#""second attempt wins""#.to_vec(),
2219            )),
2220        })?;
2221        assert_eq!(
2222            fresh_rx.recv_timeout(Duration::from_millis(50)),
2223            Ok(Ok(r#""second attempt wins""#.to_owned()))
2224        );
2225        Ok(())
2226    }
2227
2228    /// aion#195: a LOWER attempt arriving at a held site is a stale
2229    /// re-dispatch, not a retry — it must still be refused, and the refusal
2230    /// must name both attempts.
2231    #[test]
2232    fn a_lower_attempt_is_still_refused_by_name() -> Result<(), ServerError> {
2233        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2234        let workflow_id = WorkflowId::new_v4();
2235        let id = activity_id(4);
2236        let run_id = RunId::new_v4();
2237        let _held = pending.insert(workflow_id.clone(), &run_id, id.clone(), 3)?;
2238        let error = pending
2239            .insert(workflow_id, &run_id, id, 2)
2240            .err()
2241            .ok_or_else(|| {
2242                ServerError::worker_dispatch(
2243                    "test",
2244                    "stale-redispatch",
2245                    "a lower attempt silently replaced a live responder",
2246                )
2247            })?;
2248        let text = error.to_string();
2249        assert!(
2250            text.contains("attempt 2 arrived while attempt 3 still holds"),
2251            "{text}"
2252        );
2253        Ok(())
2254    }
2255
2256    #[test]
2257    fn pending_complete_unknown_returns_false() {
2258        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2259        assert!(!pending.complete(
2260            &WorkflowId::new_v4(),
2261            &activity_id(99),
2262            None,
2263            Ok("orphan".to_owned())
2264        ));
2265    }
2266
2267    #[derive(Default)]
2268    struct RecordingOutboxCallback {
2269        completions: Mutex<Vec<(WorkflowId, ActivityId, String)>>,
2270        failures: Mutex<Vec<(WorkflowId, ActivityId, String)>>,
2271        live: bool,
2272    }
2273
2274    impl OutboxDeliveryCallback for RecordingOutboxCallback {
2275        fn deliver_completion(
2276            &self,
2277            workflow_id: &WorkflowId,
2278            activity_id: &ActivityId,
2279            run_id: Option<&RunId>,
2280            result: String,
2281        ) -> Result<bool, ServerError> {
2282            let _ = run_id;
2283            self.completions
2284                .lock()
2285                .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
2286                .push((workflow_id.clone(), activity_id.clone(), result));
2287            Ok(self.live)
2288        }
2289
2290        fn deliver_failure(
2291            &self,
2292            workflow_id: &WorkflowId,
2293            activity_id: &ActivityId,
2294            run_id: Option<&RunId>,
2295            reason: String,
2296        ) -> Result<bool, ServerError> {
2297            let _ = run_id;
2298            self.failures
2299                .lock()
2300                .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
2301                .push((workflow_id.clone(), activity_id.clone(), reason));
2302            Ok(self.live)
2303        }
2304    }
2305
2306    #[test]
2307    fn unmatched_completion_routes_to_outbox_callback_when_installed() -> Result<(), ServerError> {
2308        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2309        let callback = Arc::new(RecordingOutboxCallback {
2310            live: true,
2311            ..RecordingOutboxCallback::default()
2312        });
2313        // Install on one clone; the wiring must be visible to every clone.
2314        pending.clone().set_outbox_delivery(callback.clone());
2315
2316        let workflow_id = WorkflowId::new_v4();
2317        let id = activity_id(7);
2318
2319        // No pending entry: the completion is unmatched and must route to the
2320        // callback rather than being dropped. A live workflow reports true.
2321        assert!(pending.complete(&workflow_id, &id, None, Ok("done".to_owned())));
2322        let completions = callback
2323            .completions
2324            .lock()
2325            .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?;
2326        assert_eq!(completions.len(), 1);
2327        assert_eq!(completions[0].0, workflow_id);
2328        assert_eq!(completions[0].1, id);
2329        assert_eq!(completions[0].2, "done");
2330        Ok(())
2331    }
2332
2333    #[test]
2334    fn unmatched_failure_routes_to_outbox_callback_and_not_live_reports_false()
2335    -> Result<(), ServerError> {
2336        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2337        // live = false models the expected stale-completion case.
2338        let callback = Arc::new(RecordingOutboxCallback::default());
2339        pending.set_outbox_delivery(callback.clone());
2340
2341        let workflow_id = WorkflowId::new_v4();
2342        let id = activity_id(8);
2343
2344        assert!(!pending.complete(&workflow_id, &id, None, Err("retryable:boom".to_owned())));
2345        let failures = callback
2346            .failures
2347            .lock()
2348            .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?;
2349        assert_eq!(failures.len(), 1);
2350        assert_eq!(failures[0].2, "retryable:boom");
2351        Ok(())
2352    }
2353
2354    #[test]
2355    fn unmatched_completion_is_silent_drop_when_no_callback_installed() {
2356        // Flag-off byte-identical behaviour: no callback, unmatched returns
2357        // false (silent drop) exactly as before.
2358        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2359        assert!(!pending.complete(
2360            &WorkflowId::new_v4(),
2361            &activity_id(9),
2362            None,
2363            Ok("x".to_owned())
2364        ));
2365    }
2366
2367    #[test]
2368    fn matched_completion_never_reaches_outbox_callback() -> Result<(), ServerError> {
2369        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2370        let callback = Arc::new(RecordingOutboxCallback {
2371            live: true,
2372            ..RecordingOutboxCallback::default()
2373        });
2374        pending.set_outbox_delivery(callback.clone());
2375
2376        let workflow_id = WorkflowId::new_v4();
2377        let id = activity_id(10);
2378        let rx = insert_attempt_one(&pending, &workflow_id, &id)?.1;
2379
2380        assert!(pending.complete(&workflow_id, &id, None, Ok("matched".to_owned())));
2381        assert_eq!(
2382            rx.recv_timeout(Duration::from_millis(50)),
2383            Ok(Ok("matched".to_owned()))
2384        );
2385        assert!(
2386            callback
2387                .completions
2388                .lock()
2389                .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
2390                .is_empty(),
2391            "a matched completion must deliver to its waiter, not the outbox callback"
2392        );
2393        Ok(())
2394    }
2395
2396    /// #207: parking resolves the matched waiter with the ephemeral parked
2397    /// sentinel — the exact string the engine's retry loop classifies as
2398    /// `Parked` — and nothing else.
2399    #[test]
2400    fn park_activity_resolves_matched_waiter_with_the_parked_sentinel() -> Result<(), ServerError> {
2401        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2402        let workflow_id = WorkflowId::new_v4();
2403        let id = activity_id(11);
2404        let rx = insert_attempt_one(&pending, &workflow_id, &id)?.1;
2405
2406        pending.park_activity(&workflow_id, &id)?;
2407        let result = rx
2408            .recv_timeout(Duration::from_millis(50))
2409            .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
2410        assert_eq!(result, Err(aion::PARKED_ACTIVITY_REASON.to_owned()));
2411        Ok(())
2412    }
2413
2414    /// #207: an unmatched park is a no-op and is NEVER routed to the outbox
2415    /// delivery callback — a park is not a failure and must never reach a
2416    /// workflow.
2417    #[test]
2418    fn unmatched_park_is_a_noop_and_never_reaches_the_outbox_callback() -> Result<(), ServerError> {
2419        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2420        let callback = Arc::new(RecordingOutboxCallback {
2421            live: true,
2422            ..RecordingOutboxCallback::default()
2423        });
2424        pending.set_outbox_delivery(callback.clone());
2425
2426        pending.park_activity(&WorkflowId::new_v4(), &activity_id(12))?;
2427
2428        assert!(
2429            callback
2430                .failures
2431                .lock()
2432                .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
2433                .is_empty(),
2434            "a park must never be delivered as an outbox failure"
2435        );
2436        assert!(
2437            callback
2438                .completions
2439                .lock()
2440                .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
2441                .is_empty(),
2442            "a park must never be delivered as an outbox completion"
2443        );
2444        Ok(())
2445    }
2446
2447    #[test]
2448    fn completion_sink_routes_success() -> Result<(), ServerError> {
2449        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2450        let workflow_id = WorkflowId::new_v4();
2451        let id = activity_id(2);
2452        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2453        let payload = Payload::new(ContentType::Json, br#"{"greeting":"hi"}"#.to_vec());
2454
2455        pending.complete_activity(ActivityCompletion {
2456            workflow_id,
2457            activity_id: id,
2458            run_id: None,
2459            completion_token,
2460            outcome: ActivityCompletionOutcome::Succeeded(payload),
2461        })?;
2462
2463        let result = rx
2464            .recv_timeout(Duration::from_millis(50))
2465            .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
2466        assert_eq!(result, Ok(r#"{"greeting":"hi"}"#.to_owned()));
2467        Ok(())
2468    }
2469
2470    #[test]
2471    fn accepted_hook_runs_before_the_result_is_observable() -> Result<(), ServerError> {
2472        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2473        let workflow_id = WorkflowId::new_v4();
2474        let id = activity_id(13);
2475        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2476        let accepted = AtomicBool::new(false);
2477
2478        pending.complete_activity_after_accept(
2479            ActivityCompletion {
2480                workflow_id,
2481                activity_id: id,
2482                run_id: None,
2483                completion_token,
2484                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2485                    ContentType::Json,
2486                    br#""ordered""#.to_vec(),
2487                )),
2488            },
2489            || {
2490                accepted.store(true, Ordering::SeqCst);
2491                Ok(())
2492            },
2493        )?;
2494
2495        let result = rx
2496            .recv_timeout(Duration::from_millis(50))
2497            .map_err(|error| ServerError::worker_dispatch("", "", format!("channel: {error}")))?;
2498        assert_eq!(result, Ok(r#""ordered""#.to_owned()));
2499        assert!(
2500            accepted.load(Ordering::SeqCst),
2501            "the accepted-path cleanup hook must finish before the waiter can observe the result"
2502        );
2503        Ok(())
2504    }
2505
2506    #[test]
2507    fn a_non_publishing_settlement_restores_the_generation_for_the_true_resolver()
2508    -> Result<(), ServerError> {
2509        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2510        let workflow_id = WorkflowId::new_v4();
2511        let id = activity_id(16);
2512        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2513
2514        // The synthesized-failure path finding its dispatch already resolved
2515        // by another path: settlement reports "do not publish". The generation
2516        // it consumed on accept must come back, because the OTHER path (the
2517        // lost-worker sweep) presents the same token next.
2518        let settled = pending.complete_fenced_after_accept(
2519            &workflow_id,
2520            &id,
2521            None,
2522            &completion_token,
2523            Err("retryable:worker lost".to_owned()),
2524            || Ok(false),
2525        )?;
2526        assert!(!settled, "a non-publishing settlement must report false");
2527        assert!(
2528            rx.try_recv().is_err(),
2529            "a non-publishing settlement must not publish"
2530        );
2531
2532        // The true resolver arrives with the same token: it must be accepted,
2533        // not refused with NoCurrentGeneration.
2534        let published = pending.complete_fenced_after_accept(
2535            &workflow_id,
2536            &id,
2537            None,
2538            &completion_token,
2539            Err("retryable:worker lost".to_owned()),
2540            || Ok(true),
2541        )?;
2542        assert!(
2543            published,
2544            "the true resolver must be accepted after a non-publishing settlement"
2545        );
2546        assert_eq!(
2547            rx.recv_timeout(Duration::from_millis(50))
2548                .map_err(|error| ServerError::worker_dispatch(
2549                    "",
2550                    "",
2551                    format!("channel: {error}")
2552                ))?,
2553            Err("retryable:worker lost".to_owned())
2554        );
2555        Ok(())
2556    }
2557
2558    #[test]
2559    fn a_poisoned_tracker_fails_open_and_never_withholds_a_completion() {
2560        let tracker = HeartbeatTracker::new(TEST_HEARTBEAT_WINDOW);
2561        tracker.poison_for_tests();
2562        let workflow_id = WorkflowId::new_v4();
2563        let id = activity_id(17);
2564        // An empty registry: the worker was never registered here, so the
2565        // selection-side count has nothing to correct. This test is about the
2566        // TRACKER's poison policy, and the registry is present only because
2567        // the two counts are written by one call.
2568        let registry = ConnectedWorkerRegistry::default();
2569        let was_tracked = clear_completed_task_tracking(
2570            &tracker,
2571            &registry,
2572            WorkerId::from_value(1),
2573            &workflow_id,
2574            &id,
2575        );
2576        assert!(
2577            was_tracked,
2578            "a poisoned tracker must fail open (deliver), never withhold"
2579        );
2580    }
2581
2582    #[test]
2583    fn accepted_hook_failure_blocks_publication_and_restores_generation() -> Result<(), ServerError>
2584    {
2585        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2586        let workflow_id = WorkflowId::new_v4();
2587        let id = activity_id(15);
2588        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2589
2590        let rejected = pending.complete_activity_after_accept(
2591            ActivityCompletion {
2592                workflow_id: workflow_id.clone(),
2593                activity_id: id.clone(),
2594                run_id: None,
2595                completion_token: completion_token.clone(),
2596                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2597                    ContentType::Json,
2598                    br#""ordered""#.to_vec(),
2599                )),
2600            },
2601            || Err(ServerError::lock_poisoned("accepted-path settlement")),
2602        );
2603
2604        assert!(matches!(rejected, Err(ServerError::LockPoisoned { .. })));
2605        assert!(
2606            rx.try_recv().is_err(),
2607            "failed accepted-path settlement must not publish the completion"
2608        );
2609
2610        pending.complete_activity_after_accept(
2611            ActivityCompletion {
2612                workflow_id,
2613                activity_id: id,
2614                run_id: None,
2615                completion_token,
2616                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2617                    ContentType::Json,
2618                    br#""ordered""#.to_vec(),
2619                )),
2620            },
2621            || Ok(()),
2622        )?;
2623        assert_eq!(
2624            rx.recv_timeout(Duration::from_millis(50))
2625                .map_err(|error| ServerError::worker_dispatch(
2626                    "",
2627                    "",
2628                    format!("channel: {error}")
2629                ))?,
2630            Ok(r#""ordered""#.to_owned())
2631        );
2632        Ok(())
2633    }
2634
2635    #[test]
2636    fn refused_generation_never_runs_the_accepted_hook() -> Result<(), ServerError> {
2637        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2638        let workflow_id = WorkflowId::new_v4();
2639        let id = activity_id(14);
2640        let (_completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2641        let accepted = AtomicBool::new(false);
2642
2643        let rejected = pending.complete_activity_after_accept(
2644            ActivityCompletion {
2645                workflow_id,
2646                activity_id: id,
2647                run_id: None,
2648                completion_token: CompletionToken::for_test(),
2649                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2650                    ContentType::Json,
2651                    br#""stale""#.to_vec(),
2652                )),
2653            },
2654            || {
2655                accepted.store(true, Ordering::SeqCst);
2656                Ok(())
2657            },
2658        );
2659
2660        assert!(matches!(
2661            rejected,
2662            Err(ServerError::ActivityCompletionRejected { .. })
2663        ));
2664        assert!(
2665            !accepted.load(Ordering::SeqCst),
2666            "a refused generation must preserve liveness by skipping accepted-path cleanup"
2667        );
2668        assert!(
2669            rx.try_recv().is_err(),
2670            "a refused generation must leave the current waiter unresolved"
2671        );
2672        Ok(())
2673    }
2674
2675    #[test]
2676    fn malformed_payload_does_not_consume_the_current_generation() -> Result<(), ServerError> {
2677        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2678        let workflow_id = WorkflowId::new_v4();
2679        let id = activity_id(12);
2680        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2681
2682        let malformed = pending.complete_activity(ActivityCompletion {
2683            workflow_id: workflow_id.clone(),
2684            activity_id: id.clone(),
2685            run_id: None,
2686            completion_token: completion_token.clone(),
2687            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2688                ContentType::Json,
2689                vec![0xff],
2690            )),
2691        });
2692        assert!(matches!(malformed, Err(ServerError::WorkerDispatch { .. })));
2693        assert!(
2694            rx.try_recv().is_err(),
2695            "an invalid result must leave the waiter unresolved"
2696        );
2697
2698        pending.complete_activity(ActivityCompletion {
2699            workflow_id,
2700            activity_id: id,
2701            run_id: None,
2702            completion_token,
2703            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2704                ContentType::Json,
2705                br#""valid""#.to_vec(),
2706            )),
2707        })?;
2708        let result = rx
2709            .recv_timeout(Duration::from_millis(50))
2710            .map_err(|error| ServerError::worker_dispatch("", "", format!("channel: {error}")))?;
2711        assert_eq!(result, Ok(r#""valid""#.to_owned()));
2712        Ok(())
2713    }
2714
2715    #[test]
2716    fn completion_sink_routes_retryable_error() -> Result<(), ServerError> {
2717        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2718        let workflow_id = WorkflowId::new_v4();
2719        let id = activity_id(3);
2720        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2721
2722        pending.complete_activity(ActivityCompletion {
2723            workflow_id,
2724            activity_id: id,
2725            run_id: None,
2726            completion_token,
2727            outcome: ActivityCompletionOutcome::Failed(ActivityError {
2728                kind: ActivityErrorKind::Retryable,
2729                message: "temporary".to_owned(),
2730                details: None,
2731            }),
2732        })?;
2733
2734        let result = rx
2735            .recv_timeout(Duration::from_millis(50))
2736            .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
2737        assert_eq!(result, Err("retryable:temporary".to_owned()));
2738        Ok(())
2739    }
2740
2741    #[test]
2742    fn completion_sink_routes_policy_refusal_with_its_own_prefix() -> Result<(), ServerError> {
2743        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2744        let workflow_id = WorkflowId::new_v4();
2745        let id = activity_id(4);
2746        let (completion_token, rx) = insert_attempt_one(&pending, &workflow_id, &id)?;
2747
2748        pending.complete_activity(ActivityCompletion {
2749            workflow_id,
2750            activity_id: id,
2751            run_id: None,
2752            completion_token,
2753            outcome: ActivityCompletionOutcome::Failed(ActivityError {
2754                kind: ActivityErrorKind::PolicyRefused,
2755                message: "provider safety policy".to_owned(),
2756                details: None,
2757            }),
2758        })?;
2759
2760        let result = rx
2761            .recv_timeout(Duration::from_millis(50))
2762            .map_err(|error| ServerError::worker_dispatch("", "", format!("channel: {error}")))?;
2763        assert_eq!(
2764            result,
2765            Err("policy_refused:provider safety policy".to_owned())
2766        );
2767        Ok(())
2768    }
2769
2770    /// Regression test (#59, brief D12): pending tracking must be keyed by
2771    /// the full `(WorkflowId, ActivityId)` pair. The dispatcher fabricates
2772    /// activity ids from a process-local counter that resets on server
2773    /// restart, so a stale result re-reported from a worker's previous
2774    /// session carries the same bare `ActivityId` as a fresh post-restart
2775    /// dispatch. Under bare-`ActivityId` keying the stale result completed
2776    /// the wrong execution; with pair keying it is dropped and the genuine
2777    /// result still completes.
2778    #[test]
2779    fn stale_result_for_other_workflow_does_not_complete_pending_dispatch()
2780    -> Result<(), ServerError> {
2781        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2782        let post_restart_workflow = WorkflowId::new_v4();
2783        let pre_restart_workflow = WorkflowId::new_v4();
2784        // Counter resets to the same sequence position after restart.
2785        let id = activity_id(1);
2786        let (completion_token, rx) = insert_attempt_one(&pending, &post_restart_workflow, &id)?;
2787
2788        // Stale pre-restart result: same activity id, different workflow.
2789        let rejected = pending.complete_activity(ActivityCompletion {
2790            workflow_id: pre_restart_workflow,
2791            activity_id: id.clone(),
2792            run_id: None,
2793            completion_token: CompletionToken::for_test(),
2794            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2795                ContentType::Json,
2796                br#""stale""#.to_vec(),
2797            )),
2798        });
2799        assert!(matches!(
2800            rejected,
2801            Err(ServerError::ActivityCompletionRejected { .. })
2802        ));
2803        assert!(
2804            rx.try_recv().is_err(),
2805            "stale result for a different workflow must not complete this dispatch"
2806        );
2807
2808        // The genuine result for the pending execution still completes.
2809        pending.complete_activity(ActivityCompletion {
2810            workflow_id: post_restart_workflow,
2811            activity_id: id,
2812            run_id: None,
2813            completion_token,
2814            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
2815                ContentType::Json,
2816                br#""fresh""#.to_vec(),
2817            )),
2818        })?;
2819        let result = rx
2820            .recv_timeout(Duration::from_millis(50))
2821            .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
2822        assert_eq!(result, Ok(r#""fresh""#.to_owned()));
2823        Ok(())
2824    }
2825
2826    /// Liveness tracker for dispatcher unit tests; the window only matters
2827    /// to expiry checks, which nothing in these tests drives.
2828    /// The heartbeat window these unit tests wire, named once so the
2829    /// transport-loss budget they run under is explicit rather than inherited.
2830    /// It is the same window [`test_tracker`] uses, so a sink and a tracker
2831    /// built side by side in a test agree exactly as they do in production.
2832    const TEST_HEARTBEAT_WINDOW: Duration = Duration::from_secs(5);
2833
2834    /// The budget a sink ends up with is DERIVED from the operator's window,
2835    /// and this is the arithmetic — not prose about it.
2836    ///
2837    /// Before #100 the wiring was `default().with_heartbeat_window(w)`: two
2838    /// calls where the second was omissible, and deleting it from all five
2839    /// production sites reddened nothing, because every test ran the zero-budget
2840    /// default. This pin and its sibling below are what make that deletion
2841    /// impossible to perform silently.
2842    #[test]
2843    fn a_sink_derives_its_transport_budget_from_the_operator_window() {
2844        let window = Duration::from_secs(7);
2845        let sink = PendingActivities::new(window);
2846
2847        assert_eq!(
2848            sink.transport_losses().budget(),
2849            window * super::super::transport_loss::TRANSPORT_LOSS_BUDGET_WINDOWS,
2850            "the transport-loss budget must be the operator's heartbeat window times \
2851             TRANSPORT_LOSS_BUDGET_WINDOWS; a sink carrying any other budget is one no \
2852             operator declared"
2853        );
2854    }
2855
2856    /// The wiring itself, which is where the defect actually lived: a dispatcher
2857    /// is handed a `HeartbeatTracker` and must not be able to carry a transport
2858    /// budget that disagrees with it.
2859    ///
2860    /// This asserts on the dispatcher a caller really builds, so it sits on the
2861    /// path a forgetful wiring would take rather than on the constructor in
2862    /// isolation.
2863    #[test]
2864    fn a_dispatcher_carries_the_transport_budget_of_the_tracker_it_was_given() {
2865        let window = Duration::from_secs(11);
2866        let registry = ConnectedWorkerRegistry::default();
2867        let dispatcher =
2868            WorkerActivityDispatcher::new(registry, "default", HeartbeatTracker::new(window));
2869
2870        assert_eq!(
2871            dispatcher.pending().transport_losses().budget(),
2872            window * super::super::transport_loss::TRANSPORT_LOSS_BUDGET_WINDOWS,
2873            "the dispatcher's transport budget must be derived from the heartbeat window it \
2874             was constructed with; a zero or mismatched budget here means a worker loss is \
2875             declared transport-exhausted after ONE re-dispatch instead of after the \
2876             operator's window"
2877        );
2878        assert_ne!(
2879            dispatcher.pending().transport_losses().budget(),
2880            Duration::ZERO,
2881            "a ZERO budget is the specific regression this pin exists for: it grants exactly \
2882             one re-dispatchable loss and then declares the infrastructure flapping"
2883        );
2884    }
2885
2886    fn test_tracker() -> HeartbeatTracker {
2887        HeartbeatTracker::new(TEST_HEARTBEAT_WINDOW)
2888    }
2889
2890    /// A `greet` dispatch request carrying real (test-synthesized) ids, the
2891    /// engine-seam shape `WorkerActivityDispatcher::dispatch` now consumes.
2892    fn greet_request() -> ActivityDispatch {
2893        ActivityDispatch {
2894            namespace: "default".to_owned(),
2895            task_queue: "default".to_owned(),
2896            node: None,
2897            workflow_id: WorkflowId::new_v4(),
2898            run_id: RunId::new_v4(),
2899            activity_id: ActivityId::from_sequence_position(0),
2900            name: "greet".to_owned(),
2901            input: "{}".to_owned(),
2902            config: "{}".to_owned(),
2903            attempt: 1,
2904            labels: std::collections::BTreeMap::new(),
2905            advisory: false,
2906        }
2907    }
2908
2909    #[test]
2910    fn dispatcher_fails_immediately_when_draining_without_workers() {
2911        let registry = ConnectedWorkerRegistry::default();
2912        let drain = DrainState::default();
2913        let dispatcher = WorkerActivityDispatcher::new(registry, "default", test_tracker())
2914            .with_drain_state(drain.clone());
2915
2916        let _ = drain.begin();
2917
2918        let result = dispatcher.dispatch(greet_request());
2919
2920        assert!(result.is_err());
2921        let err = result.err().unwrap_or_default();
2922        assert!(
2923            err.contains("drain"),
2924            "expected drain rejection, got: {err}"
2925        );
2926    }
2927
2928    /// Regression test for the production stall where every remote activity
2929    /// timed out: the engine invoked the sync `dispatch` from inside a
2930    /// spawned tokio task (`futures::future::lazy` polled on a runtime
2931    /// worker), and the woken stream-consumer task landed in that blocked
2932    /// worker's non-stealable LIFO slot, so the queued `ActivityTask` was
2933    /// only delivered when the then-extant 30s dispatch timeout fired (the
2934    /// dispatch wait is unbounded today; the stall would now be a hang).
2935    ///
2936    /// Mirrors the real wiring minus tonic: the real registry channel that
2937    /// the gRPC stream forwarder drains, a worker task awaiting that channel
2938    /// on the same runtime, completion through the production
2939    /// `ActivityCompletionSink`, and the sync dispatch invoked from a
2940    /// runtime worker task — the worst case the `block_in_place` guard in
2941    /// `dispatch` defends against (the engine itself now routes through
2942    /// `dispatch_async`, off the async workers).
2943    /// aion#195 END TO END: attempt 1 is delivered to a worker that never
2944    /// answers (the engine's per-attempt bound has already failed it and
2945    /// dropped its future); attempt 2 of the same execution then arrives. The
2946    /// site is taken over, the worker is asked to STOP the corpse on its own
2947    /// dispatch stream — the cancel frame precedes attempt 2's task — the
2948    /// corpse's heartbeat task is untracked, attempt 2 runs and completes, and
2949    /// attempt 1's blocked dispatcher thread resolves with the retryable
2950    /// superseded sentinel instead of hanging or failing the workflow.
2951    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2952    async fn a_retry_takes_over_a_timed_out_attempt_and_asks_its_worker_to_stop()
2953    -> Result<(), Box<dyn std::error::Error>> {
2954        let registry = ConnectedWorkerRegistry::default();
2955        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
2956        let (worker_tx, mut worker_rx) = tokio::sync::mpsc::channel(32);
2957        let activity_types = [String::from("greet")];
2958        let registration = registry.register(
2959            "default",
2960            activity_types.iter(),
2961            worker_tx,
2962            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2963        )?;
2964        let worker_id = registration
2965            .worker_id()
2966            .ok_or("test worker registration missing id")?;
2967        let tracker = test_tracker();
2968        let dispatcher = Arc::new(
2969            WorkerActivityDispatcher::new(registry, "default", tracker.clone())
2970                .with_pending(pending.clone()),
2971        );
2972
2973        let request = greet_request();
2974        let workflow_id = request.workflow_id.clone();
2975        let activity_id = request.activity_id.clone();
2976
2977        let first_dispatcher = Arc::clone(&dispatcher);
2978        let first_request = request.clone();
2979        let first_attempt = tokio::spawn(futures::future::lazy(move |_| {
2980            first_dispatcher.dispatch(first_request)
2981        }));
2982        let Some(WorkerMessage::ActivityTask(first_task)) = worker_rx.recv().await else {
2983            return Err("expected attempt 1's task on the worker channel".into());
2984        };
2985        assert_eq!(first_task.attempt, 1);
2986
2987        let mut retry_request = request.clone();
2988        retry_request.attempt = 2;
2989        let retry_dispatcher = Arc::clone(&dispatcher);
2990        let retry_attempt = tokio::spawn(futures::future::lazy(move |_| {
2991            retry_dispatcher.dispatch(retry_request)
2992        }));
2993
2994        // The release ladder runs before attempt 2's task is pushed, so the
2995        // worker sees the STOP for the corpse first, then the new work.
2996        let Some(WorkerMessage::CancelActivity(cancel)) = worker_rx.recv().await else {
2997            return Err(
2998                "expected the superseded attempt's cancel frame before attempt 2's task".into(),
2999            );
3000        };
3001        assert_eq!(
3002            cancel
3003                .activity_id
3004                .map(|id| id.sequence_position)
3005                .unwrap_or_default(),
3006            activity_id.sequence_position()
3007        );
3008        let Some(WorkerMessage::ActivityTask(retry_task)) = worker_rx.recv().await else {
3009            return Err("expected attempt 2's task on the worker channel".into());
3010        };
3011        assert_eq!(retry_task.attempt, 2);
3012
3013        // The corpse's heartbeat task was untracked, so the expiry sweep can
3014        // never present its stale token; only attempt 2 remains in flight.
3015        let in_flight = tracker.in_flight_for_workflow(&workflow_id)?;
3016        assert_eq!(
3017            in_flight
3018                .iter()
3019                .map(|task| task.attempt)
3020                .collect::<Vec<_>>(),
3021            vec![2],
3022            "exactly attempt 2 must remain tracked for worker {worker_id:?}"
3023        );
3024
3025        let retry_token = CompletionToken::from_wire(
3026            &workflow_id,
3027            &activity_id,
3028            retry_task.completion_token.clone(),
3029        )
3030        .map_err(|error| error.to_string())?;
3031        pending.complete_activity(ActivityCompletion {
3032            workflow_id: workflow_id.clone(),
3033            activity_id: activity_id.clone(),
3034            run_id: None,
3035            completion_token: retry_token,
3036            outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
3037                ContentType::Json,
3038                br#""attempt 2 delivered""#.to_vec(),
3039            )),
3040        })?;
3041
3042        let retry_result = retry_attempt.await.map_err(|error| error.to_string())?;
3043        assert_eq!(retry_result, Ok(r#""attempt 2 delivered""#.to_owned()));
3044        let first_result = first_attempt.await.map_err(|error| error.to_string())?;
3045        let first_reason = match first_result {
3046            Ok(delivered) => {
3047                return Err(
3048                    format!("attempt 1 resolved Ok({delivered}) instead of superseded").into(),
3049                );
3050            }
3051            Err(reason) => reason,
3052        };
3053        assert!(
3054            first_reason.starts_with("retryable:")
3055                && first_reason.contains("superseded by attempt 2"),
3056            "{first_reason}"
3057        );
3058        registration.deregister()?;
3059        Ok(())
3060    }
3061
3062    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3063    async fn dispatch_inside_runtime_task_delivers_promptly_and_round_trips()
3064    -> Result<(), Box<dyn std::error::Error>> {
3065        let registry = ConnectedWorkerRegistry::default();
3066        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
3067        let (worker_tx, mut worker_rx) = tokio::sync::mpsc::channel(32);
3068        let activity_types = [String::from("greet")];
3069        let registration = registry.register(
3070            "default",
3071            activity_types.iter(),
3072            worker_tx,
3073            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3074        )?;
3075
3076        let sink = pending.clone();
3077        let echo_worker = tokio::spawn(async move {
3078            let Some(WorkerMessage::ActivityTask(task)) = worker_rx.recv().await else {
3079                return Err("expected an activity task on the worker channel".to_owned());
3080            };
3081            let workflow_id = task
3082                .workflow_id
3083                .ok_or("task missing workflow id")
3084                .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
3085            let activity_id = task
3086                .activity_id
3087                .map(ActivityId::from)
3088                .ok_or("task missing activity id")?;
3089            let completion_token =
3090                CompletionToken::from_wire(&workflow_id, &activity_id, task.completion_token)
3091                    .map_err(|error| error.to_string())?;
3092            sink.complete_activity(ActivityCompletion {
3093                workflow_id,
3094                activity_id,
3095                run_id: None,
3096                completion_token,
3097                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
3098                    ContentType::Json,
3099                    br#"{"greeting":"hello"}"#.to_vec(),
3100                )),
3101            })
3102            .map_err(|error| error.to_string())
3103        });
3104
3105        let dispatcher = Arc::new(
3106            WorkerActivityDispatcher::new(registry, "default", test_tracker())
3107                .with_pending(pending),
3108        );
3109        let started = Instant::now();
3110        // Invoke the sync dispatch inside the first poll of a spawned task:
3111        // the worst-case calling context for the `block_in_place` guard.
3112        let dispatch_task = tokio::spawn(futures::future::lazy(move |_| {
3113            dispatcher.dispatch(greet_request())
3114        }));
3115        let result = dispatch_task.await.map_err(|error| error.to_string())?;
3116        let elapsed = started.elapsed();
3117
3118        assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
3119        assert!(
3120            elapsed < Duration::from_secs(5),
3121            "dispatch round trip took {elapsed:?}; task delivery must not \
3122             depend on the blocked dispatch thread"
3123        );
3124        echo_worker.await.map_err(|error| error.to_string())??;
3125        registration.deregister()?;
3126        Ok(())
3127    }
3128
3129    /// A current-thread runtime cannot host the blocking wait (the stream
3130    /// forwarder would share its only executor thread), so dispatch must
3131    /// fail fast with a precise error instead of blocking forever.
3132    #[tokio::test]
3133    async fn dispatch_on_current_thread_runtime_fails_fast()
3134    -> Result<(), Box<dyn std::error::Error>> {
3135        let registry = ConnectedWorkerRegistry::default();
3136        let (worker_tx, _worker_rx) = tokio::sync::mpsc::channel(32);
3137        let activity_types = [String::from("greet")];
3138        let registration = registry.register(
3139            "default",
3140            activity_types.iter(),
3141            worker_tx,
3142            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3143        )?;
3144        let dispatcher = WorkerActivityDispatcher::new(registry, "default", test_tracker());
3145
3146        let started = Instant::now();
3147        let result = dispatcher.dispatch(greet_request());
3148        let elapsed = started.elapsed();
3149
3150        let err = result.err().ok_or("expected dispatch to fail")?;
3151        assert!(
3152            err.contains("multi-thread tokio runtime"),
3153            "unexpected error: {err}"
3154        );
3155        assert!(
3156            elapsed < Duration::from_secs(5),
3157            "fail-fast path took {elapsed:?}"
3158        );
3159        registration.deregister()?;
3160        Ok(())
3161    }
3162
3163    /// Bridge-level mirror of the e2e node-pin proof: two workers share the
3164    /// `(namespace, task_queue)` pool but advertise different nodes; an
3165    /// `ActivityDispatch` pinned to one node must reach ONLY the worker on that
3166    /// node through the live engine-seam `WorkerActivityDispatcher`. This is the
3167    /// regression guard for the bridge discarding the dispatch's `node`.
3168    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3169    async fn dispatch_pinned_to_node_reaches_only_that_node()
3170    -> Result<(), Box<dyn std::error::Error>> {
3171        let registry = ConnectedWorkerRegistry::default();
3172        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
3173        let activity_types = [String::from("greet")];
3174        let (n1_tx, mut n1_rx) = tokio::sync::mpsc::channel(32);
3175        let (n2_tx, mut n2_rx) = tokio::sync::mpsc::channel(32);
3176        // Register the DECOY (n2) FIRST so it owns the lowest worker id. The
3177        // bridge's `select_worker` orders candidates by worker id and starts at
3178        // the pool's rotation cursor, which is 0 for a pool nothing has selected
3179        // from yet — so the FIRST unfiltered selection here would name n2 (the
3180        // decoy), and a bridge that DISCARDED the node would route to it: the n1
3181        // echo would never fire and the round trip would time out. With the node
3182        // threaded through, selection is filtered to n1 before the cursor is
3183        // consulted, so the rotation cannot rescue a bridge that dropped it.
3184        let on_n2 = registry.register_namespaces(
3185            [String::from("default")],
3186            "default",
3187            Some(String::from("n2")),
3188            activity_types.iter(),
3189            n2_tx,
3190            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3191        )?;
3192        let on_n1 = registry.register_namespaces(
3193            [String::from("default")],
3194            "default",
3195            Some(String::from("n1")),
3196            activity_types.iter(),
3197            n1_tx,
3198            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3199        )?;
3200
3201        // Echo only on the n1 channel: the dispatch can only complete if the
3202        // task was routed to n1. If it leaked to n2, the n1 wait would stall and
3203        // the round trip below would time out instead.
3204        let sink = pending.clone();
3205        let echo_n1 = tokio::spawn(async move {
3206            let Some(WorkerMessage::ActivityTask(task)) = n1_rx.recv().await else {
3207                return Err("expected an activity task on the n1 worker channel".to_owned());
3208            };
3209            let workflow_id = task
3210                .workflow_id
3211                .ok_or("task missing workflow id")
3212                .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
3213            let activity_id = task
3214                .activity_id
3215                .map(ActivityId::from)
3216                .ok_or("task missing activity id")?;
3217            let completion_token =
3218                CompletionToken::from_wire(&workflow_id, &activity_id, task.completion_token)
3219                    .map_err(|error| error.to_string())?;
3220            sink.complete_activity(ActivityCompletion {
3221                workflow_id,
3222                activity_id,
3223                run_id: None,
3224                completion_token,
3225                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
3226                    ContentType::Json,
3227                    br#"{"greeting":"hello"}"#.to_vec(),
3228                )),
3229            })
3230            .map_err(|error| error.to_string())
3231        });
3232
3233        let dispatcher = Arc::new(
3234            WorkerActivityDispatcher::new(registry.clone(), "default", test_tracker())
3235                .with_pending(pending),
3236        );
3237
3238        let pinned = ActivityDispatch {
3239            node: Some(String::from("n1")),
3240            ..greet_request()
3241        };
3242        let started = Instant::now();
3243        let result = tokio::spawn(futures::future::lazy(move |_| dispatcher.dispatch(pinned)))
3244            .await
3245            .map_err(|error| error.to_string())?;
3246        let elapsed = started.elapsed();
3247
3248        assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
3249        assert!(
3250            elapsed < Duration::from_secs(5),
3251            "pinned dispatch round trip took {elapsed:?}; the task must route to n1"
3252        );
3253        echo_n1.await.map_err(|error| error.to_string())??;
3254
3255        // The n2 worker (wrong node) must never have been handed the task.
3256        assert!(
3257            n2_rx.try_recv().is_err(),
3258            "node=Some(\"n1\") dispatch must not reach the n2 worker"
3259        );
3260
3261        on_n1.deregister()?;
3262        on_n2.deregister()?;
3263        Ok(())
3264    }
3265
3266    /// An unpinned (`node = None`) dispatch is byte-identical to today: it
3267    /// reaches a worker in the pool regardless of the worker's advertised node.
3268    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3269    async fn unpinned_dispatch_reaches_a_pooled_worker_regardless_of_node()
3270    -> Result<(), Box<dyn std::error::Error>> {
3271        let registry = ConnectedWorkerRegistry::default();
3272        let pending = PendingActivities::new(TEST_HEARTBEAT_WINDOW);
3273        let activity_types = [String::from("greet")];
3274        let (n1_tx, mut n1_rx) = tokio::sync::mpsc::channel(32);
3275        let on_n1 = registry.register_namespaces(
3276            [String::from("default")],
3277            "default",
3278            Some(String::from("n1")),
3279            activity_types.iter(),
3280            n1_tx,
3281            crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
3282        )?;
3283
3284        let sink = pending.clone();
3285        let echo = tokio::spawn(async move {
3286            let Some(WorkerMessage::ActivityTask(task)) = n1_rx.recv().await else {
3287                return Err("expected an activity task on the worker channel".to_owned());
3288            };
3289            let workflow_id = task
3290                .workflow_id
3291                .ok_or("task missing workflow id")
3292                .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
3293            let activity_id = task
3294                .activity_id
3295                .map(ActivityId::from)
3296                .ok_or("task missing activity id")?;
3297            let completion_token =
3298                CompletionToken::from_wire(&workflow_id, &activity_id, task.completion_token)
3299                    .map_err(|error| error.to_string())?;
3300            sink.complete_activity(ActivityCompletion {
3301                workflow_id,
3302                activity_id,
3303                run_id: None,
3304                completion_token,
3305                outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
3306                    ContentType::Json,
3307                    br#"{"greeting":"hello"}"#.to_vec(),
3308                )),
3309            })
3310            .map_err(|error| error.to_string())
3311        });
3312
3313        let dispatcher = Arc::new(
3314            WorkerActivityDispatcher::new(registry.clone(), "default", test_tracker())
3315                .with_pending(pending),
3316        );
3317
3318        // greet_request() carries node: None — the unpinned path.
3319        let result = tokio::spawn(futures::future::lazy(move |_| {
3320            dispatcher.dispatch(greet_request())
3321        }))
3322        .await
3323        .map_err(|error| error.to_string())?;
3324
3325        assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
3326        echo.await.map_err(|error| error.to_string())??;
3327        on_n1.deregister()?;
3328        Ok(())
3329    }
3330}