Skip to main content

cortex/dispatcher/
server.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3use std::sync::mpsc::SyncSender;
4use std::thread;
5use std::time::{Duration, Instant};
6
7use dashmap::DashMap;
8use tracing::{debug, error, warn};
9
10use crate::backend::Backend;
11use crate::helpers::{NewTaskMessage, TaskProgress, TaskReport, TaskStatus};
12use crate::models::{Service, Task};
13
14/// Probe interval (seconds) between TCP keepalive probes once the idle threshold is crossed, and
15/// the number of unanswered probes before the OS declares the peer dead — fixed sane values so only
16/// the idle threshold needs to be a config knob (see [`crate::config::DispatcherConfig`]). With the
17/// defaults a dead worker is detected in roughly `idle + 4×30 s` ≈ a few minutes — far faster than
18/// the 1 h lease, while keeping NAT mappings warm.
19const KEEPALIVE_PROBE_INTERVAL_SECS: i32 = 30;
20/// See [`KEEPALIVE_PROBE_INTERVAL_SECS`].
21const KEEPALIVE_PROBE_COUNT: i32 = 4;
22
23/// Applies TCP keepalive to a worker-facing ZMQ socket (the ventilator's ROUTER, the sink's PULL),
24/// the "stable" half of a remote worker interface: keepalive keeps idle worker connections alive
25/// across NAT/firewall idle-timeouts (an idle mapping is otherwise silently dropped, dropping the
26/// worker from the fleet until it reconnects) and lets the OS reap a dead peer's route. It does
27/// **not** affect task-recovery correctness — the lease reaper is that net — so `idle_seconds <= 0`
28/// safely disables it (OS default). **Set before `bind`** so accepted connections inherit it.
29pub fn apply_tcp_keepalive(socket: &zmq::Socket, idle_seconds: i32) -> zmq::Result<()> {
30  if idle_seconds <= 0 {
31    return Ok(());
32  }
33  socket.set_tcp_keepalive(1)?;
34  socket.set_tcp_keepalive_idle(idle_seconds)?;
35  socket.set_tcp_keepalive_intvl(KEEPALIVE_PROBE_INTERVAL_SECS)?;
36  socket.set_tcp_keepalive_cnt(KEEPALIVE_PROBE_COUNT)?;
37  Ok(())
38}
39
40/// **Graceful-shutdown request flag** (O-1, orchestration). Set by the SIGTERM/SIGINT handler the
41/// dispatcher binary installs; the ventilator checks it each loop iteration and, when set, signals
42/// completion (`dispatch_complete`) and returns — so the manager drains the in-flight set and
43/// flushes the finalize batch before exiting, rather than the supervisor hard-killing in-flight
44/// work. ONLY a **planned** stop uses this; unexpected failures still fail-fast (panic → abort).
45/// Production-only — bounded test runs never install the handler, so this stays `false` and the
46/// dispatch path is byte-for-byte unchanged.
47pub static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
48
49/// SIGTERM/SIGINT handler. The only thing it does is an async-signal-safe atomic store — nothing
50/// allocating or locking, as `signal(2)` requires.
51extern "C" fn handle_shutdown_signal(_sig: libc::c_int) {
52  SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
53}
54
55/// Installs SIGTERM + SIGINT handlers that request a graceful drain-and-stop. Call once from the
56/// dispatcher binary's `main` (never in bounded test runs).
57pub fn install_shutdown_handlers() {
58  // SAFETY: `handle_shutdown_signal` is async-signal-safe (a lone atomic store).
59  unsafe {
60    libc::signal(
61      libc::SIGTERM,
62      handle_shutdown_signal as *const () as libc::sighandler_t,
63    );
64    libc::signal(
65      libc::SIGINT,
66      handle_shutdown_signal as *const () as libc::sighandler_t,
67    );
68  }
69}
70
71/// Whether a graceful shutdown has been requested (set by [`install_shutdown_handlers`]'s handler).
72#[must_use]
73pub fn shutdown_requested() -> bool { SHUTDOWN_REQUESTED.load(Ordering::SeqCst) }
74
75/// The dispatcher's **service cache**: a `service_name → Option<Service>` memo shared by the
76/// ventilator (populated on a cache miss) and the sink (read on every result), so a `Service` row
77/// is looked up from the DB at most once per name. Phase 4 of the dispatcher rationalization
78/// replaces the old `Arc<Mutex<HashMap>>` with a sharded `DashMap`, so this near-static,
79/// read-mostly cache is no longer behind a single global lock contended on every dispatch.
80pub type ServiceCache = DashMap<String, Option<Service>>;
81
82/// The dispatcher's **sandbox cache**: a `corpus_id → Option<sandbox_id>` memo, the corpus-keyed
83/// twin of [`ServiceCache`]. `Some(id)` means the corpus is a sandbox (Arm 5) whose result archives
84/// are name-scoped by its own id; `None` means an ordinary corpus. Sandbox-ness is immutable per
85/// corpus (a corpus is born sandbox-or-not and never flips), so a one-time DB lookup per
86/// `corpus_id` is correct forever — no invalidation. The ventilator memoises it on dispatch (it
87/// holds a `Backend`), so the sink can scope a result's output path with a lock-free read — no
88/// per-result DB hit — keeping a sandbox rerun from overwriting the parent's archives (KNOWN_ISSUES
89/// F-6).
90pub type SandboxCache = DashMap<i32, Option<i32>>;
91
92/// Rate-limited logging for high-frequency, low-value events — the dispatcher's *discarded*
93/// messages (malformed replies/requests, unknown service names, unknown task ids). The dispatcher's
94/// request and sink loops would otherwise `warn!` one line per skipped message; under a **sustained
95/// flood** (a hostile or buggy peer spamming bad frames) even leveled logging serialises a
96/// synchronous write per event and *slows the real pipeline* — a self-inflicted throughput-DoS
97/// (KNOWN_ISSUES D-11). This aggregates instead: it counts events and signals an
98/// emit at most once per `interval` (plus the very first event, so a problem is visible
99/// immediately), carrying the suppressed count. So a flood of any size costs **O(1) log I/O per
100/// interval**, not O(flood) — *counted, not narrated* — while a genuine trickle is still surfaced.
101/// Per-thread (no sharing/locking); the clock read per event is a cheap monotonic `Instant::now()`.
102pub struct RateLimitedLog {
103  events_since_emit: u64,
104  last_emit: Option<Instant>,
105  interval: Duration,
106}
107
108impl RateLimitedLog {
109  /// A new aggregator that emits at most once per `interval` (after an immediate first emit).
110  pub fn new(interval: Duration) -> Self {
111    RateLimitedLog {
112      events_since_emit: 0,
113      last_emit: None,
114      interval,
115    }
116  }
117
118  /// Records one event. Returns `Some(count)` — the number of events since the last emit, inclusive
119  /// — when a summary line is due (the first event always, then at most once per `interval`),
120  /// resetting the counter; otherwise `None` (suppress; just counted).
121  pub fn record(&mut self) -> Option<u64> {
122    self.events_since_emit += 1;
123    let due = match self.last_emit {
124      None => true,
125      Some(at) => at.elapsed() >= self.interval,
126    };
127    if due {
128      let count = self.events_since_emit;
129      self.events_since_emit = 0;
130      self.last_emit = Some(Instant::now());
131      Some(count)
132    } else {
133      None
134    }
135  }
136}
137
138/// Hard ceiling on the in-flight (progress) set. Reaching it means backpressure
139/// ([`crate::config::DispatcherConfig::max_in_flight`]) failed to hold the line, so we fail fast
140/// (panic → process abort → external restart) rather than exhaust memory — the dispatcher's
141/// intentional fail-fast design. Backpressure must engage *below* this (asserted in tests).
142pub const PROGRESS_QUEUE_HARD_LIMIT: usize = 10_000;
143/// Capacity of the bounded done (results-pending-persist) channel between the producers (sink +
144/// ventilator reaper) and the finalize thread. A **full** channel *blocks* the producer — that *is*
145/// the backpressure (a slow DB makes the sink wait, which backs up the ZMQ PULL, which backs up the
146/// workers), replacing the old `DONE_QUEUE_HARD_LIMIT` panic-then-OOM backstop with a graceful,
147/// loss-free hand-off (a bounded channel blocks rather than drops). Phase 1 of the dispatcher
148/// rationalization (`docs/archive/DISPATCHER_RATIONALIZATION.md`).
149pub const DONE_QUEUE_CAPACITY: usize = 10_000;
150
151/// Whether the in-flight set is saturated and the ventilator should apply backpressure (stop
152/// leasing new work and mock-reply). Saturation is inclusive: at the threshold we already hold
153/// back, keeping the set bounded *below* [`PROGRESS_QUEUE_HARD_LIMIT`].
154pub fn in_flight_saturated(in_flight: usize, max_in_flight: usize) -> bool {
155  in_flight >= max_in_flight
156}
157
158/// One task id's entry in the [`InFlightSet`].
159#[derive(Clone, Debug)]
160enum TaskSlot {
161  /// Leased to a worker — counted by [`InFlightSet::len`].
162  InFlight(Leased),
163  /// Out of flight, but a late result may still arrive for it (KNOWN_ISSUES D-26): released by
164  /// the reaper (re-queued or dead-lettered), or finished by an older attempt while a newer one
165  /// still runs.
166  Settling(Settling),
167}
168
169/// A leased task and what has come back for it so far.
170#[derive(Clone, Debug)]
171struct Leased {
172  /// the lease
173  progress: TaskProgress,
174  /// results received for the task that are still with their archive writer: the reaper leaves
175  /// the task alone until they are decided ([`InFlightSet::note_received`])
176  pending: u32,
177  /// the current attempt's worker has answered with something that is not a result — an
178  /// unusable archive (D-18) or a retryable cause: only the reaper moves the task on, and a
179  /// bounded run's sink does not wait for it ([`InFlightSet::awaiting_replies`])
180  answered: bool,
181}
182
183impl Leased {
184  fn new(progress: TaskProgress) -> Self {
185    Leased {
186      progress,
187      pending: 0,
188      answered: false,
189    }
190  }
191
192  /// The attempt in flight (0-based).
193  fn current_attempt(&self) -> usize { self.progress.attempt_workers.len().saturating_sub(1) }
194}
195
196/// A task out of flight that may still receive a late result: what the sink needs to accept one.
197#[derive(Clone, Debug)]
198struct Settling {
199  /// the task, for the report
200  task: Task,
201  /// the worker of each dispatch attempt (see [`TaskProgress::attempt_workers`])
202  attempt_workers: Vec<String>,
203  /// the newest attempt whose result was accepted, with that result's status, if any
204  finished: Option<(usize, TaskStatus)>,
205  /// whether the reaper dead-lettered the task (reported its stand-in Fatal)
206  dead_lettered: bool,
207  /// the last retryable cause reported for the task, carried into its queued retry's lease
208  last_cause: Vec<NewTaskMessage>,
209  /// when to forget it (unix seconds)
210  forget_at: i64,
211  /// the first attempt of the task's current run (see [`TaskProgress::run_start`]): results of
212  /// earlier attempts belong to an earlier run and never land
213  run_start: usize,
214  /// the retry count of the expired lease, its lease timeout, and whether its retry is queued in
215  /// the ventilator — what a restarted ventilator needs to queue it again
216  /// ([`InFlightSet::queued_retries`])
217  retries: i64,
218  lease_timeout_seconds: i64,
219  queued: bool,
220}
221
222/// Why an accepted result is late (KNOWN_ISSUES D-26) — the sink logs it.
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224pub enum LateResult {
225  /// An older attempt's result while a newer attempt is in flight (the older lease expired and
226  /// the task was re-leased). The newer attempt may still report, and would then replace it.
227  OlderAttempt,
228  /// The reaper had re-queued the task; it is not re-leased now.
229  AfterRequeue,
230  /// The reaper had dead-lettered the task; this replaces its stand-in Fatal.
231  AfterDeadLetter,
232  /// A newer attempt than the one whose result was accepted earlier; this replaces it.
233  NewerAttempt,
234  /// A usable result of an older attempt replacing a newer attempt's accepted failure ("good
235  /// result wins").
236  ReplacesFailure,
237}
238
239/// What the sink may do with a result, decided by [`InFlightSet::claim_result`].
240#[derive(Clone, Debug)]
241pub enum ResultClaim {
242  /// Report it for `task`: it is the task's newest finished attempt.
243  Accept {
244    /// the task the result is for
245    task: Task,
246    /// the attempt it belongs to (0-based)
247    attempt: usize,
248    /// how many attempts were dispatched
249    attempts: usize,
250    /// `Some` when the result came after its lease expired
251    late: Option<LateResult>,
252    /// the earlier accepted result this one replaces (its attempt and status), if any — restored
253    /// by [`InFlightSet::note_publish_failed`] if this one cannot be published
254    replaced: Option<(usize, TaskStatus)>,
255  },
256  /// Drop it: a result already accepted stands — a newer attempt's, or a usable one that a newer
257  /// attempt's failure (`Fatal`/`Invalid`) must not replace.
258  Superseded {
259    /// the attempt it belongs to
260    attempt: usize,
261    /// the attempt whose result stands
262    finished: usize,
263  },
264  /// Drop it: it belongs to an earlier run of the task (an operator re-ran it since — D-28).
265  Stale {
266    /// the attempt it belongs to
267    attempt: usize,
268  },
269  /// No such task in flight or in its late-result window: drop it.
270  Unknown,
271}
272
273/// What became of a retryable cause ([`InFlightSet::defer_retryable`]).
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub enum RetryDecision {
276  /// The task goes back to the retry budget, with this cause as its last one: the next reap
277  /// re-queues it.
278  Deferred,
279  /// The cause came from the task's last attempt (its budget is spent): the caller publishes its
280  /// archive and reports it as a **stand-in** Fatal for `task` — the DB and the archive on disk
281  /// agree, and any attempt's late result still replaces it.
282  Final {
283    /// the task the stand-in is for
284    task: Task,
285  },
286  /// Nothing to retry: the reason, for the log.
287  Dropped(&'static str),
288}
289
290/// Whether a result's status is a failure (`Fatal`/`Invalid`), which never replaces an accepted
291/// usable result (D-26).
292fn is_failure(status: TaskStatus) -> bool {
293  matches!(status, TaskStatus::Fatal | TaskStatus::Invalid)
294}
295
296/// The attempt a reply from `worker` belongs to: the newest attempt dispatched to that worker, or
297/// the newest attempt of all when the worker is not among them (an identity the ventilator did not
298/// record — attributing it to the newest attempt means it can never overwrite a newer result).
299fn attempt_of(attempt_workers: &[String], worker: &str) -> usize {
300  attempt_workers
301    .iter()
302    .rposition(|w| w == worker)
303    .unwrap_or_else(|| attempt_workers.len().saturating_sub(1))
304}
305
306/// Whether an expired task has spent its retry budget (the reaper dead-letters it).
307fn retries_exhausted(progress: &TaskProgress) -> bool { progress.retries > MAX_DISPATCH_RETRIES }
308
309/// Hard ceiling on the tasks held for late results. Past it a reaped task gets no late-result
310/// window (it is re-leased or dead-lettered as before), so the memory stays bounded whatever the
311/// reaping rate.
312pub const SETTLING_HARD_LIMIT: usize = 50_000;
313
314/// The default late-result window when none is configured (seconds; see
315/// [`crate::config::DispatcherConfig::late_result_window_seconds`]).
316pub const DEFAULT_LATE_RESULT_WINDOW_SECONDS: i64 = 3600;
317
318/// The dispatcher's **in-flight (progress) set**: the dispatched-but-unfinished tasks, keyed by
319/// task id. Phase 4 of the dispatcher rationalization replaces the contended
320/// `Arc<Mutex<HashMap<i64, TaskProgress>>>` with a sharded, lock-free [`DashMap`] — so the
321/// ventilator's lease ([`Self::lease`]), the sink's return ([`Self::claim_result`]), and the reaper
322/// sweep ([`Self::take_expired`]) no longer serialise on one global lock — plus an [`AtomicUsize`]
323/// size counter so the per-request backpressure check ([`in_flight_saturated`]) reads the size in
324/// **O(1)** without locking or scanning the map.
325///
326/// It also holds, for [`late_result_window`](Self::with_late_result_window) seconds, each task the
327/// reaper released (re-queued or dead-lettered), with the worker of every attempt, so the sink can
328/// accept a result whose lease expired unless a result that must stand already landed
329/// (KNOWN_ISSUES D-26). Those entries are not in flight: [`Self::len`], [`Self::is_empty`] and
330/// [`Self::ids`] count only leased tasks. Every transition of one task id happens under its map
331/// shard's lock, so a lease, a result and a reap of the same task cannot interleave.
332///
333/// The counters are maintained in lock-step with the map *here* (the only place any is mutated),
334/// so they converge to exactly the map's state; a momentary ±1 skew between a map mutation and its
335/// atomic update is harmless for backpressure (self-correcting, never a leak). Shared as
336/// `Arc<InFlightSet>` across the ventilator / sink / reaper threads.
337pub struct InFlightSet {
338  map: DashMap<i64, TaskSlot>,
339  len: AtomicUsize,
340  answered: AtomicUsize,
341  settling: AtomicUsize,
342  late_result_window: i64,
343}
344
345impl Default for InFlightSet {
346  fn default() -> Self { Self::with_late_result_window(DEFAULT_LATE_RESULT_WINDOW_SECONDS) }
347}
348
349impl InFlightSet {
350  /// A new, empty in-flight set with the default late-result window.
351  pub fn new() -> Self { Self::default() }
352
353  /// A new, empty in-flight set that keeps reaped tasks for late results for `seconds` (`<= 0`:
354  /// no late results are accepted).
355  pub fn with_late_result_window(seconds: i64) -> Self {
356    InFlightSet {
357      map: DashMap::new(),
358      len: AtomicUsize::new(0),
359      answered: AtomicUsize::new(0),
360      settling: AtomicUsize::new(0),
361      late_result_window: seconds,
362    }
363  }
364
365  /// Current number of in-flight tasks — an **O(1)** atomic load (the backpressure hot read), not a
366  /// map scan.
367  pub fn len(&self) -> usize { self.len.load(Ordering::Acquire) }
368
369  /// Whether the in-flight set is empty.
370  pub fn is_empty(&self) -> bool { self.len() == 0 }
371
372  /// Number of in-flight tasks still awaiting a reply: [`Self::len`] less those whose current
373  /// attempt answered with an unusable result or a retryable cause, which only the reaper moves
374  /// on. A bounded run's sink stops once dispatching is done and this is zero.
375  pub fn awaiting_replies(&self) -> usize {
376    self
377      .len()
378      .saturating_sub(self.answered.load(Ordering::Acquire))
379  }
380
381  /// Number of tasks held out of flight for a late result.
382  pub fn settling_len(&self) -> usize { self.settling.load(Ordering::Acquire) }
383
384  /// Fail fast past [`PROGRESS_QUEUE_HARD_LIMIT`] (backpressure failed to hold the line): panic →
385  /// process abort → external restart, rather than grow unbounded.
386  fn check_hard_limit(&self) {
387    if self.len() > PROGRESS_QUEUE_HARD_LIMIT {
388      panic!(
389        "Progress queue is too large: {:?} tasks. Stop the ventilator!",
390        self.len()
391      );
392    }
393  }
394
395  /// Account for a slot that leaves the set or is replaced.
396  fn note_left(&self, slot: &TaskSlot) {
397    match slot {
398      TaskSlot::InFlight(leased) => {
399        self.len.fetch_sub(1, Ordering::AcqRel);
400        if leased.answered {
401          self.answered.fetch_sub(1, Ordering::AcqRel);
402        }
403      },
404      TaskSlot::Settling(_) => {
405        self.settling.fetch_sub(1, Ordering::AcqRel);
406      },
407    }
408  }
409
410  /// Account for a slot that enters the set.
411  fn note_entered(&self, slot: &TaskSlot) {
412    match slot {
413      TaskSlot::InFlight(leased) => {
414        self.len.fetch_add(1, Ordering::AcqRel);
415        if leased.answered {
416          self.answered.fetch_add(1, Ordering::AcqRel);
417        }
418      },
419      TaskSlot::Settling(_) => {
420        self.settling.fetch_add(1, Ordering::AcqRel);
421      },
422    }
423  }
424
425  /// Put `slot` at `id`, keeping the counters in step.
426  fn put(&self, id: i64, slot: TaskSlot) {
427    self.note_entered(&slot);
428    if let Some(previous) = self.map.insert(id, slot) {
429      self.note_left(&previous);
430    }
431  }
432
433  /// Record a dispatched task as in-flight (keyed by `task.id`), unconditionally. Re-inserting the
434  /// same id overwrites without double-counting. The ventilator leases through [`Self::lease`].
435  pub fn insert(&self, progress_task: TaskProgress) {
436    self.put(
437      progress_task.task.id,
438      TaskSlot::InFlight(Leased::new(progress_task)),
439    );
440    self.check_hard_limit();
441  }
442
443  /// Lease a task: record it in flight, unless it is a re-queued retry that a late result already
444  /// finished while it waited in the dispatch queue (KNOWN_ISSUES D-26) — then `false`, and the
445  /// caller drops it instead of converting it again. A first attempt (`retries == 0`: a fresh
446  /// fetch, e.g. an operator's rerun of the task) is always leased; a held task's earlier
447  /// attempts become its older attempts. A retry takes the last retryable cause reported while
448  /// it was queued.
449  pub fn lease(&self, progress_task: TaskProgress) -> bool {
450    use dashmap::mapref::entry::Entry;
451    let retry = progress_task.retries > 0;
452    let mut progress_task = progress_task;
453    let leased = match self.map.entry(progress_task.task.id) {
454      Entry::Occupied(mut slot) => {
455        let refused = match slot.get() {
456          TaskSlot::Settling(held) if retry => {
457            if held.finished.is_none() && !held.last_cause.is_empty() {
458              progress_task.last_cause = held.last_cause.clone();
459            }
460            held.finished.is_some()
461          },
462          // A fresh fetch of a task the set still holds — in flight or held — is an operator's
463          // rerun: a new run (D-28). Its earlier attempts stay on record as an earlier run's, so
464          // their late results are told apart and never land in it.
465          held if !retry => {
466            let earlier = match held {
467              TaskSlot::InFlight(leased) => &leased.progress.attempt_workers,
468              TaskSlot::Settling(settling) => &settling.attempt_workers,
469            };
470            let mut workers = earlier.clone();
471            progress_task.run_start = workers.len();
472            workers.append(&mut progress_task.attempt_workers);
473            progress_task.attempt_workers = workers;
474            false
475          },
476          // A retry over an in-flight slot whose run began after the retry's: the retry belongs to
477          // an earlier run, which a rerun superseded — never let it take the new run's slot.
478          TaskSlot::InFlight(current) if retry => {
479            current.progress.run_start > progress_task.run_start
480          },
481          _ => false,
482        };
483        if refused {
484          false
485        } else {
486          let slot_value = TaskSlot::InFlight(Leased::new(progress_task));
487          self.note_entered(&slot_value);
488          let previous = slot.insert(slot_value);
489          self.note_left(&previous);
490          true
491        }
492      },
493      Entry::Vacant(slot) => {
494        let slot_value = TaskSlot::InFlight(Leased::new(progress_task));
495        self.note_entered(&slot_value);
496        slot.insert(slot_value);
497        true
498      },
499    };
500    self.check_hard_limit();
501    leased
502  }
503
504  /// Remove and return the in-flight task with `taskid`. Negative (mock-reply) ids are never
505  /// tracked, so they short-circuit to `None`; removing an already-gone task is a no-op that never
506  /// underflows the counter, and a task held for a late result is left alone.
507  pub fn remove(&self, taskid: i64) -> Option<TaskProgress> {
508    if taskid < 0 {
509      // Mock ids are to be skipped
510      return None;
511    }
512    let (_, slot) = self
513      .map
514      .remove_if(&taskid, |_, slot| matches!(slot, TaskSlot::InFlight(_)))?;
515    self.note_left(&slot);
516    match slot {
517      TaskSlot::InFlight(leased) => Some(leased.progress),
518      TaskSlot::Settling(_) => None,
519    }
520  }
521
522  /// The task a result for `taskid` would be reported against — in flight or held for a late
523  /// result — without claiming it. The sink checks the result's service and output path against
524  /// it first, so an unplaceable result leaves the task exactly where it was (the reaper's).
525  pub fn task(&self, taskid: i64) -> Option<Task> {
526    if taskid < 0 {
527      return None;
528    }
529    self.map.get(&taskid).map(|slot| match slot.value() {
530      TaskSlot::InFlight(leased) => leased.progress.task.clone(),
531      TaskSlot::Settling(settling) => settling.task.clone(),
532    })
533  }
534
535  /// A result for in-flight task `taskid` was received and handed to its archive writer: the
536  /// reaper leaves the task alone until the writer decides the result ([`Self::claim_result`],
537  /// [`Self::defer_retryable`], [`Self::note_unusable_reply`]), so a result that arrives at the
538  /// deadline is not raced by a re-lease.
539  pub fn note_received(&self, taskid: i64) {
540    if let Some(mut slot) = self.map.get_mut(&taskid)
541      && let TaskSlot::InFlight(leased) = slot.value_mut()
542    {
543      leased.pending += 1;
544    }
545  }
546
547  /// The worker `worker` replied for in-flight task `taskid` with a result that could not be used
548  /// (empty, unreadable, unwritable, unplaceable — D-18): the task stays in flight for the reaper
549  /// (retry, then dead-letter). If the reply is the current attempt's, the task awaits no reply.
550  /// `received` says whether the reply was recorded by [`Self::note_received`] (an unplaceable
551  /// reply never was).
552  pub fn note_unusable_reply(&self, taskid: i64, worker: &str, received: bool) {
553    if let Some(mut slot) = self.map.get_mut(&taskid)
554      && let TaskSlot::InFlight(leased) = slot.value_mut()
555    {
556      if received {
557        leased.pending = leased.pending.saturating_sub(1);
558      }
559      if attempt_of(&leased.progress.attempt_workers, worker) == leased.current_attempt()
560        && !leased.answered
561      {
562        leased.answered = true;
563        self.answered.fetch_add(1, Ordering::AcqRel);
564      }
565    }
566  }
567
568  /// The worker `worker` reported a **retryable** cause for task `taskid` (the harness's
569  /// `Fatal:timeout:wallclock`, `Fatal:oom:shed`, `Fatal:crash:unrecorded`, tagged
570  /// `Info:cortex:retryable`): not a result to record, but the task's retry budget's business.
571  /// For the attempt in flight with budget left, the cause becomes the task's last cause and its
572  /// lease is due now: the next reap re-queues it ([`RetryDecision::Deferred`]). On the task's last
573  /// attempt the cause is its final record ([`RetryDecision::Final`]): the caller publishes it as a
574  /// stand-in, held like an accepted failure. For a task re-queued meanwhile, the newest attempt's
575  /// cause rides along to its retry. Otherwise — an older attempt's cause, a task whose result
576  /// already landed, or one already dead-lettered — it is dropped.
577  pub fn defer_retryable(
578    &self,
579    taskid: i64,
580    worker: &str,
581    cause: Vec<NewTaskMessage>,
582  ) -> RetryDecision {
583    use dashmap::mapref::entry::Entry;
584    let Entry::Occupied(mut slot) = self.map.entry(taskid) else {
585      return RetryDecision::Dropped("the task is no longer in flight or held");
586    };
587    match slot.get_mut() {
588      TaskSlot::InFlight(leased) => {
589        leased.pending = leased.pending.saturating_sub(1);
590        if attempt_of(&leased.progress.attempt_workers, worker) != leased.current_attempt() {
591          return RetryDecision::Dropped("an older attempt's cause; a newer attempt is running");
592        }
593        if retries_exhausted(&leased.progress) {
594          // The last attempt: the cause is the task's stand-in, accepted as a failure and held for
595          // a late usable result ("good result wins"), so a later publish failure restores it —
596          // or, with no window or the hold cap reached, taken out of flight like any final result.
597          let task = leased.progress.task.clone();
598          if self.late_result_window > 0 && self.settling_len() < SETTLING_HARD_LIMIT {
599            let held = TaskSlot::Settling(Settling {
600              task: task.clone(),
601              attempt_workers: leased.progress.attempt_workers.clone(),
602              finished: Some((leased.current_attempt(), TaskStatus::Fatal)),
603              dead_lettered: true,
604              last_cause: Vec::new(),
605              forget_at: chrono::Utc::now().timestamp() + self.late_result_window,
606              run_start: leased.progress.run_start,
607              retries: leased.progress.retries,
608              lease_timeout_seconds: leased.progress.lease_timeout_seconds,
609              queued: false,
610            });
611            self.note_entered(&held);
612            let previous = slot.insert(held);
613            self.note_left(&previous);
614          } else {
615            // Removed through the entry, under its lock: the reaper cannot dead-letter it between.
616            let (_, previous) = slot.remove_entry();
617            self.note_left(&previous);
618          }
619          return RetryDecision::Final { task };
620        }
621        leased.progress.last_cause = cause;
622        // Due now: `expected_at` is measured from the attempt's dispatch.
623        leased.progress.created_at = 0;
624        if !leased.answered {
625          leased.answered = true;
626          self.answered.fetch_add(1, Ordering::AcqRel);
627        }
628        RetryDecision::Deferred
629      },
630      TaskSlot::Settling(held) => {
631        if held.finished.is_some() {
632          RetryDecision::Dropped("a result for the task already landed")
633        } else if held.dead_lettered {
634          RetryDecision::Dropped("the task was already dead-lettered")
635        } else if attempt_of(&held.attempt_workers, worker)
636          != held.attempt_workers.len().saturating_sub(1)
637        {
638          RetryDecision::Dropped("an older attempt's cause; the newest attempt's stands")
639        } else {
640          held.last_cause = cause;
641          RetryDecision::Deferred
642        }
643      },
644    }
645  }
646
647  /// Decide a result with status `status` for `taskid` from the worker `worker` (KNOWN_ISSUES
648  /// D-26): accept it unless a result that must stand already landed — a newer attempt's, or a
649  /// usable one that this failure (`Fatal`/`Invalid`) must not replace. An accepted result takes
650  /// the task out of flight; if it belongs to an older attempt than the one in flight, the task is
651  /// held so the newer attempt's result can still replace it, and a result for a re-queued task
652  /// keeps the queued retry from being leased ([`Self::lease`]).
653  pub fn claim_result(&self, taskid: i64, worker: &str, status: TaskStatus) -> ResultClaim {
654    use dashmap::mapref::entry::Entry;
655    if taskid < 0 {
656      return ResultClaim::Unknown;
657    }
658    let now = chrono::Utc::now().timestamp();
659    let Entry::Occupied(mut slot) = self.map.entry(taskid) else {
660      return ResultClaim::Unknown;
661    };
662    let held = match slot.get_mut() {
663      TaskSlot::InFlight(leased) => {
664        let attempts = leased.progress.attempt_workers.len().max(1);
665        let attempt = attempt_of(&leased.progress.attempt_workers, worker);
666        if attempt < leased.progress.run_start {
667          // The slot stays in flight for the new run: release the receipt this result took, or
668          // the reaper would never take the new lease (`take_expired` needs `pending == 0`).
669          leased.pending = leased.pending.saturating_sub(1);
670          return ResultClaim::Stale { attempt };
671        }
672        let task = leased.progress.task.clone();
673        let older = attempt + 1 < attempts;
674        // Held for the late-result window when a later result may still replace this one: a newer
675        // attempt is running, or this one is a failure that any usable result replaces ("good
676        // result wins", user ruling 2026-10-12).
677        // A single-attempt failure has nothing that could replace it: not held (the hold cap is
678        // for tasks that may still get a better result).
679        let hold = ((older || (is_failure(status) && attempts > 1))
680          && self.late_result_window > 0
681          && self.settling_len() < SETTLING_HARD_LIMIT)
682          .then(|| {
683            TaskSlot::Settling(Settling {
684              task: task.clone(),
685              attempt_workers: leased.progress.attempt_workers.clone(),
686              finished: Some((attempt, status)),
687              dead_lettered: false,
688              last_cause: Vec::new(),
689              forget_at: now + self.late_result_window,
690              run_start: leased.progress.run_start,
691              retries: leased.progress.retries,
692              lease_timeout_seconds: leased.progress.lease_timeout_seconds,
693              queued: false,
694            })
695          });
696        let claim = ResultClaim::Accept {
697          task,
698          attempt,
699          attempts,
700          late: older.then_some(LateResult::OlderAttempt),
701          replaced: None,
702        };
703        (hold, claim)
704      },
705      TaskSlot::Settling(settling) => {
706        let attempts = settling.attempt_workers.len().max(1);
707        let attempt = attempt_of(&settling.attempt_workers, worker);
708        if attempt < settling.run_start {
709          return ResultClaim::Stale { attempt };
710        }
711        // A usable result replaces a failure and a failure never replaces a usable result,
712        // whatever attempts produced them ("good result wins"); between results of the same kind,
713        // the newer attempt's wins.
714        let late = match settling.finished {
715          Some((finished, finished_status)) => {
716            let replaces = match (is_failure(finished_status), is_failure(status)) {
717              (true, false) => true,
718              (false, true) => false,
719              _ => attempt > finished,
720            };
721            if !replaces {
722              return ResultClaim::Superseded { attempt, finished };
723            }
724            if attempt > finished {
725              LateResult::NewerAttempt
726            } else {
727              LateResult::ReplacesFailure
728            }
729          },
730          None if settling.dead_lettered => LateResult::AfterDeadLetter,
731          None => LateResult::AfterRequeue,
732        };
733        let replaced = settling.finished;
734        settling.finished = Some((attempt, status));
735        settling.forget_at = now + self.late_result_window;
736        return ResultClaim::Accept {
737          task: settling.task.clone(),
738          attempt,
739          attempts,
740          late: Some(late),
741          replaced,
742        };
743      },
744    };
745    let (hold, claim) = held;
746    match hold {
747      Some(settling) => {
748        self.note_entered(&settling);
749        let previous = slot.insert(settling);
750        self.note_left(&previous);
751      },
752      None => {
753        let (_, previous) = slot.remove_entry();
754        self.note_left(&previous);
755      },
756    }
757    claim
758  }
759
760  /// The accepted result for held task `taskid` could not be published: restore the result it
761  /// replaced (`replaced`, from [`ResultClaim::Accept`]), or, when it replaced none, leave the
762  /// task open for any later result — a publish failure never costs the task a good result.
763  pub fn note_publish_failed(&self, taskid: i64, replaced: Option<(usize, TaskStatus)>) {
764    if let Some(mut slot) = self.map.get_mut(&taskid)
765      && let TaskSlot::Settling(held) = slot.value_mut()
766    {
767      held.finished = replaced;
768    }
769  }
770
771  /// Take every in-flight task past its visibility-timeout deadline (the reaper sweep), except one
772  /// whose received result is still with its writer. Each one stays in the set, out of flight, for
773  /// the late-result window (unless the window is off or [`SETTLING_HARD_LIMIT`] is reached): a
774  /// result of any of its attempts may still land there.
775  pub fn take_expired(&self) -> Vec<TaskProgress> {
776    let now = chrono::Utc::now().timestamp();
777    let due = |slot: &TaskSlot| {
778      matches!(slot, TaskSlot::InFlight(leased)
779        if leased.pending == 0 && leased.progress.expected_at() < now)
780    };
781    let expired_keys: Vec<i64> = self
782      .map
783      .iter()
784      .filter(|entry| due(entry.value()))
785      .map(|entry| *entry.key())
786      .collect();
787    let mut expired_tasks = Vec::with_capacity(expired_keys.len());
788    for key in expired_keys {
789      use dashmap::mapref::entry::Entry;
790      let Entry::Occupied(mut slot) = self.map.entry(key) else {
791        continue;
792      };
793      // Claimed, re-leased or received since the scan?
794      let progress = match slot.get() {
795        TaskSlot::InFlight(leased) if due(slot.get()) => leased.progress.clone(),
796        _ => continue,
797      };
798      if self.late_result_window > 0 && self.settling_len() < SETTLING_HARD_LIMIT {
799        let held = TaskSlot::Settling(Settling {
800          task: progress.task.clone(),
801          attempt_workers: progress.attempt_workers.clone(),
802          finished: None,
803          dead_lettered: retries_exhausted(&progress),
804          last_cause: progress.last_cause.clone(),
805          forget_at: now + self.late_result_window,
806          run_start: progress.run_start,
807          retries: progress.retries,
808          lease_timeout_seconds: progress.lease_timeout_seconds,
809          // A retry the reaper is about to queue (`classify_expired` requeues what it does not
810          // dead-letter).
811          queued: !retries_exhausted(&progress),
812        });
813        self.note_entered(&held);
814        let previous = slot.insert(held);
815        self.note_left(&previous);
816      } else {
817        let (_, previous) = slot.remove_entry();
818        self.note_left(&previous);
819      }
820      expired_tasks.push(progress);
821    }
822    expired_tasks
823  }
824
825  /// Forget the tasks whose late-result window has passed. Returns how many. Run on the reaper's
826  /// cadence.
827  pub fn forget_settled(&self) -> usize {
828    let now = chrono::Utc::now().timestamp();
829    let mut forgotten = 0;
830    self.map.retain(|_, slot| match slot {
831      TaskSlot::Settling(settling) if settling.forget_at <= now => {
832        self.settling.fetch_sub(1, Ordering::AcqRel);
833        forgotten += 1;
834        false
835      },
836      _ => true,
837    });
838    forgotten
839  }
840
841  /// The retries the reaper queued in the ventilator that no lease or late result has taken yet,
842  /// rebuilt for a **restarted** ventilator (its queue died with it) to queue again rather than
843  /// reset to `TODO`: a reset task would be re-fetched as a new run, and its earlier attempts'
844  /// late results dropped as stale (D-28). Due at once (`created_at` 0).
845  pub fn queued_retries(&self) -> Vec<TaskProgress> {
846    self
847      .map
848      .iter()
849      .filter_map(|entry| match entry.value() {
850        TaskSlot::Settling(held) if held.queued && held.finished.is_none() => Some(TaskProgress {
851          task: held.task.clone(),
852          created_at: 0,
853          retries: held.retries + 1,
854          lease_timeout_seconds: held.lease_timeout_seconds,
855          attempt_workers: held.attempt_workers.clone(),
856          last_cause: held.last_cause.clone(),
857          run_start: held.run_start,
858        }),
859        _ => None,
860      })
861      .collect()
862  }
863
864  /// Whether `worker` worked an attempt of `taskid` that the set still holds — so a fresh fetch
865  /// leased to it would start a new run whose results could not be told from the old run's.
866  pub fn worked_earlier_run(&self, taskid: i64, worker: &str) -> bool {
867    self.map.get(&taskid).is_some_and(|slot| {
868      let workers = match slot.value() {
869        TaskSlot::InFlight(leased) => &leased.progress.attempt_workers,
870        TaskSlot::Settling(held) => &held.attempt_workers,
871      };
872      workers.iter().any(|w| w == worker)
873    })
874  }
875
876  /// The task ids a restarted ventilator must not reset to `TODO`: every task in flight (its
877  /// result may still come) and every task held for late results — a queued retry (queued again,
878  /// [`Self::queued_retries`]), or one whose report (a late result, a stand-in) may not be written
879  /// yet: a reset row is no longer at the report's lease mark, so finalize would drop it (D-28).
880  pub fn kept_on_restart(&self) -> Vec<i64> { self.map.iter().map(|entry| *entry.key()).collect() }
881
882  /// Snapshot of the currently in-flight task ids — used on a ventilator restart to **exclude**
883  /// these from the `Queued → TODO` crash-recovery reset (`clear_limbo_tasks_except`), so tasks the
884  /// sink is still processing are not re-leased mid-flight (KNOWN_ISSUES D-4).
885  pub fn ids(&self) -> Vec<i64> {
886    self
887      .map
888      .iter()
889      .filter(|entry| matches!(entry.value(), TaskSlot::InFlight(_)))
890      .map(|entry| *entry.key())
891      .collect()
892  }
893}
894
895/// Persists a **batch** of finished reports to the Task store, with bounded retry on a transient DB
896/// failure. The finalize thread drains the batch off the bounded done channel and hands it here in
897/// one `mark_done` call (amortizing the round-trip). On exhausted retries it returns `Err`, which
898/// the finalize thread propagates into a panic → the manager aborts the whole dispatcher — the
899/// intended fail-fast on a DB runaway (the channel design replaces the old mutex-poisoning that
900/// achieved the same propagation). A crash here loses nothing: the tasks remain `Queued` and
901/// recover on restart.
902pub fn mark_done_batch(backend: &mut Backend, reports: &[TaskReport]) -> Result<(), String> {
903  if reports.is_empty() {
904    return Ok(());
905  }
906  let request_time = chrono::Utc::now();
907  let mut success = false;
908  match backend.mark_done(reports) {
909    Err(e) => {
910      warn!(error = ?e, batch = reports.len(), "mark_done attempt failed");
911      // DB persist failed, retry
912      let mut retries = 0;
913      while retries < 3 {
914        thread::sleep(Duration::new(2, 0)); // wait 2 seconds before retrying, in case this is latency related
915        retries += 1;
916        match backend.mark_done(reports) {
917          Ok(_) => {
918            success = true;
919            break;
920          },
921          Err(e) => warn!(error = ?e, attempt = retries, "mark_done retry failed"),
922        };
923      }
924    },
925    _ => {
926      success = true;
927    },
928  }
929  if !success {
930    return Err(String::from(
931      "Database ran away during mark_done persisting.",
932    ));
933  }
934  let request_duration = (chrono::Utc::now() - request_time).num_milliseconds();
935  debug!(
936    count = reports.len(),
937    took_ms = request_duration,
938    "mark_done: persisted batch to DB"
939  );
940  Ok(())
941}
942
943/// Hands a finished report off to the finalize thread over the bounded done channel. A full channel
944/// **blocks** the producer (backpressure — see [`DONE_QUEUE_CAPACITY`]). An `Err` means the
945/// finalize receiver is gone (the thread died) — the report can't be persisted, but its task stays
946/// `Queued` and is recovered on restart, and the manager's supervision aborts the dispatcher, so we
947/// only log.
948pub fn send_done(done_tx: &SyncSender<TaskReport>, report: TaskReport) {
949  if done_tx.send(report).is_err() {
950    error!("done channel closed (finalize thread gone); the manager will abort for a restart");
951  }
952}
953
954/// The maximum number of dispatch retries before a perpetually-incomplete task is given up on.
955/// A task re-dispatched this many times that still never returns a result is treated as a hard
956/// failure (`Fatal`) rather than retried forever.
957///
958/// **1** with the short `lease_timeout_seconds` (~240 s, just above the worker's 180 s per-document
959/// timeout): a task whose worker keeps dying is almost always an unprocessable paper (a fresh
960/// recycle-clean worker dies on it too), so 2 retries (3 attempts total) catch the rare
961/// transient/worker-induced death and then converge to `Fatal` within a single run — the
962/// `(retries+1)×180` backoff cumulates to (1+2+3)×180 ≈ 1080 s, well inside a corpus pass —
963/// instead of the old 4 retries × 3600 s that stranded the task for hours.
964pub const MAX_DISPATCH_RETRIES: i64 = 1;
965
966/// The fate of a timed-out in-flight task, decided by [`classify_expired`].
967pub enum ExpiredOutcome {
968  /// Re-dispatch the task — retry budget remains; its retry count is incremented.
969  Requeue(TaskProgress),
970  /// Give up — retry budget exhausted; report the task `Fatal`.
971  Fatal(TaskReport),
972}
973
974/// Decides what to do with an in-flight task that timed out: retry it (until
975/// [`MAX_DISPATCH_RETRIES`] dispatches) or give up and report it `Fatal` — a stand-in that any
976/// attempt's late result replaces. Its message is the last retryable cause reported for the task
977/// (e.g. the harness's `Fatal:timeout:wallclock`), or `cortex` / `never_completed_with_retries`
978/// when no attempt reported one.
979pub fn classify_expired(expired: TaskProgress) -> ExpiredOutcome {
980  if retries_exhausted(&expired) {
981    let task_id = expired.task.id;
982    let messages = if expired.last_cause.is_empty() {
983      vec![NewTaskMessage::new(
984        task_id,
985        "fatal",
986        "cortex".to_string(),
987        crate::helpers::NEVER_COMPLETED_WHAT.to_string(),
988        String::new(),
989      )]
990    } else {
991      expired.last_cause
992    };
993    ExpiredOutcome::Fatal(TaskReport {
994      task: expired.task,
995      status: TaskStatus::Fatal,
996      messages,
997      stand_in: true,
998    })
999  } else {
1000    ExpiredOutcome::Requeue(TaskProgress {
1001      task: expired.task,
1002      // Re-stamped when the retry is leased (D-27).
1003      created_at: expired.created_at,
1004      retries: expired.retries + 1,
1005      // D-17: a re-lease keeps the per-task lease captured at first dispatch.
1006      lease_timeout_seconds: expired.lease_timeout_seconds,
1007      // D-26: the workers of the earlier attempts, to attribute their late results.
1008      attempt_workers: expired.attempt_workers,
1009      // The last retryable cause, for the dead-letter if the retries run out.
1010      last_cause: expired.last_cause,
1011      // A retry stays in its run.
1012      run_start: expired.run_start,
1013    })
1014  }
1015}
1016
1017/// Tally of one reaping pass — the dispatcher's re-lease / dead-letter health signal (Arm 8
1018/// observability). Returned by [`reap_expired_into`] so the ventilator can log it without the
1019/// reaper itself reaching for tracing.
1020#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1021pub struct ReapSummary {
1022  /// Timed-out tasks re-queued for another dispatch attempt (retry budget remained).
1023  pub requeued: usize,
1024  /// Timed-out tasks given up on and reported `Fatal` (retry budget exhausted) — dead-letters.
1025  pub dead_lettered: usize,
1026  /// Reaped tasks whose late-result window passed this pass (KNOWN_ISSUES D-26).
1027  pub forgotten: usize,
1028}
1029
1030/// Reaps timed-out in-flight tasks and routes each to **its own service's** dispatch queue (a
1031/// retry) or the done queue (a `Fatal`). Decoupled from the refetch path so the in-flight set
1032/// drains even under sustained backpressure (KNOWN_ISSUES D-6); routing by `task.service_id` rather
1033/// than the requesting service fixes the latent cross-service requeue bug. Returns a
1034/// [`ReapSummary`] of what it did (re-leases vs dead-letters) for the health log.
1035pub fn reap_expired_into(
1036  queues: &mut HashMap<i32, Vec<TaskProgress>>,
1037  in_flight: &InFlightSet,
1038  done_tx: &SyncSender<TaskReport>,
1039) -> ReapSummary {
1040  let mut summary = ReapSummary {
1041    forgotten: in_flight.forget_settled(),
1042    ..ReapSummary::default()
1043  };
1044  for expired in in_flight.take_expired() {
1045    match classify_expired(expired) {
1046      ExpiredOutcome::Requeue(task_progress) => {
1047        queues
1048          .entry(task_progress.task.service_id)
1049          .or_default()
1050          .push(task_progress);
1051        summary.requeued += 1;
1052      },
1053      ExpiredOutcome::Fatal(report) => {
1054        send_done(done_tx, report);
1055        summary.dead_lettered += 1;
1056      },
1057    }
1058  }
1059  summary
1060}
1061
1062/// Memoized getter for a `Service` record from the backend, populating the shared [`ServiceCache`]
1063/// on a miss. The `or_insert_with` holds only the relevant DashMap *shard* during the one-time DB
1064/// lookup (vs. the old whole-map `Mutex`), so concurrent lookups of *other* services are unblocked.
1065pub fn get_sync_service(
1066  service_name: &str,
1067  services: &ServiceCache,
1068  backend: &mut Backend,
1069) -> Option<Service> {
1070  services
1071    .entry(service_name.to_string())
1072    .or_insert_with(|| Service::find_by_name(service_name, &mut backend.connection).ok())
1073    .value()
1074    .clone()
1075}
1076
1077/// Getter for a `Service` from the shared [`ServiceCache`], with no DB access (a `None` entry means
1078/// a prior lookup found no such service).
1079pub fn get_service(service_name: &str, services: &ServiceCache) -> Option<Service> {
1080  services
1081    .get(service_name)
1082    .and_then(|entry| entry.value().clone())
1083}
1084
1085/// Memoised getter for a corpus's sandbox id, populating the shared [`SandboxCache`] from the
1086/// backend on a miss (the ventilator's DB-holding counterpart to [`get_sandbox_id`]). One lookup
1087/// per `corpus_id` ever dispatched — bounded by the corpus count, not the task count.
1088///
1089/// Fails closed: a failed lookup is **not** cached and yields `None` (unknown), so the sink derives
1090/// no result path and hands the task back to the in-flight set, where the reaper retries it
1091/// (re-attempting this lookup) or dead-letters it.
1092/// Memoising a failure as "ordinary" would route a sandbox's results over the parent's published
1093/// archives for the dispatcher's lifetime.
1094pub fn get_sync_sandbox_id(
1095  corpus_id: i32,
1096  sandboxes: &SandboxCache,
1097  backend: &mut Backend,
1098) -> Option<Option<i32>> {
1099  if let Some(known) = get_sandbox_id(corpus_id, sandboxes) {
1100    return Some(known);
1101  }
1102  match crate::models::Corpus::find_by_id(corpus_id, &mut backend.connection) {
1103    Ok(corpus) => {
1104      let sandbox_id = corpus.sandbox_id();
1105      sandboxes.insert(corpus_id, sandbox_id);
1106      Some(sandbox_id)
1107    },
1108    Err(error) => {
1109      warn!(corpus_id, %error, "ventilator: corpus sandbox lookup failed; left unknown");
1110      None
1111    },
1112  }
1113}
1114
1115/// Getter for a corpus's sandbox id from the shared [`SandboxCache`], with no DB access — the
1116/// sink's read on the result path. `Some(None)` is an ordinary corpus, `Some(Some(id))` a sandbox,
1117/// and `None` **unknown** (the lookup failed): the sink then writes nothing and hands the task
1118/// back to the reaper, never guessing the parent's archive name.
1119pub fn get_sandbox_id(corpus_id: i32, sandboxes: &SandboxCache) -> Option<Option<i32>> {
1120  sandboxes.get(&corpus_id).map(|entry| *entry.value())
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125  use super::*;
1126  use crate::config::DispatcherConfig;
1127  use crate::models::Task;
1128  use std::sync::Arc;
1129
1130  fn dummy_progress(id: i64) -> TaskProgress {
1131    TaskProgress {
1132      run_start: 0,
1133      last_cause: Vec::new(),
1134      task: Task {
1135        id,
1136        service_id: 1,
1137        corpus_id: 1,
1138        status: 0,
1139        entry: String::new(),
1140      },
1141      created_at: 0,
1142      retries: 0,
1143      lease_timeout_seconds: 3600,
1144      attempt_workers: Vec::new(),
1145    }
1146  }
1147
1148  #[test]
1149  fn reaper_honors_per_service_lease_timeouts() {
1150    // D-17: two tasks both dispatched 100 s ago — one on a fast service (60 s lease → expired) and
1151    // one on a slow service (3600 s lease → still valid). `take_expired` must reap only the
1152    // short-lease task, so one dispatcher serves fast and slow workers without false-reaping the
1153    // slow one mid-conversion.
1154    let now = chrono::Utc::now().timestamp();
1155    let set = InFlightSet::new();
1156    let mut short = dummy_progress(1);
1157    short.created_at = now - 100;
1158    short.lease_timeout_seconds = 60; // expected_at = now - 40 → expired
1159    set.insert(short);
1160    let mut long = dummy_progress(2);
1161    long.created_at = now - 100;
1162    long.lease_timeout_seconds = 3600; // expected_at = now + 3500 → still leased
1163    set.insert(long);
1164    let expired = set.take_expired();
1165    assert_eq!(expired.len(), 1, "only the short-lease task expired");
1166    assert_eq!(
1167      expired[0].task.id, 1,
1168      "the short-lease (fast-service) task is reaped; the slow service's long lease is honored"
1169    );
1170  }
1171
1172  #[test]
1173  fn reap_expired_into_tallies_requeue_and_dead_letter() {
1174    use std::sync::mpsc::sync_channel;
1175    // created_at = 0 (epoch) ⇒ every task's deadline is far in the past ⇒ all expired.
1176    let set = InFlightSet::new();
1177    set.insert(dummy_progress(1)); // retries 0 → within budget → re-queue
1178    set.insert(dummy_progress(2)); // retries 0 → within budget → re-queue
1179    set.insert(TaskProgress {
1180      retries: MAX_DISPATCH_RETRIES + 1, // budget exhausted → dead-letter Fatal
1181      ..dummy_progress(3)
1182    });
1183    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1184    let (done_tx, done_rx) = sync_channel(16);
1185    let summary = reap_expired_into(&mut queues, &set, &done_tx);
1186    assert_eq!(summary.requeued, 2, "two within retry budget are re-leased");
1187    assert_eq!(summary.dead_lettered, 1, "one over budget is dead-lettered");
1188    assert_eq!(set.len(), 0, "the in-flight set fully drained");
1189    assert_eq!(
1190      done_rx.try_iter().count(),
1191      1,
1192      "the dead-letter Fatal reached the done channel"
1193    );
1194  }
1195
1196  #[test]
1197  fn in_flight_saturation_is_inclusive_at_the_threshold() {
1198    assert!(!in_flight_saturated(0, 5));
1199    assert!(!in_flight_saturated(4, 5));
1200    assert!(
1201      in_flight_saturated(5, 5),
1202      "at the threshold we already apply backpressure"
1203    );
1204    assert!(in_flight_saturated(6, 5));
1205  }
1206
1207  #[test]
1208  fn backpressure_engages_below_the_hard_panic_bound() {
1209    // If the configured backpressure threshold ever reached the hard limit, backpressure would be
1210    // dead code and the in-flight set could still grow to the panic — the very crash D-6 exists to
1211    // prevent. Guard that invariant so a future config change can't silently reintroduce it.
1212    assert!(
1213      DispatcherConfig::default().max_in_flight < PROGRESS_QUEUE_HARD_LIMIT,
1214      "backpressure must engage before the hard panic bound"
1215    );
1216  }
1217
1218  #[test]
1219  fn inflight_len_tracks_dispatch_and_drain() {
1220    let set = InFlightSet::new();
1221    assert_eq!(set.len(), 0);
1222    set.insert(dummy_progress(1));
1223    set.insert(dummy_progress(2));
1224    assert_eq!(set.len(), 2, "two tasks now in flight");
1225    // The sink draining a returned result shrinks the in-flight set — how backpressure recovers.
1226    set.remove(1);
1227    assert_eq!(set.len(), 1);
1228  }
1229
1230  #[test]
1231  fn inflight_count_ignores_duplicate_insert_and_negative_remove() {
1232    // The O(1) size counter must stay exactly equal to the map size under the edge cases the
1233    // dispatcher actually hits: a re-leased task (same id inserted again) and the sink's mock-id
1234    // (negative) results. A drift here would mis-fire backpressure or leak toward the hard bound.
1235    let set = InFlightSet::new();
1236    set.insert(dummy_progress(7));
1237    set.insert(dummy_progress(7)); // same id → overwrite, not a second entry
1238    assert_eq!(
1239      set.len(),
1240      1,
1241      "re-inserting the same task id does not double-count"
1242    );
1243    assert!(
1244      set.remove(-1).is_none(),
1245      "negative (mock) ids are never tracked"
1246    );
1247    assert_eq!(set.len(), 1);
1248    assert!(set.remove(7).is_some());
1249    assert_eq!(set.len(), 0);
1250    assert!(set.remove(7).is_none(), "removing a gone task is a no-op");
1251    assert_eq!(
1252      set.len(),
1253      0,
1254      "a redundant remove does not underflow the counter"
1255    );
1256  }
1257
1258  #[test]
1259  fn inflight_handles_200_concurrent_tasks_with_consistent_count() {
1260    // Deployment sizing is ~200 concurrent workers; the in-flight set must absorb 200 simultaneous
1261    // leases from many threads (and then 200 concurrent drains) without losing entries or drifting
1262    // its O(1) size counter — the property the sharded DashMap + AtomicUsize must guarantee in
1263    // place of the old global Mutex. (Dispatcher rationalization phase 4.)
1264    let set = Arc::new(InFlightSet::new());
1265    let n: i64 = 200;
1266    let leasers: Vec<_> = (1..=n)
1267      .map(|id| {
1268        let set = set.clone();
1269        thread::spawn(move || set.insert(dummy_progress(id)))
1270      })
1271      .collect();
1272    for h in leasers {
1273      h.join().unwrap();
1274    }
1275    assert_eq!(
1276      set.len(),
1277      n as usize,
1278      "all 200 concurrent leases tracked, counter consistent"
1279    );
1280    assert_eq!(
1281      set.ids().len(),
1282      n as usize,
1283      "every id is present in the map"
1284    );
1285    let drainers: Vec<_> = (1..=n)
1286      .map(|id| {
1287        let set = set.clone();
1288        thread::spawn(move || set.remove(id))
1289      })
1290      .collect();
1291    let drained = drainers
1292      .into_iter()
1293      .filter_map(|h| h.join().unwrap())
1294      .count();
1295    assert_eq!(
1296      drained, n as usize,
1297      "each of the 200 tasks drained exactly once"
1298    );
1299    assert_eq!(
1300      set.len(),
1301      0,
1302      "counter back to zero after a full concurrent drain"
1303    );
1304    assert!(set.is_empty());
1305  }
1306
1307  fn expired_progress(id: i64, service_id: i32, retries: i64) -> TaskProgress {
1308    let mut tp = dummy_progress(id);
1309    tp.task.service_id = service_id;
1310    tp.created_at = 0; // expected_at = (retries+1)*3600, far in the past -> always expired
1311    tp.retries = retries;
1312    tp
1313  }
1314
1315  #[test]
1316  fn classify_expired_retries_then_gives_up() {
1317    // Budget remains -> requeue with the retry count incremented.
1318    match classify_expired(expired_progress(1, 3, MAX_DISPATCH_RETRIES)) {
1319      ExpiredOutcome::Requeue(tp) => assert_eq!(tp.retries, MAX_DISPATCH_RETRIES + 1),
1320      ExpiredOutcome::Fatal(_) => panic!("still within retry budget -> should requeue"),
1321    }
1322    // Budget exhausted -> Fatal.
1323    match classify_expired(expired_progress(2, 3, MAX_DISPATCH_RETRIES + 1)) {
1324      ExpiredOutcome::Fatal(report) => assert_eq!(report.task.id, 2),
1325      ExpiredOutcome::Requeue(_) => panic!("retry budget exhausted -> should be Fatal"),
1326    }
1327  }
1328
1329  #[test]
1330  fn rate_limited_log_emits_first_then_throttles_then_summarizes() {
1331    let mut log = RateLimitedLog::new(Duration::from_millis(40));
1332    // The first event always emits, so a problem is visible immediately.
1333    assert_eq!(log.record(), Some(1));
1334    // Subsequent events within the interval are suppressed (counted, not narrated).
1335    assert_eq!(log.record(), None);
1336    assert_eq!(log.record(), None);
1337    // After the interval, the next event emits a summary carrying the suppressed count
1338    // (the two suppressed above + this one).
1339    std::thread::sleep(Duration::from_millis(50));
1340    assert_eq!(log.record(), Some(3));
1341    // ...and the counter resets for the next window.
1342    assert_eq!(log.record(), None);
1343  }
1344
1345  #[test]
1346  fn reap_routes_to_own_service_and_drains() {
1347    let progress = InFlightSet::new();
1348    progress.insert(expired_progress(1, 7, 0)); // retriable, service 7
1349    progress.insert(expired_progress(2, 9, MAX_DISPATCH_RETRIES + 1)); // fatal, svc 9
1350    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1351    let (done_tx, done_rx) = std::sync::mpsc::sync_channel::<TaskReport>(10);
1352
1353    reap_expired_into(&mut queues, &progress, &done_tx);
1354
1355    // The retriable task is requeued to ITS OWN service (7), not some requester's queue, retry++.
1356    assert_eq!(queues.get(&7).map(Vec::len), Some(1));
1357    assert_eq!(queues[&7][0].retries, 1);
1358    assert!(
1359      !queues.contains_key(&9),
1360      "the exhausted task is not requeued"
1361    );
1362    // The exhausted task is reported Fatal on the done channel.
1363    let reports: Vec<TaskReport> = done_rx.try_iter().collect();
1364    assert_eq!(reports.len(), 1);
1365    assert_eq!(reports[0].task.id, 2);
1366    // The in-flight set is fully drained.
1367    assert_eq!(progress.len(), 0);
1368  }
1369
1370  #[test]
1371  fn a_failed_sandbox_lookup_is_not_cached_as_an_ordinary_corpus() {
1372    // A corpus id the DB can't resolve stands in for any lookup failure. Caching it as "ordinary"
1373    // would route a sandbox's results to the parent's published `<service>.zip` for the
1374    // dispatcher's lifetime.
1375    let sandboxes = SandboxCache::new();
1376    let mut backend = crate::backend::testdb();
1377    let _ = get_sync_sandbox_id(i32::MAX, &sandboxes, &mut backend);
1378    assert!(
1379      !sandboxes.contains_key(&i32::MAX),
1380      "an unresolved corpus must stay uncached (unknown), not be memoised as ordinary"
1381    );
1382  }
1383
1384  #[test]
1385  fn the_sink_distinguishes_an_unknown_corpus_from_an_ordinary_one() {
1386    // Unknown (the ventilator's lookup failed) must not read as "ordinary": that would write a
1387    // sandbox's result over the parent's published archive. `None` = unknown, so the sink derives
1388    // no path and hands the task back to the reaper.
1389    let sandboxes = SandboxCache::new();
1390    sandboxes.insert(1, None);
1391    sandboxes.insert(2, Some(2));
1392    assert_eq!(get_sandbox_id(1, &sandboxes), Some(None), "cached ordinary");
1393    assert_eq!(
1394      get_sandbox_id(2, &sandboxes),
1395      Some(Some(2)),
1396      "cached sandbox"
1397    );
1398    assert_eq!(get_sandbox_id(3, &sandboxes), None, "unknown");
1399  }
1400
1401  /// A task leased to the workers `workers` in turn (its current attempt is the last), dispatched
1402  /// long ago so the reaper takes it.
1403  fn leased_to(id: i64, workers: &[&str]) -> TaskProgress {
1404    TaskProgress {
1405      retries: workers.len() as i64 - 1,
1406      attempt_workers: workers.iter().map(|w| w.to_string()).collect(),
1407      ..dummy_progress(id)
1408    }
1409  }
1410
1411  fn accepted(claim: ResultClaim) -> (usize, Option<LateResult>) {
1412    match claim {
1413      ResultClaim::Accept { attempt, late, .. } => (attempt, late),
1414      other => panic!("expected the result to be accepted, got {other:?}"),
1415    }
1416  }
1417
1418  #[test]
1419  fn the_lease_clock_starts_at_dispatch_not_at_fetch() {
1420    // D-27: a task fetched into the dispatch queue long ago (created_at = epoch) must get a full
1421    // lease from the moment it is leased, not be reaped as already expired while it converts.
1422    let set = InFlightSet::new();
1423    let mut queue = vec![dummy_progress(1)];
1424    let leased = crate::dispatcher::ventilator::lease_next(&mut queue, &set, "w0")
1425      .expect("the queued task is leased");
1426    let now = chrono::Utc::now().timestamp();
1427    assert!(leased.created_at >= now - 1, "stamped at dispatch");
1428    assert_eq!(leased.attempt_workers, vec!["w0".to_string()]);
1429    assert_eq!(set.len(), 1);
1430    assert!(
1431      set.take_expired().is_empty(),
1432      "a just-leased task is not expired"
1433    );
1434  }
1435
1436  #[test]
1437  fn a_result_after_a_requeue_is_accepted_and_the_retry_is_not_leased() {
1438    // The lease expired and the reaper re-queued the task; then attempt 0's result arrives.
1439    let set = InFlightSet::new();
1440    set.insert(leased_to(1, &["w0"]));
1441    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1442    let (done_tx, done_rx) = std::sync::mpsc::sync_channel(4);
1443    assert_eq!(reap_expired_into(&mut queues, &set, &done_tx).requeued, 1);
1444    assert_eq!((set.len(), set.settling_len()), (0, 1));
1445    assert_eq!(
1446      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1447      (0, Some(LateResult::AfterRequeue))
1448    );
1449    // The queued retry is dropped rather than converted again.
1450    let queue = queues.get_mut(&1).expect("re-queued to its service");
1451    assert!(crate::dispatcher::ventilator::lease_next(queue, &set, "w1").is_none());
1452    assert!(queue.is_empty());
1453    assert_eq!(set.len(), 0);
1454    assert_eq!(done_rx.try_iter().count(), 0, "no dead-letter");
1455  }
1456
1457  #[test]
1458  fn a_result_after_a_dead_letter_replaces_never_completed_and_stands() {
1459    // Three attempts, all expired: the reaper reports never_completed. Then attempt 2's result
1460    // arrives (accepted: it replaces the Fatal), and after it attempt 0's (superseded).
1461    let set = InFlightSet::new();
1462    set.insert(leased_to(1, &["w0", "w1", "w2"]));
1463    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1464    let (done_tx, done_rx) = std::sync::mpsc::sync_channel(4);
1465    assert_eq!(
1466      reap_expired_into(&mut queues, &set, &done_tx).dead_lettered,
1467      1
1468    );
1469    let dead_letter: Vec<TaskReport> = done_rx.try_iter().collect();
1470    assert_eq!(dead_letter.len(), 1);
1471    assert_eq!(
1472      accepted(set.claim_result(1, "w2", TaskStatus::NoProblem)),
1473      (2, Some(LateResult::AfterDeadLetter))
1474    );
1475    match set.claim_result(1, "w0", TaskStatus::NoProblem) {
1476      ResultClaim::Superseded { attempt, finished } => assert_eq!((attempt, finished), (0, 2)),
1477      other => panic!("an older attempt must not overwrite a newer result, got {other:?}"),
1478    }
1479    // A repeat of the accepted attempt is not accepted twice.
1480    assert!(matches!(
1481      set.claim_result(1, "w2", TaskStatus::NoProblem),
1482      ResultClaim::Superseded { .. }
1483    ));
1484  }
1485
1486  #[test]
1487  fn an_older_attempt_finishing_first_is_replaced_by_the_newer_one() {
1488    // Attempt 0 expired, the task was re-leased to w1 (in flight), then w0's late result arrives:
1489    // accepted, and the task is held so w1's result can still land — and replace it.
1490    let set = InFlightSet::new();
1491    set.insert(leased_to(1, &["w0"]));
1492    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1493    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
1494    reap_expired_into(&mut queues, &set, &done_tx);
1495    let queue = queues.get_mut(&1).unwrap();
1496    crate::dispatcher::ventilator::lease_next(queue, &set, "w1").expect("re-leased");
1497    assert_eq!((set.len(), set.settling_len()), (1, 0));
1498    assert_eq!(
1499      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1500      (0, Some(LateResult::OlderAttempt))
1501    );
1502    assert_eq!((set.len(), set.settling_len()), (0, 1));
1503    assert_eq!(
1504      accepted(set.claim_result(1, "w1", TaskStatus::NoProblem)),
1505      (1, Some(LateResult::NewerAttempt))
1506    );
1507    assert!(matches!(
1508      set.claim_result(1, "w0", TaskStatus::NoProblem),
1509      ResultClaim::Superseded { .. }
1510    ));
1511  }
1512
1513  #[test]
1514  fn a_fresh_fetch_of_a_finished_task_is_leased() {
1515    // An operator re-runs a task whose late result was accepted, within the window: the fresh
1516    // fetch (a first attempt) is leased; only the old queued retry is refused.
1517    let set = InFlightSet::new();
1518    set.insert(leased_to(1, &["w0"]));
1519    let _ = set.take_expired();
1520    accepted(set.claim_result(1, "w0", TaskStatus::NoProblem));
1521    let mut retry = leased_to(1, &["w0", "w1"]);
1522    retry.created_at = chrono::Utc::now().timestamp();
1523    assert!(!set.lease(retry), "the queued retry is refused");
1524    let mut fresh = leased_to(1, &["w2"]);
1525    fresh.created_at = chrono::Utc::now().timestamp();
1526    assert!(set.lease(fresh), "a rerun's fresh lease is not");
1527    assert_eq!((set.len(), set.settling_len()), (1, 0));
1528    // The old run's attempt stays on record, so its late result is told apart and dropped (D-28).
1529    assert!(matches!(
1530      set.claim_result(1, "w0", TaskStatus::NoProblem),
1531      ResultClaim::Stale { attempt: 0 }
1532    ));
1533    assert_eq!(
1534      accepted(set.claim_result(1, "w2", TaskStatus::NoProblem)).0,
1535      1
1536    );
1537  }
1538
1539  #[test]
1540  fn a_newer_attempt_finishing_first_is_never_overwritten() {
1541    // The in-flight attempt 1 (w1) reports first; attempt 0's late result is then dropped.
1542    let set = InFlightSet::new();
1543    set.insert(leased_to(1, &["w0", "w1"]));
1544    assert_eq!(
1545      accepted(set.claim_result(1, "w1", TaskStatus::NoProblem)),
1546      (1, None)
1547    );
1548    assert_eq!((set.len(), set.settling_len()), (0, 0));
1549    assert!(matches!(
1550      set.claim_result(1, "w0", TaskStatus::NoProblem),
1551      ResultClaim::Unknown
1552    ));
1553  }
1554
1555  #[test]
1556  fn two_attempts_on_one_worker_attribute_to_the_newer() {
1557    // A harness slot keeps its identity across respawns, so one worker can hold two attempts of a
1558    // task; its result belongs to the newer one.
1559    let set = InFlightSet::new();
1560    set.insert(leased_to(1, &["w0", "w1", "w0"]));
1561    assert_eq!(
1562      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1563      (2, None)
1564    );
1565  }
1566
1567  #[test]
1568  fn without_a_late_result_window_reaped_tasks_are_forgotten() {
1569    let set = InFlightSet::with_late_result_window(0);
1570    set.insert(leased_to(1, &["w0"]));
1571    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1572    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
1573    reap_expired_into(&mut queues, &set, &done_tx);
1574    assert_eq!(set.settling_len(), 0);
1575    assert!(matches!(
1576      set.claim_result(1, "w0", TaskStatus::NoProblem),
1577      ResultClaim::Unknown
1578    ));
1579  }
1580
1581  #[test]
1582  fn a_passed_late_result_window_is_forgotten_on_the_reap() {
1583    let set = InFlightSet::new();
1584    set.map.insert(
1585      1,
1586      TaskSlot::Settling(Settling {
1587        task: dummy_progress(1).task,
1588        attempt_workers: vec!["w0".into()],
1589        finished: None,
1590        dead_lettered: true,
1591        last_cause: Vec::new(),
1592        forget_at: 0,
1593        run_start: 0,
1594        retries: 2,
1595        lease_timeout_seconds: 240,
1596        queued: false,
1597      }),
1598    );
1599    set.settling.fetch_add(1, Ordering::AcqRel);
1600    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1601    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
1602    assert_eq!(reap_expired_into(&mut queues, &set, &done_tx).forgotten, 1);
1603    assert_eq!(set.settling_len(), 0);
1604    assert!(matches!(
1605      set.claim_result(1, "w0", TaskStatus::NoProblem),
1606      ResultClaim::Unknown
1607    ));
1608  }
1609
1610  #[test]
1611  fn an_unusable_reply_leaves_the_task_in_flight_but_answered() {
1612    // D-18: an empty / unreadable result is no verdict — the reaper still retries the task — but
1613    // the task awaits no reply, so a bounded run's sink can finish.
1614    let set = InFlightSet::new();
1615    set.insert(TaskProgress {
1616      created_at: chrono::Utc::now().timestamp(),
1617      ..leased_to(1, &["w0"])
1618    });
1619    assert_eq!((set.len(), set.awaiting_replies()), (1, 1));
1620    set.note_unusable_reply(1, "w0", true);
1621    set.note_unusable_reply(1, "w0", true);
1622    assert_eq!((set.len(), set.awaiting_replies()), (1, 0));
1623    assert_eq!(set.ids(), vec![1]);
1624    // A usable reply after all is still accepted.
1625    assert_eq!(
1626      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1627      (0, None)
1628    );
1629    assert_eq!((set.len(), set.awaiting_replies()), (0, 0));
1630    // And the reaper still takes an answered task.
1631    set.insert(leased_to(2, &["w0"]));
1632    set.note_unusable_reply(2, "w0", true);
1633    assert_eq!(set.take_expired().len(), 1);
1634    assert_eq!(
1635      (set.len(), set.awaiting_replies(), set.settling_len()),
1636      (0, 0, 1)
1637    );
1638  }
1639
1640  #[test]
1641  fn held_tasks_are_not_in_flight() {
1642    // Backpressure, the sink's drain check and the limbo reset see only leased tasks.
1643    let set = InFlightSet::new();
1644    set.insert(leased_to(1, &["w0"]));
1645    set.insert(TaskProgress {
1646      created_at: chrono::Utc::now().timestamp(),
1647      ..leased_to(2, &["w0"])
1648    });
1649    let _ = set.take_expired();
1650    assert_eq!(set.len(), 1);
1651    assert_eq!(set.ids(), vec![2]);
1652    assert!(
1653      set.remove(1).is_none(),
1654      "a held task is not removed as in flight"
1655    );
1656    assert!(set.task(1).is_some(), "but a result can still find it");
1657  }
1658
1659  /// `(category, what)` of each message, for comparing causes.
1660  fn whats(messages: &[NewTaskMessage]) -> Vec<(String, String)> {
1661    use crate::models::LogRecord;
1662    messages
1663      .iter()
1664      .map(|m| (m.category().to_string(), m.what().to_string()))
1665      .collect()
1666  }
1667
1668  /// The messages of a harness's retryable timeout report for task `id`.
1669  fn timeout_cause(id: i64) -> Vec<NewTaskMessage> {
1670    vec![NewTaskMessage::new(
1671      id,
1672      "fatal",
1673      "timeout".to_string(),
1674      "wallclock".to_string(),
1675      "the watchdog ended the worker".to_string(),
1676    )]
1677  }
1678
1679  #[test]
1680  fn a_retryable_cause_retries_and_a_success_then_stands() {
1681    // Retry, then cause (user ruling 2026-10-12): the harness reports attempt 0 timed out; the
1682    // task goes back to its budget, is re-leased, and attempt 1's result is the one recorded.
1683    let set = InFlightSet::new();
1684    set.insert(TaskProgress {
1685      created_at: chrono::Utc::now().timestamp(),
1686      ..leased_to(1, &["w0"])
1687    });
1688    set.note_received(1);
1689    assert!(
1690      set.take_expired().is_empty(),
1691      "not due, and a result is with its writer"
1692    );
1693    assert_eq!(
1694      set.defer_retryable(1, "w0", timeout_cause(1)),
1695      RetryDecision::Deferred
1696    );
1697    assert_eq!(
1698      set.awaiting_replies(),
1699      0,
1700      "answered: only the reaper moves it on"
1701    );
1702    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1703    let (done_tx, done_rx) = std::sync::mpsc::sync_channel(4);
1704    assert_eq!(
1705      reap_expired_into(&mut queues, &set, &done_tx).requeued,
1706      1,
1707      "due at the next reap"
1708    );
1709    let queue = queues.get_mut(&1).unwrap();
1710    let retry = crate::dispatcher::ventilator::lease_next(queue, &set, "w1").expect("re-leased");
1711    assert_eq!(
1712      whats(&retry.last_cause),
1713      whats(&timeout_cause(1)),
1714      "the cause rides along"
1715    );
1716    assert_eq!(
1717      accepted(set.claim_result(1, "w1", TaskStatus::Warning)),
1718      (1, None)
1719    );
1720    assert_eq!(done_rx.try_iter().count(), 0);
1721  }
1722
1723  #[test]
1724  fn a_retryable_cause_on_the_last_attempt_is_its_stand_in() {
1725    // The budget is spent: the cause is the task's final record, a stand-in (published with its
1726    // archive by the writer) that a real late result still replaces.
1727    let set = InFlightSet::new();
1728    set.insert(TaskProgress {
1729      created_at: chrono::Utc::now().timestamp(),
1730      ..leased_to(1, &["w0", "w1", "w2"])
1731    });
1732    assert!(matches!(
1733      set.defer_retryable(1, "w2", timeout_cause(1)),
1734      RetryDecision::Final { .. }
1735    ));
1736    assert_eq!(
1737      (set.len(), set.awaiting_replies(), set.settling_len()),
1738      (0, 0, 1)
1739    );
1740    assert_eq!(
1741      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1742      (0, Some(LateResult::ReplacesFailure))
1743    );
1744  }
1745
1746  #[test]
1747  fn a_last_attempt_stand_in_is_restored_after_a_failed_publish() {
1748    // The stand-in is an accepted failure: a later good result replaces it, and if that one cannot
1749    // be published, the stand-in (its archive on disk) is what stands again.
1750    let set = InFlightSet::new();
1751    set.insert(TaskProgress {
1752      created_at: chrono::Utc::now().timestamp(),
1753      ..leased_to(1, &["w0", "w1", "w2"])
1754    });
1755    assert!(matches!(
1756      set.defer_retryable(1, "w2", timeout_cause(1)),
1757      RetryDecision::Final { .. }
1758    ));
1759    let replaced = match set.claim_result(1, "w0", TaskStatus::NoProblem) {
1760      ResultClaim::Accept { late, replaced, .. } => {
1761        assert_eq!(late, Some(LateResult::ReplacesFailure));
1762        replaced
1763      },
1764      other => panic!("{other:?}"),
1765    };
1766    assert_eq!(replaced, Some((2, TaskStatus::Fatal)));
1767    set.note_publish_failed(1, replaced);
1768    // The stand-in stands: another failure cannot replace it, a usable result still can.
1769    assert!(matches!(
1770      set.claim_result(1, "w1", TaskStatus::Fatal),
1771      ResultClaim::Superseded { finished: 2, .. }
1772    ));
1773    assert_eq!(
1774      accepted(set.claim_result(1, "w1", TaskStatus::Warning)),
1775      (1, Some(LateResult::ReplacesFailure))
1776    );
1777  }
1778
1779  #[test]
1780  fn without_a_window_a_last_attempt_stand_in_leaves_the_set() {
1781    let set = InFlightSet::with_late_result_window(0);
1782    set.insert(TaskProgress {
1783      created_at: chrono::Utc::now().timestamp(),
1784      ..leased_to(1, &["w0", "w1", "w2"])
1785    });
1786    assert!(matches!(
1787      set.defer_retryable(1, "w2", timeout_cause(1)),
1788      RetryDecision::Final { .. }
1789    ));
1790    assert_eq!((set.len(), set.settling_len()), (0, 0));
1791  }
1792
1793  #[test]
1794  fn an_earlier_runs_result_never_lands_in_a_rerun_of_a_held_task() {
1795    // D-28: the reaper released the task (held); an operator re-ran it and the ventilator fetched
1796    // it afresh (retries 0) while still held. The old run's late result is stale — even though
1797    // the claim would carry the new lease's mark — and the new run's own Fatal stands.
1798    let set = InFlightSet::new();
1799    set.insert(leased_to(1, &["w0"]));
1800    assert_eq!(set.take_expired().len(), 1);
1801    let mut fresh = vec![dummy_progress(1)];
1802    let leased =
1803      crate::dispatcher::ventilator::lease_next(&mut fresh, &set, "w1").expect("leased afresh");
1804    assert_eq!(leased.retries, 0);
1805    set.note_received(1);
1806    assert!(matches!(
1807      set.claim_result(1, "w0", TaskStatus::NoProblem),
1808      ResultClaim::Stale { attempt: 0 }
1809    ));
1810    assert_eq!(
1811      accepted(set.claim_result(1, "w1", TaskStatus::Fatal)),
1812      (1, None)
1813    );
1814  }
1815
1816  #[test]
1817  fn an_earlier_runs_result_never_lands_in_a_rerun_of_an_in_flight_task() {
1818    // The same, re-fetched while the old attempt is still in flight.
1819    let set = InFlightSet::new();
1820    set.insert(TaskProgress {
1821      created_at: chrono::Utc::now().timestamp(),
1822      ..leased_to(1, &["w0"])
1823    });
1824    let mut fresh = vec![dummy_progress(1)];
1825    crate::dispatcher::ventilator::lease_next(&mut fresh, &set, "w1").expect("leased afresh");
1826    // Each old-run report is received (the sink holds the reaper off) before it is decided.
1827    set.note_received(1);
1828    assert!(matches!(
1829      set.defer_retryable(1, "w0", timeout_cause(1)),
1830      RetryDecision::Dropped(_)
1831    ));
1832    set.note_received(1);
1833    assert!(matches!(
1834      set.claim_result(1, "w0", TaskStatus::NoProblem),
1835      ResultClaim::Stale { attempt: 0 }
1836    ));
1837    // Neither left a receipt behind: once the new lease is due, the reaper takes it.
1838    if let Some(mut slot) = set.map.get_mut(&1)
1839      && let TaskSlot::InFlight(leased) = slot.value_mut()
1840    {
1841      assert_eq!(leased.pending, 0, "no receipt leaked");
1842      leased.progress.created_at = 0;
1843    }
1844    assert_eq!(
1845      set.take_expired().len(),
1846      1,
1847      "the new run's lease is reaped when due"
1848    );
1849    assert_eq!(
1850      accepted(set.claim_result(1, "w1", TaskStatus::Warning)),
1851      (1, Some(LateResult::AfterRequeue))
1852    );
1853  }
1854
1855  #[test]
1856  fn a_retry_of_an_earlier_run_never_takes_a_new_runs_slot() {
1857    let set = InFlightSet::new();
1858    set.insert(TaskProgress {
1859      created_at: chrono::Utc::now().timestamp(),
1860      ..leased_to(1, &["w0"])
1861    });
1862    let mut fresh = vec![dummy_progress(1)];
1863    crate::dispatcher::ventilator::lease_next(&mut fresh, &set, "w1").expect("leased afresh");
1864    // A retry of the old run (run_start 0, retries 1) shows up: refused.
1865    let mut old_retry = leased_to(1, &["w0", "w2"]);
1866    old_retry.created_at = chrono::Utc::now().timestamp();
1867    assert!(!set.lease(old_retry));
1868    assert_eq!(
1869      accepted(set.claim_result(1, "w1", TaskStatus::NoProblem)),
1870      (1, None)
1871    );
1872  }
1873
1874  #[test]
1875  fn a_rerun_is_not_handed_to_a_worker_of_the_earlier_run() {
1876    let set = InFlightSet::new();
1877    set.insert(leased_to(1, &["w0"]));
1878    let _ = set.take_expired();
1879    let mut queue = vec![dummy_progress(1)];
1880    assert!(
1881      crate::dispatcher::ventilator::lease_next(&mut queue, &set, "w0").is_none(),
1882      "w0 worked the earlier run"
1883    );
1884    assert_eq!(queue.len(), 1, "the task stays queued for another worker");
1885    let leased = crate::dispatcher::ventilator::lease_next(&mut queue, &set, "w1").expect("leased");
1886    assert_eq!(
1887      leased.attempt_workers.last().map(String::as_str),
1888      Some("w1")
1889    );
1890  }
1891
1892  #[test]
1893  fn a_restarted_ventilator_keeps_every_held_task() {
1894    // In flight, re-queued, dead-lettered, and finished late: none is reset to TODO.
1895    let set = InFlightSet::new();
1896    set.insert(TaskProgress {
1897      created_at: chrono::Utc::now().timestamp(),
1898      ..leased_to(1, &["w0"])
1899    });
1900    set.insert(leased_to(2, &["w0"]));
1901    set.insert(leased_to(3, &["w0", "w1", "w2"]));
1902    set.insert(leased_to(4, &["w0"]));
1903    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1904    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
1905    reap_expired_into(&mut queues, &set, &done_tx);
1906    accepted(set.claim_result(4, "w0", TaskStatus::NoProblem));
1907    let mut kept = set.kept_on_restart();
1908    kept.sort_unstable();
1909    assert_eq!(kept, vec![1, 2, 3, 4]);
1910    assert_eq!(set.queued_retries().len(), 1, "only task 2 is queued again");
1911  }
1912
1913  #[test]
1914  fn a_restarted_ventilator_queues_the_held_retries_again() {
1915    let set = InFlightSet::new();
1916    set.insert(leased_to(1, &["w0"]));
1917    set.insert(leased_to(2, &["w0", "w1", "w2"])); // exhausted: dead-lettered, not queued
1918    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1919    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
1920    reap_expired_into(&mut queues, &set, &done_tx);
1921    let again = set.queued_retries();
1922    assert_eq!(again.len(), 1);
1923    assert_eq!((again[0].task.id, again[0].retries), (1, 1));
1924    assert_eq!(again[0].attempt_workers, vec!["w0".to_string()]);
1925    // A late result that finished it takes it off the list.
1926    accepted(set.claim_result(1, "w0", TaskStatus::NoProblem));
1927    assert!(set.queued_retries().is_empty());
1928  }
1929
1930  #[test]
1931  fn a_single_attempt_failure_is_not_held() {
1932    let set = InFlightSet::new();
1933    set.insert(TaskProgress {
1934      created_at: chrono::Utc::now().timestamp(),
1935      ..leased_to(1, &["w0"])
1936    });
1937    assert_eq!(
1938      accepted(set.claim_result(1, "w0", TaskStatus::Fatal)),
1939      (0, None)
1940    );
1941    assert_eq!(set.settling_len(), 0, "nothing could replace it");
1942  }
1943
1944  #[test]
1945  fn an_unanswered_last_attempt_dead_letters_with_the_last_cause() {
1946    // Attempt 1 reports a timeout (budget remains: deferred, re-queued); attempt 2 never answers:
1947    // the dead-letter carries attempt 1's cause, not never_completed_with_retries.
1948    let set = InFlightSet::new();
1949    set.insert(TaskProgress {
1950      created_at: chrono::Utc::now().timestamp(),
1951      ..leased_to(1, &["w0", "w1"])
1952    });
1953    assert_eq!(
1954      set.defer_retryable(1, "w1", timeout_cause(1)),
1955      RetryDecision::Deferred
1956    );
1957    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
1958    let (done_tx, done_rx) = std::sync::mpsc::sync_channel(4);
1959    assert_eq!(reap_expired_into(&mut queues, &set, &done_tx).requeued, 1);
1960    let queue = queues.get_mut(&1).unwrap();
1961    crate::dispatcher::ventilator::lease_next(queue, &set, "w2").expect("re-leased");
1962    let mut last = set.remove(1).expect("in flight");
1963    last.created_at = 0;
1964    set.insert(last);
1965    assert_eq!(
1966      reap_expired_into(&mut queues, &set, &done_tx).dead_lettered,
1967      1
1968    );
1969    let reports: Vec<TaskReport> = done_rx.try_iter().collect();
1970    assert_eq!(reports.len(), 1);
1971    let report = &reports[0];
1972    assert!(report.stand_in && report.status == TaskStatus::Fatal);
1973    assert_eq!(
1974      whats(&report.messages),
1975      whats(&timeout_cause(1)),
1976      "the last cause, not never_completed_with_retries"
1977    );
1978    assert_eq!(
1979      accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)),
1980      (0, Some(LateResult::AfterDeadLetter))
1981    );
1982  }
1983
1984  #[test]
1985  fn a_good_result_replaces_a_newer_attempts_crash() {
1986    // Good result wins (user ruling 2026-10-12): attempt 1 (w1, in flight) crashes first — a Fatal
1987    // recorded at once, but held — then attempt 0's late good result replaces it.
1988    let set = InFlightSet::new();
1989    set.insert(TaskProgress {
1990      created_at: chrono::Utc::now().timestamp(),
1991      ..leased_to(1, &["w0", "w1"])
1992    });
1993    assert_eq!(
1994      accepted(set.claim_result(1, "w1", TaskStatus::Fatal)),
1995      (1, None)
1996    );
1997    assert_eq!(
1998      set.settling_len(),
1999      1,
2000      "a failure is held for a better result"
2001    );
2002    match set.claim_result(1, "w0", TaskStatus::Warning) {
2003      ResultClaim::Accept {
2004        attempt,
2005        late,
2006        replaced,
2007        ..
2008      } => {
2009        assert_eq!(attempt, 0);
2010        assert_eq!(late, Some(LateResult::ReplacesFailure));
2011        assert_eq!(replaced, Some((1, TaskStatus::Fatal)));
2012      },
2013      other => panic!("the good result must replace the crash, got {other:?}"),
2014    }
2015    // ...and stands against a later failure.
2016    assert!(matches!(
2017      set.claim_result(1, "w1", TaskStatus::Fatal),
2018      ResultClaim::Superseded { .. }
2019    ));
2020  }
2021
2022  #[test]
2023  fn a_publish_failure_restores_the_result_it_replaced() {
2024    let set = InFlightSet::new();
2025    set.insert(TaskProgress {
2026      created_at: chrono::Utc::now().timestamp(),
2027      ..leased_to(1, &["w0", "w1"])
2028    });
2029    accepted(set.claim_result(1, "w0", TaskStatus::NoProblem)); // older, held
2030    let replaced = match set.claim_result(1, "w1", TaskStatus::Warning) {
2031      ResultClaim::Accept { replaced, .. } => replaced,
2032      other => panic!("{other:?}"),
2033    };
2034    assert_eq!(replaced, Some((0, TaskStatus::NoProblem)));
2035    set.note_publish_failed(1, replaced);
2036    // Attempt 0's good result stands again: a failure cannot replace it.
2037    assert!(matches!(
2038      set.claim_result(1, "w1", TaskStatus::Fatal),
2039      ResultClaim::Superseded { finished: 0, .. }
2040    ));
2041  }
2042
2043  #[test]
2044  fn of_two_retryable_causes_the_newest_attempts_stands() {
2045    let shed = |id: i64| {
2046      vec![NewTaskMessage::new(
2047        id,
2048        "fatal",
2049        "oom".to_string(),
2050        "shed".to_string(),
2051        String::new(),
2052      )]
2053    };
2054    let set = InFlightSet::new();
2055    set.insert(leased_to(1, &["w0", "w1"]));
2056    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
2057    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
2058    assert_eq!(reap_expired_into(&mut queues, &set, &done_tx).requeued, 1);
2059    assert!(matches!(
2060      set.defer_retryable(1, "w0", timeout_cause(1)),
2061      RetryDecision::Dropped(_)
2062    ));
2063    assert_eq!(
2064      set.defer_retryable(1, "w1", shed(1)),
2065      RetryDecision::Deferred
2066    );
2067    assert!(matches!(
2068      set.defer_retryable(1, "w0", timeout_cause(1)),
2069      RetryDecision::Dropped(_)
2070    ));
2071    let queue = queues.get_mut(&1).unwrap();
2072    crate::dispatcher::ventilator::lease_next(queue, &set, "w2").expect("re-leased");
2073    let retry = set.remove(1).expect("in flight");
2074    assert_eq!(whats(&retry.last_cause), whats(&shed(1)));
2075  }
2076
2077  #[test]
2078  fn a_late_good_result_is_not_replaced_by_a_newer_attempts_failure() {
2079    // Attempt 0 (w0) is late but good while attempt 1 (w1) runs; then attempt 1 reports a
2080    // timeout (retryable) or crashes (a Fatal result): the good result stands either way.
2081    let set = InFlightSet::new();
2082    set.insert(TaskProgress {
2083      created_at: chrono::Utc::now().timestamp(),
2084      ..leased_to(1, &["w0", "w1"])
2085    });
2086    assert_eq!(
2087      accepted(set.claim_result(1, "w0", TaskStatus::Warning)),
2088      (0, Some(LateResult::OlderAttempt))
2089    );
2090    assert!(matches!(
2091      set.defer_retryable(1, "w1", timeout_cause(1)),
2092      RetryDecision::Dropped(_)
2093    ));
2094    assert!(matches!(
2095      set.claim_result(1, "w1", TaskStatus::Fatal),
2096      ResultClaim::Superseded {
2097        attempt: 1,
2098        finished: 0,
2099      }
2100    ));
2101    // A newer attempt's usable result still replaces it.
2102    assert_eq!(
2103      accepted(set.claim_result(1, "w1", TaskStatus::NoProblem)),
2104      (1, Some(LateResult::NewerAttempt))
2105    );
2106  }
2107
2108  #[test]
2109  fn an_older_attempts_retryable_cause_is_dropped_while_a_newer_runs() {
2110    let set = InFlightSet::new();
2111    set.insert(TaskProgress {
2112      created_at: chrono::Utc::now().timestamp(),
2113      ..leased_to(1, &["w0", "w1"])
2114    });
2115    assert!(matches!(
2116      set.defer_retryable(1, "w0", timeout_cause(1)),
2117      RetryDecision::Dropped(_)
2118    ));
2119    assert!(
2120      set.take_expired().is_empty(),
2121      "the newer attempt keeps its lease"
2122    );
2123    // An older attempt's unusable reply does not mark the task answered either.
2124    set.note_unusable_reply(1, "w0", true);
2125    assert_eq!(set.awaiting_replies(), 1);
2126  }
2127
2128  #[test]
2129  fn a_retryable_cause_after_a_requeue_rides_to_the_retry() {
2130    let set = InFlightSet::new();
2131    set.insert(leased_to(1, &["w0"]));
2132    let mut queues: HashMap<i32, Vec<TaskProgress>> = HashMap::new();
2133    let (done_tx, _done_rx) = std::sync::mpsc::sync_channel(4);
2134    reap_expired_into(&mut queues, &set, &done_tx);
2135    assert_eq!(
2136      set.defer_retryable(1, "w0", timeout_cause(1)),
2137      RetryDecision::Deferred
2138    );
2139    let queue = queues.get_mut(&1).unwrap();
2140    crate::dispatcher::ventilator::lease_next(queue, &set, "w1").expect("re-leased");
2141    let retry = set.remove(1).expect("in flight");
2142    assert_eq!(whats(&retry.last_cause), whats(&timeout_cause(1)));
2143  }
2144
2145  #[test]
2146  fn a_received_result_holds_off_the_reaper() {
2147    let set = InFlightSet::new();
2148    set.insert(leased_to(1, &["w0"]));
2149    set.note_received(1);
2150    assert!(
2151      set.take_expired().is_empty(),
2152      "its writer is deciding the result"
2153    );
2154    set.note_unusable_reply(1, "w0", true);
2155    assert_eq!(
2156      set.take_expired().len(),
2157      1,
2158      "an unusable result leaves it to the reaper"
2159    );
2160  }
2161}