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
14const KEEPALIVE_PROBE_INTERVAL_SECS: i32 = 30;
20const KEEPALIVE_PROBE_COUNT: i32 = 4;
22
23pub 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
40pub static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
48
49extern "C" fn handle_shutdown_signal(_sig: libc::c_int) {
52 SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
53}
54
55pub fn install_shutdown_handlers() {
58 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#[must_use]
73pub fn shutdown_requested() -> bool { SHUTDOWN_REQUESTED.load(Ordering::SeqCst) }
74
75pub type ServiceCache = DashMap<String, Option<Service>>;
81
82pub type SandboxCache = DashMap<i32, Option<i32>>;
91
92pub struct RateLimitedLog {
103 events_since_emit: u64,
104 last_emit: Option<Instant>,
105 interval: Duration,
106}
107
108impl RateLimitedLog {
109 pub fn new(interval: Duration) -> Self {
111 RateLimitedLog {
112 events_since_emit: 0,
113 last_emit: None,
114 interval,
115 }
116 }
117
118 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
138pub const PROGRESS_QUEUE_HARD_LIMIT: usize = 10_000;
143pub const DONE_QUEUE_CAPACITY: usize = 10_000;
150
151pub fn in_flight_saturated(in_flight: usize, max_in_flight: usize) -> bool {
155 in_flight >= max_in_flight
156}
157
158#[derive(Clone, Debug)]
160enum TaskSlot {
161 InFlight(Leased),
163 Settling(Settling),
167}
168
169#[derive(Clone, Debug)]
171struct Leased {
172 progress: TaskProgress,
174 pending: u32,
177 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 fn current_attempt(&self) -> usize { self.progress.attempt_workers.len().saturating_sub(1) }
194}
195
196#[derive(Clone, Debug)]
198struct Settling {
199 task: Task,
201 attempt_workers: Vec<String>,
203 finished: Option<(usize, TaskStatus)>,
205 dead_lettered: bool,
207 last_cause: Vec<NewTaskMessage>,
209 forget_at: i64,
211 run_start: usize,
214 retries: i64,
218 lease_timeout_seconds: i64,
219 queued: bool,
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224pub enum LateResult {
225 OlderAttempt,
228 AfterRequeue,
230 AfterDeadLetter,
232 NewerAttempt,
234 ReplacesFailure,
237}
238
239#[derive(Clone, Debug)]
241pub enum ResultClaim {
242 Accept {
244 task: Task,
246 attempt: usize,
248 attempts: usize,
250 late: Option<LateResult>,
252 replaced: Option<(usize, TaskStatus)>,
255 },
256 Superseded {
259 attempt: usize,
261 finished: usize,
263 },
264 Stale {
266 attempt: usize,
268 },
269 Unknown,
271}
272
273#[derive(Clone, Debug, PartialEq, Eq)]
275pub enum RetryDecision {
276 Deferred,
279 Final {
283 task: Task,
285 },
286 Dropped(&'static str),
288}
289
290fn is_failure(status: TaskStatus) -> bool {
293 matches!(status, TaskStatus::Fatal | TaskStatus::Invalid)
294}
295
296fn 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
306fn retries_exhausted(progress: &TaskProgress) -> bool { progress.retries > MAX_DISPATCH_RETRIES }
308
309pub const SETTLING_HARD_LIMIT: usize = 50_000;
313
314pub const DEFAULT_LATE_RESULT_WINDOW_SECONDS: i64 = 3600;
317
318pub 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 pub fn new() -> Self { Self::default() }
352
353 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 pub fn len(&self) -> usize { self.len.load(Ordering::Acquire) }
368
369 pub fn is_empty(&self) -> bool { self.len() == 0 }
371
372 pub fn awaiting_replies(&self) -> usize {
376 self
377 .len()
378 .saturating_sub(self.answered.load(Ordering::Acquire))
379 }
380
381 pub fn settling_len(&self) -> usize { self.settling.load(Ordering::Acquire) }
383
384 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 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 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 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 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 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 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 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 pub fn remove(&self, taskid: i64) -> Option<TaskProgress> {
508 if taskid < 0 {
509 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 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 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 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 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 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 let (_, previous) = slot.remove_entry();
617 self.note_left(&previous);
618 }
619 return RetryDecision::Final { task };
620 }
621 leased.progress.last_cause = cause;
622 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 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 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 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 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 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 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 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 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 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 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 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 pub fn kept_on_restart(&self) -> Vec<i64> { self.map.iter().map(|entry| *entry.key()).collect() }
881
882 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
895pub 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 let mut retries = 0;
913 while retries < 3 {
914 thread::sleep(Duration::new(2, 0)); 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
943pub 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
954pub const MAX_DISPATCH_RETRIES: i64 = 1;
965
966pub enum ExpiredOutcome {
968 Requeue(TaskProgress),
970 Fatal(TaskReport),
972}
973
974pub 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 created_at: expired.created_at,
1004 retries: expired.retries + 1,
1005 lease_timeout_seconds: expired.lease_timeout_seconds,
1007 attempt_workers: expired.attempt_workers,
1009 last_cause: expired.last_cause,
1011 run_start: expired.run_start,
1013 })
1014 }
1015}
1016
1017#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1021pub struct ReapSummary {
1022 pub requeued: usize,
1024 pub dead_lettered: usize,
1026 pub forgotten: usize,
1028}
1029
1030pub 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
1062pub 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
1077pub 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
1085pub 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
1115pub 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 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; set.insert(short);
1160 let mut long = dummy_progress(2);
1161 long.created_at = now - 100;
1162 long.lease_timeout_seconds = 3600; 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 let set = InFlightSet::new();
1177 set.insert(dummy_progress(1)); set.insert(dummy_progress(2)); set.insert(TaskProgress {
1180 retries: MAX_DISPATCH_RETRIES + 1, ..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 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 set.remove(1);
1227 assert_eq!(set.len(), 1);
1228 }
1229
1230 #[test]
1231 fn inflight_count_ignores_duplicate_insert_and_negative_remove() {
1232 let set = InFlightSet::new();
1236 set.insert(dummy_progress(7));
1237 set.insert(dummy_progress(7)); 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 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; tp.retries = retries;
1312 tp
1313 }
1314
1315 #[test]
1316 fn classify_expired_retries_then_gives_up() {
1317 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 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 assert_eq!(log.record(), Some(1));
1334 assert_eq!(log.record(), None);
1336 assert_eq!(log.record(), None);
1337 std::thread::sleep(Duration::from_millis(50));
1340 assert_eq!(log.record(), Some(3));
1341 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)); progress.insert(expired_progress(2, 9, MAX_DISPATCH_RETRIES + 1)); 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 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 let reports: Vec<TaskReport> = done_rx.try_iter().collect();
1364 assert_eq!(reports.len(), 1);
1365 assert_eq!(reports[0].task.id, 2);
1366 assert_eq!(progress.len(), 0);
1368 }
1369
1370 #[test]
1371 fn a_failed_sandbox_lookup_is_not_cached_as_an_ordinary_corpus() {
1372 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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"])); 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 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 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 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 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)); 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 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 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 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 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}