1use core::cmp::Reverse;
8use core::time::Duration;
9use std::collections::{BTreeMap, BinaryHeap, VecDeque};
10use std::sync::Arc;
11
12use rustc_hash::{FxHashMap, FxHashSet};
13
14type StrMap<V> = std::collections::HashMap<Arc<str>, V>;
17type StrSet = std::collections::HashSet<Arc<str>>;
18
19use crate::autoscale::{AutoscaleState, Signals};
20use crate::config::{CoreConfig, OverflowPolicy, PoolConfig, ResourceKind, UnknownDependency};
21use crate::io::*;
22use crate::lane::Lane;
23use crate::reservations::Reservations;
24use crate::rng::Rng;
25use crate::snapshot::{GroupSnapshot, PoolSnapshot, Snapshot};
26use crate::spec::*;
27use crate::stats::{DurStats, P90Stats, Window};
28use crate::time::{Time, secs};
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
35struct Key {
36 idx: u32,
37 generation: u32,
38}
39
40#[derive(Copy, Clone, Debug, PartialEq, Eq)]
41enum Loc {
42 None,
43 Lane,
44 Edf,
45 Due,
46 Parked(ResourceId),
47 LabelWait,
49 Fair,
51}
52
53#[derive(Clone, Debug)]
54struct FixedInfo {
55 at: Time,
56 tolerance: Duration,
57 on_miss: MissPolicy,
58 res_start: Time,
60 held: bool,
61 hold_pending: bool,
62 missed: bool,
63 skip_if_live: Option<TaskId>,
65}
66
67struct Entry<P> {
68 id: TaskId,
69 seq: u64,
70 arrival: Time,
71 payload: Arc<P>,
72 kind: u32,
73 pool: u16,
74 class: u8,
75 status: TaskStatus,
76 loc: Loc,
77 key: Option<Arc<str>>,
78 deadline: Time,
79 lst: Time,
80 hint_ns: u64,
81 timeout_ns: u64,
82 retry: Option<Box<RetryPolicy>>,
83 attempt: u32,
84 unmet: u32,
85 awaiting: u32,
86 has_awaits: bool,
88 pending: bool,
90 lease: Option<LeaseId>,
91 retry_at: Time,
92 dependents: Vec<(TaskId, EdgeCond)>,
93 resources: Option<Box<[(ResourceId, u32)]>>,
94 tags: Option<Tags>,
95 fixed: Option<Box<FixedInfo>>,
96 constraint: u32,
98 tenant: u32,
100 vtag: u64,
102}
103
104struct Slot<P> {
105 generation: u32,
106 entry: Option<Entry<P>>,
107}
108
109#[derive(Clone, Debug)]
110struct Lease {
111 key: Key,
112 id: TaskId,
113 pool: u16,
114 group: u16,
116 started: Time,
117 expires: Time,
118}
119
120#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
121enum Timer {
122 Retry(TaskId),
123 HoldAt(TaskId),
124 StartAt(TaskId),
125 MissAt(TaskId),
126 AwaitTimeout(TaskId),
127 AttemptTimeout(LeaseId),
128 LeaseExpiry(LeaseId),
129 Refill(u16),
130 Autoscale(u16),
131 Recurring(Arc<str>, Time),
132}
133
134struct Resource {
135 name: Arc<str>,
136 kind: ResourceKind,
137 available: f64,
138 capacity: f64,
139 last_refill: Time,
140 waiters: VecDeque<Key>,
141 refill_at: Option<Time>,
142}
143
144impl Resource {
145 fn refill(&mut self, now: Time) {
146 if let ResourceKind::TokenBucket { rate, .. } = self.kind {
147 let dt = now.diff_secs(self.last_refill);
148 if dt > 0.0 && rate > 0.0 {
149 self.available = (self.available + rate * dt).min(self.capacity);
150 }
151 }
152 self.last_refill = self.last_refill.max(now);
153 }
154}
155
156struct Pool {
157 cfg: PoolConfig,
158 capacity: u32,
160 busy: u32,
162 held: u32,
163 groups: Vec<Group>,
165 label_waiters: BTreeMap<u32, VecDeque<Key>>,
167 fair: Option<Box<FairState>>,
169 lanes: [Lane<Key>; Priority::COUNT],
170 edf: [BTreeMap<(Time, u64), Key>; Priority::COUNT],
171 due: BTreeMap<(Time, u64), Key>,
172 pending_holds: BTreeMap<(Time, u64), Key>,
173 held_tasks: BTreeMap<(Time, u64), Key>,
175 reservations: Reservations,
176 unstarted: Reservations,
178 retrying: [usize; Priority::COUNT],
180 pending: usize,
181 pending_class: [usize; Priority::COUNT],
182 autoscale: Option<AutoscaleState>,
183 arrivals: u64,
184 last_eval: Time,
185 stats: DurStats,
186 adhoc_stats: P90Stats,
188 window: Window,
189 last_acct: Time,
190 worker_secs: f64,
191 cost: f64,
192 dirty: bool,
193}
194
195struct FairState {
200 queues: [BTreeMap<(u64, u64), Key>; Priority::COUNT],
201 vtime: [u64; Priority::COUNT],
203 finish: FxHashMap<(u8, u32), u64>,
205 cost: FxHashMap<u32, u64>,
207 weights: StrMap<f64>,
208 default_weight: f64,
209}
210
211impl FairState {
212 fn new(cfg: &crate::config::Fairness) -> Self {
213 FairState {
214 queues: Default::default(),
215 vtime: [0; Priority::COUNT],
216 finish: FxHashMap::default(),
217 cost: FxHashMap::default(),
218 weights: cfg.weights.iter().cloned().collect(),
219 default_weight: cfg.default_weight,
220 }
221 }
222
223 fn len(&self) -> usize {
224 self.queues.iter().map(|q| q.len()).sum()
225 }
226}
227
228struct Group {
230 labels: Labels,
231 capacity: u32,
232 busy: u32,
233}
234
235impl Group {
236 #[inline]
237 fn free(&self) -> u32 {
238 self.capacity.saturating_sub(self.busy)
239 }
240}
241
242fn satisfies(group: &[(Arc<str>, Arc<str>)], need: &[(Arc<str>, Arc<str>)]) -> bool {
244 need.iter().all(|n| group.binary_search(n).is_ok())
245}
246
247fn normalize(labels: &[(Arc<str>, Arc<str>)]) -> Labels {
249 let mut v = labels.to_vec();
250 v.sort();
251 v.dedup();
252 v.into()
253}
254
255impl Pool {
256 #[inline]
257 fn free(&self) -> u32 {
258 self.capacity.saturating_sub(self.busy + self.held)
259 }
260 fn account(&mut self, now: Time) {
261 if self.last_acct == Time::ZERO {
262 self.last_acct = now;
264 return;
265 }
266 let dt = now.diff_secs(self.last_acct);
267 if dt <= 0.0 {
268 return;
269 }
270 self.last_acct = now;
271 let cap = self.capacity as f64;
272 let busy = self.busy as f64;
273 let b = self.window.at(now);
274 b.busy_secs += busy * dt;
275 b.capacity_secs += cap * dt;
276 self.worker_secs += cap * dt;
277 if let Some(a) = &self.cfg.autoscale {
278 self.cost += cap * dt * a.cost_per_worker_second;
279 }
280 }
281 fn ready_len(&self) -> usize {
282 self.lanes.iter().map(|l| l.len()).sum::<usize>()
283 + self.edf.iter().map(|l| l.len()).sum::<usize>()
284 + self.due.len()
285 + self.fair.as_ref().map_or(0, |f| f.len())
286 }
287}
288
289struct Recurring<P> {
290 spec: RecurringSpec<P>,
291 cron: croner::Cron,
292 tz: chrono_tz::Tz,
293 next: Option<Time>,
294 last_task: Option<TaskId>,
295 materialized: Option<Time>,
297 chain_last: Option<TaskId>,
300}
301
302enum Parent {
303 Live(TaskId),
304 Done(TaskStatus),
305 Await(Arc<str>),
306}
307
308#[derive(Clone, Debug, Default)]
310pub struct Totals {
311 pub submitted: u64,
313 pub dispatched: u64,
315 pub succeeded: u64,
317 pub failed: u64,
319 pub dead_lettered: u64,
321 pub cancelled: u64,
323 pub skipped: u64,
325 pub missed: u64,
327 pub shed: u64,
329 pub retries: u64,
331 pub timeouts: u64,
333 pub lease_expiries: u64,
335 pub recurring_conflicts: u64,
337 pub recurring_invalid: u64,
340 pub stale_completions: u64,
343}
344
345pub struct Core<P> {
351 cfg: CoreConfig,
352 pools: Vec<Pool>,
353 pool_index: FxHashMap<Arc<str>, u16>,
354 resources: Vec<Resource>,
355 resource_index: FxHashMap<Arc<str>, u16>,
356 slots: Vec<Slot<P>>,
357 free_slots: Vec<u32>,
358 index: FxHashMap<TaskId, Key>,
359 keys: StrMap<TaskId>,
360 awaits: StrMap<Vec<(TaskId, EdgeCond)>>,
361 retained: FxHashMap<TaskId, (TaskStatus, Time)>,
362 retained_keys: StrMap<TaskId>,
363 retention_q: VecDeque<(Time, TaskId, Option<Arc<str>>)>,
364 leases: FxHashMap<LeaseId, Lease>,
365 timers: BinaryHeap<Reverse<(Time, u64, Timer)>>,
366 timer_seq: u64,
367 kinds: Vec<Arc<str>>,
368 kind_index: StrMap<u32>,
369 tenants: Vec<Arc<str>>,
371 tenant_index: StrMap<u32>,
372 constraints: Vec<Labels>,
374 constraint_index: std::collections::HashMap<Labels, u32>,
375 dur: FxHashMap<(u16, u32), DurStats>,
376 recurring: BTreeMap<Arc<str>, Recurring<P>>,
377 actions: VecDeque<Action<P>>,
378 rng: Rng,
379 next_id: u64,
380 next_seq: u64,
381 next_lease: u64,
382 now: Time,
383 dirty: Vec<u16>,
384 total_pending: usize,
385 pending_class: [usize; Priority::COUNT],
386 space_waiters: bool,
387 emit_transitions: bool,
388 totals: Totals,
389 empty_tags: Tags,
390 recovering_keys: Option<StrSet>,
392 recurring_history: StrMap<(Option<TaskId>, Option<Time>)>,
395 started: bool,
397 awaited_by: FxHashMap<TaskId, Box<[Arc<str>]>>,
399}
400
401impl<P> Core<P> {
402 pub fn new(cfg: CoreConfig) -> Self {
406 let cfg = cfg.sanitized();
407 assert!(!cfg.pools.is_empty(), "at least one pool is required");
408 assert!(cfg.pools.len() <= u16::MAX as usize && cfg.resources.len() <= u16::MAX as usize);
409 let mut pool_index = FxHashMap::default();
410 let mut pools = Vec::new();
411 let mut timers = BinaryHeap::new();
412 for (i, pc) in cfg.pools.iter().enumerate() {
413 assert!(
414 pool_index.insert(pc.name.clone(), i as u16).is_none(),
415 "duplicate pool name"
416 );
417 assert!(
418 pc.min_workers <= pc.max_workers,
419 "min_workers > max_workers"
420 );
421 let initial = pc.initial_workers.min(pc.max_workers);
423 if let Some(a) = &pc.autoscale {
424 timers.push(Reverse((
425 Time::ZERO + a.interval,
426 i as u64,
427 Timer::Autoscale(i as u16),
428 )));
429 }
430 pools.push(Pool {
431 cfg: pc.clone(),
432 capacity: initial,
433 busy: 0,
434 held: 0,
435 groups: vec![Group {
436 labels: Arc::from(Vec::new()),
437 capacity: initial,
438 busy: 0,
439 }],
440 label_waiters: BTreeMap::new(),
441 fair: pc.fairness.as_ref().map(|f| Box::new(FairState::new(f))),
442 lanes: Default::default(),
443 edf: Default::default(),
444 due: BTreeMap::new(),
445 pending_holds: BTreeMap::new(),
446 held_tasks: BTreeMap::new(),
447 reservations: Reservations::default(),
448 unstarted: Reservations::default(),
449 retrying: [0; Priority::COUNT],
450 pending: 0,
451 pending_class: [0; Priority::COUNT],
452 autoscale: pc.autoscale.as_ref().map(|_| AutoscaleState::default()),
453 arrivals: 0,
454 last_eval: Time::ZERO,
455 stats: DurStats::default(),
456 adhoc_stats: P90Stats::default(),
457 window: Window::new(cfg.metrics_window),
458 last_acct: Time::ZERO,
459 worker_secs: 0.0,
460 cost: 0.0,
461 dirty: false,
462 });
463 }
464 let mut resource_index = FxHashMap::default();
465 let resources = cfg
466 .resources
467 .iter()
468 .enumerate()
469 .map(|(i, rc)| {
470 assert!(
471 resource_index.insert(rc.name.clone(), i as u16).is_none(),
472 "duplicate resource name"
473 );
474 let cap = match rc.kind {
475 ResourceKind::Semaphore { permits } => permits as f64,
476 ResourceKind::TokenBucket { burst, .. } => burst as f64,
477 };
478 Resource {
479 name: rc.name.clone(),
480 kind: rc.kind.clone(),
481 available: cap,
482 capacity: cap,
483 last_refill: Time::ZERO,
484 waiters: VecDeque::new(),
485 refill_at: None,
486 }
487 })
488 .collect();
489 let timer_seq = pools.len() as u64;
490 let id_epoch = cfg.id_epoch as u64;
491 Core {
492 rng: Rng::new(cfg.seed),
493 next_id: match cfg.id_shard {
494 Some(s) => TaskId::first_in_shard(s, cfg.id_epoch).0,
495 None => id_epoch << 48 | 1,
496 },
497 cfg,
498 pools,
499 pool_index,
500 resources,
501 resource_index,
502 slots: Vec::new(),
503 free_slots: Vec::new(),
504 index: FxHashMap::default(),
505 keys: StrMap::default(),
506 awaits: StrMap::default(),
507 retained: FxHashMap::default(),
508 retained_keys: StrMap::default(),
509 retention_q: VecDeque::new(),
510 leases: FxHashMap::default(),
511 timers,
512 timer_seq,
513 kinds: Vec::new(),
514 kind_index: StrMap::default(),
515 tenants: vec![Arc::from("")],
516 tenant_index: StrMap::default(),
517 constraints: vec![Arc::from(Vec::new())],
518 constraint_index: std::collections::HashMap::new(),
519 dur: FxHashMap::default(),
520 recurring: BTreeMap::new(),
521 actions: VecDeque::new(),
522 next_seq: 0,
523 next_lease: id_epoch << 48 | 1,
524 now: Time::ZERO,
525 dirty: Vec::new(),
526 total_pending: 0,
527 pending_class: [0; Priority::COUNT],
528 space_waiters: false,
529 emit_transitions: false,
530 totals: Totals::default(),
531 empty_tags: Arc::from(Vec::new()),
532 recovering_keys: None,
533 recurring_history: StrMap::default(),
534 started: false,
535 awaited_by: FxHashMap::default(),
536 }
537 }
538
539 pub fn set_emit_transitions(&mut self, on: bool) {
541 self.emit_transitions = on;
542 }
543
544 pub fn config(&self) -> &CoreConfig {
546 &self.cfg
547 }
548
549 pub fn pool_id(&self, name: &str) -> Option<PoolId> {
551 self.pool_index.get(name).map(|&i| PoolId(i))
552 }
553
554 pub fn pool_name(&self, pool: PoolId) -> &Arc<str> {
556 &self.pools[pool.0 as usize].cfg.name
557 }
558
559 pub fn pool_capacity(&self, pool: PoolId) -> u32 {
561 self.pools[pool.0 as usize].capacity
562 }
563
564 pub fn totals(&self) -> &Totals {
566 &self.totals
567 }
568
569 #[inline]
571 pub fn poll_action(&mut self) -> Option<Action<P>> {
572 self.actions.pop_front()
573 }
574
575 pub fn drain_actions(&mut self, out: &mut Vec<Action<P>>) {
577 out.extend(self.actions.drain(..));
578 }
579
580 pub fn has_actions(&self) -> bool {
582 !self.actions.is_empty()
583 }
584
585 pub fn next_wakeup(&self) -> Option<Time> {
587 self.timers.peek().map(|Reverse((t, _, _))| *t)
588 }
589
590 pub fn tick(&mut self, now: Time) {
592 self.advance(now);
593 self.pump();
594 }
595
596 pub fn submit(&mut self, now: Time, spec: TaskSpec<P>) -> Result<Submitted, SubmitError> {
602 self.try_submit(now, spec).map_err(|(e, _)| e)
603 }
604
605 #[allow(clippy::result_large_err)] pub fn try_submit(
609 &mut self,
610 now: Time,
611 spec: TaskSpec<P>,
612 ) -> Result<Submitted, (SubmitError, TaskSpec<P>)> {
613 self.advance(now);
614 if let Some(id) = self.existing_key(spec.key.as_deref()) {
615 return Ok(Submitted {
616 id,
617 duplicate: true,
618 });
619 }
620 let pool = match self.validate(&spec, &StrSet::default()) {
621 Ok(p) => p,
622 Err(e) => return Err((e, spec)),
623 };
624 if let Some(f) = &spec.fixed_start {
627 let (s, e) = self.reservation_interval(pool, &spec, f);
628 if self.pools[pool as usize].reservations.max_overlap(s, e)
629 >= self.pools[pool as usize].cfg.reservation_limit()
630 {
631 return Err((SubmitError::ReservationConflict, spec));
632 }
633 }
634 if let Err(e) = self.check_bounds(pool, spec.priority, 1, true) {
635 return Err((e, spec));
636 }
637 let fixed = spec.fixed_start.is_some();
638 let id = self.alloc_id();
639 let seq = self.alloc_seq();
640 self.insert(now, id, seq, now, spec, pool, 0, None);
641 if fixed {
642 self.advance(now);
644 }
645 self.pump();
646 Ok(Submitted {
647 id,
648 duplicate: false,
649 })
650 }
651
652 pub fn submit_batch(
655 &mut self,
656 now: Time,
657 specs: Vec<TaskSpec<P>>,
658 ) -> Result<Vec<Submitted>, (usize, SubmitError)> {
659 self.advance(now);
660 let mut batch_keys: StrMap<usize> = StrMap::default();
662 for (i, s) in specs.iter().enumerate() {
663 if let Some(k) = &s.key
664 && self.existing_key(Some(k)).is_none()
665 && batch_keys.insert(k.clone(), i).is_some()
666 {
667 return Err((i, SubmitError::DuplicateKeyInBatch(k.clone())));
668 }
669 }
670 let key_set: StrSet = batch_keys.keys().cloned().collect();
671 let mut pools = Vec::with_capacity(specs.len());
672 for (i, s) in specs.iter().enumerate() {
673 if self.existing_key(s.key.as_deref()).is_some() {
674 pools.push(u16::MAX);
675 continue;
676 }
677 pools.push(self.validate(s, &key_set).map_err(|e| (i, e))?);
678 }
679 let n = specs.len();
681 let mut indeg = vec![0u32; n];
682 let mut children: Vec<Vec<usize>> = vec![Vec::new(); n];
683 for (i, s) in specs.iter().enumerate() {
684 if pools[i] == u16::MAX {
685 continue;
686 }
687 for d in &s.deps {
688 if let TaskRef::Key(k) = &d.target
689 && let Some(&j) = batch_keys.get(k)
690 {
691 if self.keys.contains_key(k) || self.retained_keys.contains_key(k) {
692 continue;
693 }
694 indeg[i] += 1;
695 children[j].push(i);
696 }
697 }
698 }
699 let mut order: Vec<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
700 let mut h = 0;
701 while h < order.len() {
702 let j = order[h];
703 h += 1;
704 for &c in &children[j] {
705 indeg[c] -= 1;
706 if indeg[c] == 0 {
707 order.push(c);
708 }
709 }
710 }
711 if order.len() != n {
712 let bad = (0..n).find(|&i| indeg[i] > 0).unwrap_or(0);
713 return Err((bad, SubmitError::Cycle));
714 }
715 let mut counts: FxHashMap<(u16, Priority), usize> = FxHashMap::default();
717 for (i, s) in specs.iter().enumerate() {
718 if pools[i] != u16::MAX {
719 *counts.entry((pools[i], s.priority)).or_default() += 1;
720 }
721 }
722 if let Some(i) = self.batch_cycle(&specs, &pools, &batch_keys) {
723 return Err((i, SubmitError::Cycle));
724 }
725 self.check_batch_bounds(&counts).map_err(|e| (0, e))?;
726 let mut tentative = Vec::new();
728 let mut conflict = None;
729 for (i, s) in specs.iter().enumerate() {
730 if pools[i] == u16::MAX {
731 continue;
732 }
733 if let Some(f) = &s.fixed_start {
734 let (st, en) = self.reservation_interval(pools[i], s, f);
735 let pool = &mut self.pools[pools[i] as usize];
736 if pool.reservations.max_overlap(st, en) >= pool.cfg.reservation_limit() {
737 conflict = Some(i);
738 break;
739 }
740 let tid = TaskId(u64::MAX - i as u64);
741 pool.reservations.insert(tid, st, en);
742 tentative.push((pools[i], tid, st));
743 }
744 }
745 for (p, tid, st) in tentative {
746 self.pools[p as usize].reservations.remove(tid, st);
747 }
748 if let Some(i) = conflict {
749 return Err((i, SubmitError::ReservationConflict));
750 }
751 let mut out = vec![
753 Submitted {
754 id: TaskId(0),
755 duplicate: false
756 };
757 n
758 ];
759 let mut specs: Vec<Option<TaskSpec<P>>> = specs.into_iter().map(Some).collect();
760 for i in order {
761 let spec = specs[i].take().expect("each index once");
762 if pools[i] == u16::MAX {
763 let id = self.existing_key(spec.key.as_deref()).expect("checked");
764 out[i] = Submitted {
765 id,
766 duplicate: true,
767 };
768 continue;
769 }
770 let id = self.alloc_id();
771 let seq = self.alloc_seq();
772 self.insert(now, id, seq, now, spec, pools[i], 0, None);
773 out[i] = Submitted {
774 id,
775 duplicate: false,
776 };
777 }
778 self.advance(now);
779 self.pump();
780 Ok(out)
781 }
782
783 fn batch_cycle(
786 &self,
787 specs: &[TaskSpec<P>],
788 pools: &[u16],
789 batch_keys: &StrMap<usize>,
790 ) -> Option<usize> {
791 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
792 enum N {
793 B(usize),
794 L(TaskId),
795 }
796 let n = specs.len();
797 let mut batch_children: Vec<Vec<usize>> = vec![Vec::new(); n];
798 let mut live_children: FxHashMap<TaskId, Vec<usize>> = FxHashMap::default();
799 for (i, s) in specs.iter().enumerate() {
800 if pools[i] == u16::MAX {
801 continue;
802 }
803 for d in &s.deps {
804 match &d.target {
805 TaskRef::Key(k) => {
806 if let Some(&id) = self.keys.get(k) {
807 live_children.entry(id).or_default().push(i);
808 } else if let Some(&j) = batch_keys.get(k) {
809 batch_children[j].push(i);
810 }
811 }
812 TaskRef::Id(id) => {
813 if self.index.contains_key(id) {
814 live_children.entry(*id).or_default().push(i);
815 }
816 }
817 }
818 }
819 }
820 let succ = |node: N| -> Vec<N> {
821 let mut out = Vec::new();
822 match node {
823 N::B(j) => {
824 out.extend(batch_children[j].iter().map(|&i| N::B(i)));
825 if let Some(k) = &specs[j].key
826 && let Some(w) = self.awaits.get(k)
827 {
828 out.extend(
829 w.iter()
830 .filter(|(id, _)| self.index.contains_key(id))
831 .map(|(id, _)| N::L(*id)),
832 );
833 }
834 }
835 N::L(id) => {
836 if let Some(e) = self.entry_by_id(id) {
837 out.extend(e.dependents.iter().map(|d| N::L(d.0)));
838 if let Some(k) = &e.key
839 && let Some(w) = self.awaits.get(k)
840 {
841 out.extend(w.iter().map(|(id, _)| N::L(*id)));
842 }
843 }
844 if let Some(c) = live_children.get(&id) {
845 out.extend(c.iter().map(|&i| N::B(i)));
846 }
847 }
848 }
849 out
850 };
851 let mut color: FxHashMap<N, u8> = FxHashMap::default(); for start in (0..n).filter(|&i| pools[i] != u16::MAX) {
854 if color.contains_key(&N::B(start)) {
855 continue;
856 }
857 let mut stack: Vec<(N, Vec<N>)> = vec![(N::B(start), succ(N::B(start)))];
858 color.insert(N::B(start), 1);
859 while let Some((node, children)) = stack.last_mut() {
860 if let Some(next) = children.pop() {
861 match color.get(&next) {
862 Some(1) => {
863 return Some(match next {
864 N::B(i) => i,
865 N::L(_) => match *node {
866 N::B(i) => i,
867 N::L(_) => start,
868 },
869 });
870 }
871 Some(_) => {}
872 None => {
873 color.insert(next, 1);
874 let c = succ(next);
875 stack.push((next, c));
876 }
877 }
878 } else {
879 color.insert(*node, 2);
880 stack.pop();
881 }
882 }
883 }
884 None
885 }
886
887 fn existing_key(&self, key: Option<&str>) -> Option<TaskId> {
888 let k = key?;
889 self.keys
890 .get(k)
891 .or_else(|| self.retained_keys.get(k))
892 .copied()
893 }
894
895 fn alloc_id(&mut self) -> TaskId {
896 let id = TaskId(self.next_id);
897 self.next_id += 1;
898 id
899 }
900
901 fn alloc_seq(&mut self) -> u64 {
902 let s = self.next_seq;
903 self.next_seq += 1;
904 s
905 }
906
907 fn validate(&self, spec: &TaskSpec<P>, batch_keys: &StrSet) -> Result<u16, SubmitError> {
909 let pool = self.check_config(spec)?;
910 self.check_deps(spec, pool, batch_keys)?;
911 Ok(pool)
912 }
913
914 fn check_config(&self, spec: &TaskSpec<P>) -> Result<u16, SubmitError> {
917 let pool = *self
918 .pool_index
919 .get(&spec.pool)
920 .ok_or_else(|| SubmitError::UnknownPool(spec.pool.clone()))?;
921 for (name, amount) in &spec.resources {
922 let r = *self
923 .resource_index
924 .get(name)
925 .ok_or_else(|| SubmitError::UnknownResource(name.clone()))?;
926 if *amount as f64 > self.resources[r as usize].capacity {
927 return Err(SubmitError::ResourceExceedsCapacity(name.clone()));
928 }
929 if *amount == 0 {
930 return Err(SubmitError::Invalid("resource amount must be > 0"));
931 }
932 }
933 if let Some(r) = &spec.retry
934 && r.max_attempts == 0
935 {
936 return Err(SubmitError::Invalid("retry.max_attempts must be >= 1"));
937 }
938 Ok(pool)
939 }
940
941 fn check_deps(
942 &self,
943 spec: &TaskSpec<P>,
944 pool: u16,
945 batch_keys: &StrSet,
946 ) -> Result<(), SubmitError> {
947 let await_ok = matches!(
948 self.pools[pool as usize].cfg.unknown_dependency,
949 UnknownDependency::Await { .. }
950 );
951 let mut live_parents: FxHashSet<TaskId> = FxHashSet::default();
952 for d in &spec.deps {
953 match &d.target {
954 TaskRef::Id(id) => {
955 if self.index.contains_key(id) {
956 live_parents.insert(*id);
957 } else if !self.retained.contains_key(id) {
958 return Err(SubmitError::UnknownDependency(id.to_string().into()));
959 }
960 }
961 TaskRef::Key(k) => {
962 if spec.key.as_ref() == Some(k) {
963 return Err(SubmitError::Cycle);
964 }
965 if let Some(id) = self.keys.get(k) {
966 live_parents.insert(*id);
967 } else if !self.retained_keys.contains_key(k)
968 && !batch_keys.contains(k)
969 && !await_ok
970 {
971 return Err(SubmitError::UnknownDependency(k.clone()));
972 }
973 }
974 }
975 }
976 if let Some(k) = &spec.key
978 && let Some(waiting) = self.awaits.get(k)
979 && !live_parents.is_empty()
980 && self.reaches_any(waiting.iter().map(|w| w.0), &live_parents)
981 {
982 return Err(SubmitError::Cycle);
983 }
984 Ok(())
985 }
986
987 fn reaches_any(
988 &self,
989 start: impl Iterator<Item = TaskId>,
990 targets: &FxHashSet<TaskId>,
991 ) -> bool {
992 let mut stack: Vec<TaskId> = start.collect();
993 let mut seen: FxHashSet<TaskId> = FxHashSet::default();
994 while let Some(id) = stack.pop() {
995 if targets.contains(&id) {
996 return true;
997 }
998 if !seen.insert(id) {
999 continue;
1000 }
1001 if let Some(e) = self.entry_by_id(id) {
1002 stack.extend(e.dependents.iter().map(|d| d.0));
1003 if let Some(k) = &e.key
1004 && let Some(w) = self.awaits.get(k)
1005 {
1006 stack.extend(w.iter().map(|d| d.0));
1007 }
1008 }
1009 }
1010 false
1011 }
1012
1013 fn check_bounds(
1014 &mut self,
1015 pool: u16,
1016 class: Priority,
1017 n: usize,
1018 allow_shed: bool,
1019 ) -> Result<(), SubmitError> {
1020 let c = class.index();
1021 loop {
1022 let p = &self.pools[pool as usize];
1023 let over = |cur: usize, lim: Option<usize>| lim.is_some_and(|l| cur + n > l);
1024 let class_full = over(
1027 self.pending_class[c],
1028 self.cfg.global_bounds.max_pending_per_class[c],
1029 ) || over(p.pending_class[c], p.cfg.bounds.max_pending_per_class[c]);
1030 let total_full = over(self.total_pending, self.cfg.global_bounds.max_pending)
1031 || over(p.pending, p.cfg.bounds.max_pending);
1032 if !class_full && !total_full {
1033 return Ok(());
1034 }
1035 let victim = match (p.cfg.overflow, allow_shed) {
1036 (OverflowPolicy::Wait, _) => {
1037 self.space_waiters = true;
1038 return Err(SubmitError::Full);
1039 }
1040 (OverflowPolicy::Reject, _) | (_, false) => return Err(SubmitError::Overloaded),
1041 (OverflowPolicy::ShedLowest, true) if class_full => None,
1043 (OverflowPolicy::ShedLowest, true) => (c + 1..Priority::COUNT)
1044 .rev()
1045 .find_map(|vc| self.shed_victim(pool, vc, true)),
1046 (OverflowPolicy::ShedOldest, true) => {
1047 let low = Priority::Low.index();
1048 if class_full && c != low {
1049 None
1050 } else {
1051 self.shed_victim(pool, low, false)
1052 }
1053 }
1054 };
1055 match victim {
1056 Some(k) => self.finish(k, TaskStatus::Shed, Some("shed under overload".into())),
1057 None => return Err(SubmitError::Overloaded),
1058 }
1059 }
1060 }
1061
1062 fn shed_victim(&self, pool: u16, class: usize, newest: bool) -> Option<Key> {
1066 let p = &self.pools[pool as usize];
1067 let seq = |k: &Key| self.entry(*k).seq;
1068 let lane = if newest {
1069 p.lanes[class].last()
1070 } else {
1071 p.lanes[class].first()
1072 };
1073 let edf = p.edf[class].values();
1074 let edf = if newest {
1075 edf.max_by_key(|k| seq(k))
1076 } else {
1077 edf.min_by_key(|k| seq(k))
1078 };
1079 let parked = self
1080 .resources
1081 .iter()
1082 .flat_map(|r| r.waiters.iter())
1083 .filter(|k| {
1084 self.slot_entry(**k).is_some_and(|e| {
1085 matches!(e.loc, Loc::Parked(_)) && e.pool == pool && e.class as usize == class
1086 })
1087 });
1088 let parked = if newest {
1089 parked.max_by_key(|k| seq(k))
1090 } else {
1091 parked.min_by_key(|k| seq(k))
1092 };
1093 let waiting = p.label_waiters.values().flatten().filter(|k| {
1094 self.slot_entry(**k)
1095 .is_some_and(|e| e.loc == Loc::LabelWait && e.class as usize == class)
1096 });
1097 let waiting = if newest {
1098 waiting.max_by_key(|k| seq(k))
1099 } else {
1100 waiting.min_by_key(|k| seq(k))
1101 };
1102 let fair = p.fair.as_ref().and_then(|f| {
1103 let q = f.queues[class].values();
1104 if newest {
1105 q.max_by_key(|k| seq(k))
1106 } else {
1107 q.min_by_key(|k| seq(k))
1108 }
1109 });
1110 let candidates = [
1111 lane,
1112 edf.copied(),
1113 parked.copied(),
1114 waiting.copied(),
1115 fair.copied(),
1116 ]
1117 .into_iter()
1118 .flatten();
1119 if newest {
1120 candidates.max_by_key(|k| seq(k))
1121 } else {
1122 candidates.min_by_key(|k| seq(k))
1123 }
1124 }
1125
1126 fn check_batch_bounds(
1128 &mut self,
1129 counts: &FxHashMap<(u16, Priority), usize>,
1130 ) -> Result<(), SubmitError> {
1131 for (&(p, c), &k) in counts {
1132 self.check_bounds(p, c, k, false)?;
1133 }
1134 let over = |cur: usize, n: usize, lim: Option<usize>| lim.is_some_and(|l| cur + n > l);
1135 let total: usize = counts.values().sum();
1136 let mut full = over(
1137 self.total_pending,
1138 total,
1139 self.cfg.global_bounds.max_pending,
1140 );
1141 let mut per_pool: FxHashMap<u16, usize> = FxHashMap::default();
1142 for c in Priority::ALL {
1143 let n: usize = counts
1144 .iter()
1145 .filter(|((_, pc), _)| *pc == c)
1146 .map(|(_, k)| k)
1147 .sum();
1148 full |= over(
1149 self.pending_class[c.index()],
1150 n,
1151 self.cfg.global_bounds.max_pending_per_class[c.index()],
1152 );
1153 }
1154 for (&(p, _), &k) in counts {
1155 *per_pool.entry(p).or_default() += k;
1156 }
1157 for (&p, &k) in &per_pool {
1158 let pool = &self.pools[p as usize];
1159 full |= over(pool.pending, k, pool.cfg.bounds.max_pending);
1160 }
1161 if !full {
1162 return Ok(());
1163 }
1164 let wait = per_pool
1165 .keys()
1166 .any(|p| self.pools[*p as usize].cfg.overflow == OverflowPolicy::Wait);
1167 if wait {
1168 self.space_waiters = true;
1169 Err(SubmitError::Full)
1170 } else {
1171 Err(SubmitError::Overloaded)
1172 }
1173 }
1174
1175 fn reservation_interval(&self, pool: u16, spec: &TaskSpec<P>, f: &FixedStart) -> (Time, Time) {
1176 let kind = self.kind_index.get(&spec.kind).copied();
1177 let d = self.estimate_raw(pool, kind, spec.duration_hint.map(nanos).unwrap_or(0));
1178 (f.at - f.tolerance, f.at + d + f.tolerance)
1179 }
1180
1181 fn intern_tenant(&mut self, t: &Arc<str>) -> u32 {
1182 if let Some(&i) = self.tenant_index.get(t) {
1183 return i;
1184 }
1185 let i = self.tenants.len() as u32;
1186 self.tenants.push(t.clone());
1187 self.tenant_index.insert(t.clone(), i);
1188 i
1189 }
1190
1191 fn fair_tag(&mut self, pool: u16, class: usize, tenant: u32) -> u64 {
1193 let name = self.tenants[tenant as usize].clone();
1194 let f = self.pools[pool as usize].fair.as_mut().unwrap();
1195 let cost = *f.cost.entry(tenant).or_insert_with(|| {
1196 let w = f.weights.get(&name).copied().unwrap_or(f.default_weight);
1197 ((1e6 / w).round() as u64).max(1)
1198 });
1199 let start = f.vtime[class].max(f.finish.get(&(class as u8, tenant)).copied().unwrap_or(0));
1200 f.finish.insert((class as u8, tenant), start + cost);
1201 if f.finish.len() > 4096 {
1202 let vtime = f.vtime;
1204 f.finish.retain(|(c, _), fin| *fin > vtime[*c as usize]);
1205 }
1206 start
1207 }
1208
1209 fn intern_constraint(&mut self, labels: &[(Arc<str>, Arc<str>)]) -> u32 {
1210 if labels.is_empty() {
1211 return 0;
1212 }
1213 let labels = normalize(labels);
1214 if let Some(&c) = self.constraint_index.get(&labels) {
1215 return c;
1216 }
1217 let c = self.constraints.len() as u32;
1218 self.constraints.push(labels.clone());
1219 self.constraint_index.insert(labels, c);
1220 c
1221 }
1222
1223 fn select_group(&self, pool: u16, constraint: u32) -> Option<u16> {
1227 let p = &self.pools[pool as usize];
1228 if p.groups.len() == 1 {
1229 let g = &p.groups[0];
1230 return (g.free() > 0 && constraint == 0).then_some(0);
1231 }
1232 let need = &self.constraints[constraint as usize];
1233 p.groups
1234 .iter()
1235 .enumerate()
1236 .filter(|(_, g)| g.free() > 0 && satisfies(&g.labels, need))
1237 .min_by_key(|(_, g)| (g.labels.len(), std::cmp::Reverse(g.free())))
1238 .map(|(i, _)| i as u16)
1239 }
1240
1241 fn park_label(&mut self, k: Key) {
1243 let (pool, c) = {
1244 let e = self.entry(k);
1245 (e.pool as usize, e.constraint)
1246 };
1247 self.entry_mut(k).loc = Loc::LabelWait;
1248 self.pools[pool]
1249 .label_waiters
1250 .entry(c)
1251 .or_default()
1252 .push_back(k);
1253 }
1254
1255 fn wake_labels(&mut self, pool: u16, group: u16) {
1258 let p = &self.pools[pool as usize];
1259 if p.label_waiters.is_empty() {
1260 return;
1261 }
1262 let labels = p.groups[group as usize].labels.clone();
1263 let mut budget = p.groups[group as usize].free();
1264 let matching: Vec<u32> = p
1265 .label_waiters
1266 .keys()
1267 .copied()
1268 .filter(|c| satisfies(&labels, &self.constraints[*c as usize]))
1269 .collect();
1270 while budget > 0 {
1271 let mut best: Option<(u64, u32)> = None;
1273 for &c in &matching {
1274 loop {
1275 let front = self.pools[pool as usize]
1276 .label_waiters
1277 .get(&c)
1278 .and_then(|q| q.front().copied());
1279 match front {
1280 Some(k) if !self.slots_live(k, Loc::LabelWait) => {
1281 let q = self.pools[pool as usize].label_waiters.get_mut(&c);
1282 q.unwrap().pop_front();
1283 }
1284 _ => break,
1285 }
1286 }
1287 if let Some(&k) = self.pools[pool as usize]
1288 .label_waiters
1289 .get(&c)
1290 .and_then(|q| q.front())
1291 {
1292 let seq = self.entry(k).seq;
1293 if best.is_none_or(|(s, _)| seq < s) {
1294 best = Some((seq, c));
1295 }
1296 }
1297 }
1298 let Some((_, c)) = best else { break };
1299 let q = self.pools[pool as usize].label_waiters.get_mut(&c).unwrap();
1300 let k = q.pop_front().unwrap();
1301 if q.is_empty() {
1302 self.pools[pool as usize].label_waiters.remove(&c);
1303 }
1304 self.requeue_ready(k);
1305 budget -= 1;
1306 }
1307 for c in matching {
1308 if self.pools[pool as usize]
1309 .label_waiters
1310 .get(&c)
1311 .is_some_and(|q| q.is_empty())
1312 {
1313 self.pools[pool as usize].label_waiters.remove(&c);
1314 }
1315 }
1316 }
1317
1318 fn slots_live(&self, k: Key, loc: Loc) -> bool {
1320 self.slot_entry(k).is_some_and(|e| e.loc == loc)
1321 }
1322
1323 fn requeue_ready(&mut self, k: Key) {
1325 let (pool, class, seq, has_deadline, lst) = {
1326 let e = self.entry(k);
1327 (
1328 e.pool,
1329 e.class as usize,
1330 e.seq,
1331 e.deadline != Time::MAX,
1332 e.lst,
1333 )
1334 };
1335 let vtag = self.entry(k).vtag;
1336 let p = &mut self.pools[pool as usize];
1337 if has_deadline {
1338 p.edf[class].insert((lst, seq), k);
1339 self.entry_mut(k).loc = Loc::Edf;
1340 } else if let Some(f) = &mut p.fair {
1341 f.queues[class].insert((vtag, seq), k);
1343 self.entry_mut(k).loc = Loc::Fair;
1344 } else {
1345 p.lanes[class].insert(seq, k);
1346 self.entry_mut(k).loc = Loc::Lane;
1347 }
1348 self.mark_dirty(pool);
1349 }
1350
1351 fn intern_kind(&mut self, k: &Arc<str>) -> u32 {
1352 if let Some(&i) = self.kind_index.get(k) {
1353 return i;
1354 }
1355 let i = self.kinds.len() as u32;
1356 self.kinds.push(k.clone());
1357 self.kind_index.insert(k.clone(), i);
1358 i
1359 }
1360
1361 fn estimate_raw(&self, pool: u16, kind: Option<u32>, hint_ns: u64) -> Duration {
1362 if hint_ns > 0 {
1363 return Duration::from_nanos(hint_ns);
1364 }
1365 kind.and_then(|k| self.dur.get(&(pool, k)))
1366 .and_then(|s| s.estimate())
1367 .or_else(|| self.pools[pool as usize].stats.estimate())
1368 .unwrap_or(self.pools[pool as usize].cfg.default_duration)
1369 }
1370
1371 fn hold_lead(&self, pool: u16, tolerance: Duration) -> Duration {
1374 let p = &self.pools[pool as usize];
1375 let p90 = p.adhoc_stats.p90().unwrap_or(p.cfg.default_duration);
1376 tolerance.max(p90).min(p.cfg.max_hold)
1377 }
1378
1379 #[allow(clippy::too_many_arguments)]
1381 fn insert(
1382 &mut self,
1383 now: Time,
1384 id: TaskId,
1385 seq: u64,
1386 arrival: Time,
1387 spec: TaskSpec<P>,
1388 pool: u16,
1389 attempt: u32,
1390 skip_if_live: Option<TaskId>,
1391 ) {
1392 self.totals.submitted += 1;
1393 if self.cfg.persist {
1394 self.actions
1395 .push_back(Action::Persist(Box::new(Mutation::Submitted {
1396 id,
1397 seq,
1398 arrival,
1399 spec: spec.clone(),
1400 })));
1401 }
1402 let kind = self.intern_kind(&spec.kind);
1403 let constraint = self.intern_constraint(&spec.labels);
1404 let tenant = match (&spec.tenant, self.pools[pool as usize].fair.is_some()) {
1405 (Some(t), true) => self.intern_tenant(t),
1406 _ => 0,
1407 };
1408 let hint_ns = spec.duration_hint.map(nanos).unwrap_or(0);
1409 let resources = if spec.resources.is_empty() {
1410 None
1411 } else {
1412 Some(
1413 spec.resources
1414 .iter()
1415 .map(|(n, a)| (ResourceId(self.resource_index[n]), *a))
1416 .collect::<Vec<_>>()
1417 .into_boxed_slice(),
1418 )
1419 };
1420 let fixed = spec.fixed_start.map(|f| {
1421 let (s, e) = self.reservation_interval(pool, &spec, &f);
1422 let p = &mut self.pools[pool as usize];
1423 p.reservations.insert(id, s, e);
1424 if attempt == 0 {
1425 p.unstarted.insert(id, s, e);
1426 }
1427 Box::new(FixedInfo {
1428 at: f.at,
1429 tolerance: f.tolerance,
1430 on_miss: f.on_miss,
1431 res_start: s,
1432 held: false,
1433 hold_pending: false,
1434 missed: false,
1435 skip_if_live,
1436 })
1437 });
1438 let await_timeout = match self.pools[pool as usize].cfg.unknown_dependency {
1440 UnknownDependency::Await { timeout } => timeout,
1441 UnknownDependency::Reject => None,
1442 };
1443 let mut parents = Vec::with_capacity(spec.deps.len());
1444 for d in spec.deps.iter() {
1445 let recovering = self.recovering_keys.is_some();
1446 let p = match &d.target {
1447 TaskRef::Id(pid) => match self.index.get(pid) {
1448 Some(_) => Parent::Live(*pid),
1449 None => match self.retained.get(pid) {
1450 Some(r) => Parent::Done(r.0),
1451 None if recovering => continue,
1454 None => Parent::Done(TaskStatus::Succeeded),
1456 },
1457 },
1458 TaskRef::Key(k) => {
1459 if let Some(&pid) = self.keys.get(k) {
1460 Parent::Live(pid)
1461 } else if let Some(r) = self
1462 .retained_keys
1463 .get(k)
1464 .and_then(|pid| self.retained.get(pid))
1465 {
1466 Parent::Done(r.0)
1467 } else if self
1468 .recovering_keys
1469 .as_ref()
1470 .is_some_and(|ks| !ks.contains(k))
1471 {
1472 continue;
1475 } else {
1476 Parent::Await(k.clone())
1477 }
1478 }
1479 };
1480 parents.push((p, d.cond));
1481 }
1482 let entry = Entry {
1483 id,
1484 seq,
1485 arrival,
1486 payload: spec.payload,
1487 kind,
1488 pool,
1489 class: spec.priority as u8,
1490 status: TaskStatus::Blocked,
1491 loc: Loc::None,
1492 key: spec.key.clone(),
1493 deadline: spec.deadline.unwrap_or(Time::MAX),
1494 lst: Time::MAX,
1495 hint_ns,
1496 timeout_ns: spec.timeout.map(nanos).unwrap_or(0),
1497 retry: spec.retry.map(Box::new),
1498 attempt,
1499 unmet: 0,
1500 awaiting: 0,
1501 has_awaits: false,
1502 pending: true,
1503 lease: None,
1504 retry_at: Time::ZERO,
1505 dependents: Vec::new(),
1506 resources,
1507 tags: if spec.tags.is_empty() {
1508 None
1509 } else {
1510 Some(spec.tags.into())
1511 },
1512 fixed,
1513 constraint,
1514 tenant,
1515 vtag: 0,
1516 };
1517 let key = self.slab_insert(entry);
1518 self.index.insert(id, key);
1519 if let Some(k) = &spec.key {
1520 self.keys.insert(k.clone(), id);
1521 }
1522 {
1523 let p = &mut self.pools[pool as usize];
1524 p.pending += 1;
1525 p.pending_class[spec.priority.index()] += 1;
1526 }
1527 self.total_pending += 1;
1528 self.pending_class[spec.priority.index()] += 1;
1529
1530 let mut unsatisfied: Option<TaskStatus> = None;
1532 let (mut unmet, mut awaiting) = (0u32, 0u32);
1533 let mut awaited: Option<Vec<Arc<str>>> = None;
1534 for (p, cond) in parents {
1535 match p {
1536 Parent::Live(pid) => {
1537 let pk = self.index[&pid];
1538 self.entry_mut(pk).dependents.push((id, cond));
1539 unmet += 1;
1540 }
1541 Parent::Done(st) => {
1542 if !edge_satisfied(cond, st) {
1543 unsatisfied = Some(st);
1544 }
1545 }
1546 Parent::Await(k) => {
1547 self.awaits.entry(k.clone()).or_default().push((id, cond));
1548 awaited.get_or_insert_with(Vec::new).push(k);
1549 unmet += 1;
1550 awaiting += 1;
1551 }
1552 }
1553 }
1554 if let Some(k) = &spec.key
1556 && let Some(waiting) = self.awaits.remove(k)
1557 {
1558 for (cid, _) in &waiting {
1559 if let Some(ck) = self.index.get(cid).copied() {
1560 let c = self.entry_mut(ck);
1561 c.awaiting = c.awaiting.saturating_sub(1);
1562 }
1563 }
1564 self.entry_mut(key).dependents.extend(waiting);
1565 }
1566 {
1567 let e = self.entry_mut(key);
1568 e.unmet = unmet;
1569 e.awaiting = awaiting;
1570 if let Some(keys) = awaited {
1571 e.has_awaits = true;
1572 self.awaited_by.insert(id, keys.into_boxed_slice());
1573 }
1574 }
1575 if awaiting > 0
1576 && let Some(t) = await_timeout
1577 {
1578 self.schedule(now + t, Timer::AwaitTimeout(id));
1579 }
1580 if let Some(f) = spec.fixed_start {
1581 let max_hold = self.pools[pool as usize].cfg.max_hold;
1584 self.schedule((f.at - max_hold).max(now), Timer::HoldAt(id));
1585 self.schedule(f.at, Timer::StartAt(id));
1586 self.schedule(
1588 f.at + f.tolerance + Duration::from_nanos(1),
1589 Timer::MissAt(id),
1590 );
1591 }
1592 if let Some(st) = unsatisfied {
1593 self.finish(
1594 key,
1595 TaskStatus::Skipped,
1596 Some(format!("dependency ended as {st}").into()),
1597 );
1598 return;
1599 }
1600 if attempt > 0 && unmet == 0 {
1601 let max = {
1603 let e = self.entry(key);
1604 e.retry
1605 .as_deref()
1606 .copied()
1607 .unwrap_or(self.pools[pool as usize].cfg.retry)
1608 .max_attempts
1609 };
1610 if attempt >= max {
1611 self.finish(
1612 key,
1613 TaskStatus::DeadLettered,
1614 Some("attempt lost in failover; retries exhausted".into()),
1615 );
1616 return;
1617 }
1618 if self.entry(key).fixed.is_none() {
1619 self.schedule_retry(now, key, None);
1620 return;
1621 }
1622 }
1624 self.emit(key, TaskStatus::Blocked, None);
1625 if unmet == 0 {
1626 self.make_eligible(key);
1627 }
1628 }
1629
1630 pub fn complete(&mut self, now: Time, lease: LeaseId, outcome: Outcome) -> bool {
1636 self.advance(now);
1637 let Some(l) = self.leases.remove(&lease) else {
1638 self.totals.stale_completions += 1;
1641 return false;
1642 };
1643 self.end_attempt(&l);
1644 let ran = now.saturating_sub(l.started);
1645 match outcome {
1646 Outcome::Success => {
1647 let (pool, kind, fixed) = {
1648 let e = self.entry(l.key);
1649 (e.pool, e.kind, e.fixed.is_some())
1650 };
1651 let s = ran.as_secs_f64();
1652 self.dur.entry((pool, kind)).or_default().observe(s);
1653 let p = &mut self.pools[pool as usize];
1654 p.stats.observe(s);
1655 if !fixed {
1656 p.adhoc_stats.observe(s);
1657 }
1658 let b = p.window.at(now);
1659 b.succeeded += 1;
1660 b.work_secs += s;
1661 b.run.record(ran);
1662 self.finish(l.key, TaskStatus::Succeeded, None);
1663 }
1664 Outcome::Cancelled => self.finish(l.key, TaskStatus::Cancelled, None),
1665 Outcome::Failure {
1666 retryable,
1667 retry_after,
1668 message,
1669 } => {
1670 self.fail_attempt(now, l.key, retryable, retry_after, message);
1671 }
1672 }
1673 self.pump();
1674 true
1675 }
1676
1677 pub fn heartbeat(&mut self, now: Time, lease: LeaseId) -> bool {
1679 self.advance(now);
1680 let known = match self.leases.get_mut(&lease) {
1681 Some(l) => {
1682 if let Some(ttl) = self.pools[l.pool as usize].cfg.lease_ttl {
1683 l.expires = now + ttl;
1684 }
1685 true
1686 }
1687 None => false,
1688 };
1689 self.pump();
1691 known
1692 }
1693
1694 fn end_attempt(&mut self, l: &Lease) {
1695 let now = self.now;
1696 let pool = &mut self.pools[l.pool as usize];
1697 pool.account(now);
1698 pool.busy -= 1;
1699 let g = &mut pool.groups[l.group as usize];
1700 g.busy = g.busy.saturating_sub(1);
1701 pool.dirty = true;
1702 self.dirty.push(l.pool);
1703 self.wake_labels(l.pool, l.group);
1704 let e = self.entry_mut(l.key);
1705 e.lease = None;
1706 let res = e.resources.clone();
1707 if let Some(res) = res {
1708 for (r, amount) in res.iter() {
1709 let rs = &mut self.resources[r.0 as usize];
1710 if let ResourceKind::Semaphore { .. } = rs.kind {
1711 rs.available += *amount as f64;
1712 self.wake_waiters(r.0);
1713 }
1714 }
1715 self.dirty_due_pools();
1716 }
1717 }
1718
1719 fn dirty_due_pools(&mut self) {
1720 for i in 0..self.pools.len() {
1721 if !self.pools[i].due.is_empty() {
1722 self.mark_dirty(i as u16);
1723 }
1724 }
1725 }
1726
1727 fn fail_attempt(
1728 &mut self,
1729 now: Time,
1730 key: Key,
1731 retryable: bool,
1732 retry_after: Option<Duration>,
1733 message: Arc<str>,
1734 ) {
1735 let (attempt, max) = {
1736 let e = self.entry(key);
1737 let policy = e
1738 .retry
1739 .as_deref()
1740 .copied()
1741 .unwrap_or(self.pools[e.pool as usize].cfg.retry);
1742 (e.attempt, policy.max_attempts)
1743 };
1744 let pool = self.entry(key).pool;
1745 self.pools[pool as usize].window.at(now).failed += 1;
1746 if retryable && attempt < max {
1747 self.schedule_retry(now, key, retry_after);
1748 let e = self.entry(key);
1749 let (id, pool, k, at) = (e.id, e.pool, e.key.clone(), e.attempt);
1750 self.push_status(id, pool, k, TaskStatus::Retrying, at, Some(message), true);
1751 } else if retryable {
1752 self.finish(key, TaskStatus::DeadLettered, Some(message));
1753 } else {
1754 self.finish(key, TaskStatus::Failed, Some(message));
1755 }
1756 }
1757
1758 fn schedule_retry(&mut self, now: Time, key: Key, retry_after: Option<Duration>) {
1759 self.totals.retries += 1;
1760 let (pool, attempt, policy) = {
1761 let e = self.entry(key);
1762 (
1763 e.pool,
1764 e.attempt,
1765 e.retry
1766 .as_deref()
1767 .copied()
1768 .unwrap_or(self.pools[e.pool as usize].cfg.retry),
1769 )
1770 };
1771 let mut delay = policy.backoff(attempt + 1);
1772 if policy.jitter {
1773 delay = delay.mul_f64(self.rng.next_f64());
1774 }
1775 if let Some(ra) = retry_after {
1776 delay = delay.max(ra);
1777 }
1778 let at = now + delay;
1779 let (id, class) = {
1780 let e = self.entry_mut(key);
1781 e.status = TaskStatus::Retrying;
1782 e.retry_at = at;
1783 (e.id, e.class as usize)
1784 };
1785 self.pools[pool as usize].retrying[class] += 1;
1786 self.pools[pool as usize].window.at(now).retried += 1;
1787 self.add_pending(key, pool, class);
1788 self.schedule(at, Timer::Retry(id));
1789 }
1790
1791 fn add_pending(&mut self, key: Key, pool: u16, class: usize) {
1792 let e = self.entry_mut(key);
1793 if !e.pending {
1794 e.pending = true;
1795 let p = &mut self.pools[pool as usize];
1796 p.pending += 1;
1797 p.pending_class[class] += 1;
1798 self.total_pending += 1;
1799 self.pending_class[class] += 1;
1800 }
1801 }
1802
1803 fn remove_pending(&mut self, key: Key) {
1804 let e = self.entry_mut(key);
1805 if e.pending {
1806 e.pending = false;
1807 let (pool, class) = (e.pool as usize, e.class as usize);
1808 let p = &mut self.pools[pool];
1809 p.pending -= 1;
1810 p.pending_class[class] -= 1;
1811 self.total_pending -= 1;
1812 self.pending_class[class] -= 1;
1813 if self.space_waiters {
1814 self.space_waiters = false;
1815 self.actions.push_back(Action::SpaceAvailable);
1816 }
1817 }
1818 }
1819
1820 pub fn cancel(&mut self, now: Time, target: &TaskRef) -> bool {
1822 self.advance(now);
1823 let Some(key) = self.lookup(target) else {
1824 return false;
1825 };
1826 if let Some(lease) = self.entry(key).lease
1827 && let Some(l) = self.leases.remove(&lease)
1828 {
1829 self.end_attempt(&l);
1830 self.actions.push_back(Action::Abort {
1831 lease,
1832 id: l.id,
1833 reason: AbortReason::Cancelled,
1834 });
1835 }
1836 self.finish(key, TaskStatus::Cancelled, Some("cancelled".into()));
1837 self.pump();
1838 true
1839 }
1840
1841 pub fn set_capacity(&mut self, now: Time, pool: PoolId, capacity: u32) {
1843 self.set_group_capacity(now, pool, &[], capacity);
1844 }
1845
1846 pub fn set_group_capacity(
1851 &mut self,
1852 now: Time,
1853 pool: PoolId,
1854 labels: &[(Arc<str>, Arc<str>)],
1855 capacity: u32,
1856 ) {
1857 self.advance(now);
1858 let labels = normalize(labels);
1859 let p = &mut self.pools[pool.0 as usize];
1860 p.account(now);
1861 let g = match p.groups.iter().position(|g| g.labels == labels) {
1862 Some(g) => g,
1863 None => {
1864 p.groups.push(Group {
1865 labels,
1866 capacity: 0,
1867 busy: 0,
1868 });
1869 p.groups.len() - 1
1870 }
1871 };
1872 p.groups[g].capacity = capacity;
1873 p.capacity = p.groups.iter().map(|g| g.capacity).sum();
1874 p.dirty = true;
1875 self.dirty.push(pool.0);
1876 self.wake_labels(pool.0, g as u16);
1877 self.pump();
1878 }
1879
1880 pub fn status(&self, target: &TaskRef) -> Option<TaskStatus> {
1882 if let Some(k) = self.lookup(target) {
1883 return Some(self.entry(k).status);
1884 }
1885 let id = match target {
1886 TaskRef::Id(id) => *id,
1887 TaskRef::Key(k) => *self.retained_keys.get(k)?,
1888 };
1889 self.retained.get(&id).map(|r| r.0)
1890 }
1891
1892 pub fn resolve(&self, target: &TaskRef) -> Option<TaskId> {
1894 match target {
1895 TaskRef::Id(id) => {
1896 (self.index.contains_key(id) || self.retained.contains_key(id)).then_some(*id)
1897 }
1898 TaskRef::Key(k) => self
1899 .keys
1900 .get(k)
1901 .or_else(|| self.retained_keys.get(k))
1902 .copied(),
1903 }
1904 }
1905
1906 pub fn live_tasks(&self) -> usize {
1908 self.index.len()
1909 }
1910
1911 pub fn list_tasks<S: AsRef<str>>(
1915 &self,
1916 pools: &[S],
1917 statuses: &[TaskStatus],
1918 after: Option<TaskId>,
1919 limit: usize,
1920 ) -> Vec<TaskSummary> {
1921 let pool = if pools.is_empty() {
1922 None
1923 } else {
1924 let ids: Vec<u16> = pools
1925 .iter()
1926 .filter_map(|p| self.pool_index.get(p.as_ref()).copied())
1927 .collect();
1928 if ids.is_empty() {
1929 return Vec::new();
1930 }
1931 Some(ids)
1932 };
1933 let mut ids: Vec<(TaskId, Key)> = self
1934 .index
1935 .iter()
1936 .filter(|(id, _)| after.is_none_or(|a| **id > a))
1937 .map(|(id, k)| (*id, *k))
1938 .filter(|(_, k)| {
1939 let e = self.entry(*k);
1940 pool.as_ref().is_none_or(|p| p.contains(&e.pool))
1941 && (statuses.is_empty() || statuses.contains(&e.status))
1942 })
1943 .collect();
1944 ids.sort_unstable_by_key(|(id, _)| *id);
1945 ids.truncate(limit);
1946 ids.into_iter()
1947 .map(|(id, k)| {
1948 let e = self.entry(k);
1949 TaskSummary {
1950 id,
1951 key: e.key.clone(),
1952 pool: self.pools[e.pool as usize].cfg.name.clone(),
1953 kind: self.kinds[e.kind as usize].clone(),
1954 priority: Priority::from_index(e.class as usize).unwrap_or_default(),
1955 status: e.status,
1956 attempt: e.attempt,
1957 tenant: (e.tenant != 0).then(|| self.tenants[e.tenant as usize].clone()),
1958 }
1959 })
1960 .collect()
1961 }
1962
1963 #[doc(hidden)]
1965 pub fn awaited_keys(&self) -> usize {
1966 self.awaits.len()
1967 }
1968
1969 fn lookup(&self, target: &TaskRef) -> Option<Key> {
1970 let id = match target {
1971 TaskRef::Id(id) => *id,
1972 TaskRef::Key(k) => *self.keys.get(k)?,
1973 };
1974 self.index.get(&id).copied()
1975 }
1976
1977 pub fn add_recurring(&mut self, now: Time, spec: RecurringSpec<P>) -> Result<(), String> {
1983 self.advance(now);
1984 self.add_recurring_inner(now, spec, None, None)?;
1985 self.pump();
1986 Ok(())
1987 }
1988
1989 fn add_recurring_inner(
1991 &mut self,
1992 now: Time,
1993 spec: RecurringSpec<P>,
1994 next: Option<Time>,
1995 chain_last: Option<TaskId>,
1996 ) -> Result<(), String> {
1997 let cron = croner::parser::CronParser::builder()
1998 .seconds(croner::parser::Seconds::Optional)
1999 .build()
2000 .parse(&spec.cron)
2001 .map_err(|e| format!("invalid cron '{}': {e}", spec.cron))?;
2002 let tz: chrono_tz::Tz = match &spec.tz {
2003 Some(t) => t.parse().map_err(|_| format!("unknown time zone '{t}'"))?,
2004 None => chrono_tz::UTC,
2005 };
2006 self.check_config(&spec.template)
2007 .map_err(|e| format!("invalid template: {e}"))?;
2008 if self.cfg.persist {
2009 self.actions
2010 .push_back(Action::Persist(Box::new(Mutation::RecurringUpsert {
2011 spec: spec.clone(),
2012 })));
2013 }
2014 let name = spec.name.clone();
2015 let (last_task, materialized) = self
2018 .recurring
2019 .get(&name)
2020 .map(|old| (old.last_task, old.materialized))
2021 .or_else(|| self.recurring_history.remove(&name))
2022 .unwrap_or((None, None));
2023 let mut r = Recurring {
2024 spec,
2025 cron,
2026 tz,
2027 next: None,
2028 last_task,
2029 materialized,
2030 chain_last,
2031 };
2032 let mut next = match next {
2033 Some(n) => Some(n),
2034 None => self.next_occurrence(&r, now),
2035 };
2036 if let (Some(n), Some(m)) = (next, materialized)
2037 && n <= m
2038 {
2039 next = self.next_occurrence(&r, m);
2040 }
2041 r.next = next;
2042 self.recurring.insert(name.clone(), r);
2043 self.arm_recurring(now, &name);
2044 Ok(())
2045 }
2046
2047 fn is_occurrence_of(&self, id: TaskId, pool: u16, name: &str) -> bool {
2049 let Some(&k) = self.index.get(&id) else {
2050 return false;
2051 };
2052 let e = self.entry(k);
2053 e.pool == pool
2054 && e.tags.as_ref().is_some_and(|t| {
2055 t.iter()
2056 .any(|(k, v)| &**k == "cronoq.recurring" && &**v == name)
2057 })
2058 }
2059
2060 pub fn remove_recurring(&mut self, now: Time, name: &str) -> bool {
2062 self.advance(now);
2063 let removed = self.recurring.remove(name);
2064 if let Some(r) = &removed {
2065 self.recurring_history
2067 .insert(name.into(), (r.last_task, r.materialized));
2068 if self.cfg.persist {
2069 self.actions
2070 .push_back(Action::Persist(Box::new(Mutation::RecurringRemove {
2071 name: name.into(),
2072 })));
2073 }
2074 }
2075 self.pump();
2076 removed.is_some()
2077 }
2078
2079 pub fn recurring_names(&self) -> Vec<Arc<str>> {
2081 self.recurring.keys().cloned().collect()
2082 }
2083
2084 pub fn list_recurring(&self) -> Vec<RecurringInfo> {
2086 self.recurring
2087 .values()
2088 .map(|r| RecurringInfo {
2089 name: r.spec.name.clone(),
2090 cron: r.spec.cron.clone(),
2091 tz: r.spec.tz.clone(),
2092 pool: r.spec.template.pool.clone(),
2093 kind: r.spec.template.kind.clone(),
2094 overlap: r.spec.overlap,
2095 next: r.next,
2096 last_task: r.last_task,
2097 })
2098 .collect()
2099 }
2100
2101 fn next_occurrence(&self, r: &Recurring<P>, after: Time) -> Option<Time> {
2102 use chrono::TimeZone;
2103 let wall = self.cfg.epoch_unix_nanos as i128 + after.0 as i128;
2104 let secs = i64::try_from(wall.div_euclid(1_000_000_000)).ok()?;
2107 let dt = r.tz.timestamp_opt(secs, 0).single()?;
2108 let next = r.cron.find_next_occurrence(&dt, false).ok()?;
2109 let ns = next.timestamp_nanos_opt()? as i128 - self.cfg.epoch_unix_nanos as i128;
2110 (ns >= 0).then_some(Time(ns as u64))
2111 }
2112
2113 fn recurring_lead(&self, pool: u16, tolerance: Duration) -> Duration {
2114 (self.hold_lead(pool, tolerance) + tolerance).min(self.pools[pool as usize].cfg.max_hold)
2115 }
2116
2117 fn arm_recurring(&mut self, not_before: Time, name: &Arc<str>) {
2119 let Some(r) = self.recurring.get(name) else {
2120 return;
2121 };
2122 let Some(next) = r.next else { return };
2123 let pool = self.pool_index[&r.spec.template.pool];
2124 let lead = self.recurring_lead(pool, r.spec.tolerance);
2125 let at = (next - lead).max(not_before);
2126 self.schedule(at, Timer::Recurring(name.clone(), next));
2127 }
2128
2129 fn fire_recurring(&mut self, now: Time, name: Arc<str>, occurrence: Time) {
2130 let Some(r) = self.recurring.get(&name) else {
2131 return;
2132 };
2133 if r.next != Some(occurrence) {
2134 return; }
2136 let at = occurrence;
2137 let late = at + r.spec.tolerance < now;
2140 if late && r.spec.on_miss != MissPolicy::RunLate {
2141 self.totals.missed += 1;
2142 let next = self.next_occurrence(r, now);
2143 let r = self.recurring.get_mut(&name).unwrap();
2144 r.next = next;
2145 let last = r.chain_last;
2146 if self.cfg.persist {
2147 self.actions
2148 .push_back(Action::Persist(Box::new(Mutation::RecurringNext {
2149 name: name.clone(),
2150 next,
2151 last,
2152 })));
2153 }
2154 self.arm_recurring(now, &name);
2155 return;
2156 }
2157 let pool = self.pool_index[&r.spec.template.pool];
2158 let mut spec = r.spec.template.clone();
2159 spec.fixed_start = Some(FixedStart {
2160 at,
2161 tolerance: r.spec.tolerance,
2162 on_miss: r.spec.on_miss,
2163 });
2164 spec.key = r
2165 .spec
2166 .template
2167 .key
2168 .as_ref()
2169 .map(|k| occurrence_key(k, self.cfg.epoch_unix_nanos, at));
2170 spec.deadline = r.spec.deadline_after.map(|d| at + d);
2171 spec.deps.clear();
2172 let chain = match r.spec.overlap {
2173 OverlapPolicy::Queue => Some(EdgeCond::OnComplete),
2174 OverlapPolicy::QueueOnSuccess => Some(EdgeCond::OnSuccess),
2175 OverlapPolicy::Skip | OverlapPolicy::Allow => None,
2176 };
2177 if let (Some(cond), Some(prev)) = (chain, r.chain_last) {
2178 spec.deps.push(Dep {
2180 target: TaskRef::Id(prev),
2181 cond,
2182 });
2183 }
2184 let prev = match r.spec.overlap {
2185 OverlapPolicy::Skip => r.last_task,
2186 _ => None,
2187 };
2188 let (s, e) = self.reservation_interval(pool, &spec, spec.fixed_start.as_ref().unwrap());
2189 let fits = self.pools[pool as usize].reservations.max_overlap(s, e)
2190 < self.pools[pool as usize].cfg.reservation_limit();
2191 let mut task = None;
2192 let existing = self.existing_key(spec.key.as_deref());
2193 if let Some(id) = existing.filter(|id| self.is_occurrence_of(*id, pool, &name)) {
2194 task = Some(id);
2196 } else if fits {
2197 if existing.is_some() {
2198 spec.key = None;
2201 }
2202 let id = self.alloc_id();
2203 let seq = self.alloc_seq();
2204 spec.tags.push(("cronoq.recurring".into(), name.clone()));
2205 self.insert(now, id, seq, now, spec, pool, 0, prev);
2206 task = Some(id);
2207 } else {
2208 self.totals.recurring_conflicts += 1;
2209 }
2210 let from = if late { now } else { at };
2211 let next = self.next_occurrence(self.recurring.get(&name).unwrap(), from);
2212 let r = self.recurring.get_mut(&name).unwrap();
2213 if task.is_some() {
2214 r.last_task = task;
2215 r.chain_last = task;
2216 }
2217 r.materialized = Some(at);
2218 r.next = next;
2219 let last = r.chain_last;
2220 if self.cfg.persist {
2221 self.actions
2222 .push_back(Action::Persist(Box::new(Mutation::RecurringNext {
2223 name: name.clone(),
2224 next,
2225 last,
2226 })));
2227 }
2228 self.arm_recurring(at.max(now), &name);
2230 }
2231
2232 pub fn recover(&mut self, now: Time, rec: Recovered<P>) {
2238 self.advance(now);
2239 self.next_id = self.next_id.max(rec.next_id);
2240 for t in rec.terminal {
2241 self.retain(t.id, t.key, t.status, t.at);
2242 }
2243 let mut tasks = rec.tasks;
2244 tasks.sort_by_key(|t| t.id);
2245 self.recovering_keys = Some(tasks.iter().filter_map(|t| t.spec.key.clone()).collect());
2248 for t in tasks {
2249 self.next_id = self.next_id.max(t.id.0 + 1);
2250 self.next_seq = self.next_seq.max(t.seq + 1);
2251 let pool = self.pool_index.get(&t.spec.pool).copied();
2252 let missing =
2253 match pool {
2254 None => Some(format!("pool '{}' no longer exists", t.spec.pool)),
2255 Some(_) => t.spec.resources.iter().find_map(|(n, a)| {
2256 match self.resource_index.get(n) {
2257 None => Some(format!("resource '{n}' no longer exists")),
2258 Some(&r) if *a as f64 > self.resources[r as usize].capacity => Some(
2259 format!("resource '{n}' no longer has capacity for this task"),
2260 ),
2261 Some(_) => None,
2262 }
2263 }),
2264 };
2265 if let Some(reason) = missing {
2266 self.totals.failed += 1;
2268 self.retain(t.id, t.spec.key.clone(), TaskStatus::Failed, now);
2269 self.push_status(
2270 t.id,
2271 0,
2272 t.spec.key,
2273 TaskStatus::Failed,
2274 t.attempt,
2275 Some(reason.into()),
2276 true,
2277 );
2278 continue;
2279 }
2280 self.insert(
2281 now,
2282 t.id,
2283 t.seq,
2284 t.arrival,
2285 t.spec,
2286 pool.unwrap(),
2287 t.attempt,
2288 None,
2289 );
2290 }
2291 self.recovering_keys = None;
2292 let mut latest: StrMap<(TaskId, Time)> = StrMap::default();
2294 for k in self.index.values() {
2295 let e = self.entry(*k);
2296 let (Some(tags), Some(f)) = (&e.tags, &e.fixed) else {
2297 continue;
2298 };
2299 if let Some((_, name)) = tags.iter().find(|(t, _)| &**t == "cronoq.recurring") {
2300 let slot = latest.entry(name.clone()).or_insert((e.id, f.at));
2301 if f.at > slot.1 {
2302 *slot = (e.id, f.at);
2303 }
2304 }
2305 }
2306 for r in rec.recurring {
2307 let name = r.spec.name.clone();
2308 if let Some(&(id, at)) = latest.get(&name) {
2309 self.recurring_history.insert(name, (Some(id), Some(at)));
2310 }
2311 if self
2314 .add_recurring_inner(now, r.spec, r.next, r.last)
2315 .is_err()
2316 {
2317 self.totals.recurring_invalid += 1;
2318 }
2319 }
2320 self.advance(now);
2323 self.pump();
2324 }
2325
2326 fn schedule(&mut self, at: Time, t: Timer) {
2331 self.timer_seq += 1;
2332 self.timers.push(Reverse((at, self.timer_seq, t)));
2333 }
2334
2335 fn advance(&mut self, now: Time) {
2336 let now = now.max(self.now);
2337 self.now = now;
2338 if !self.started {
2339 self.started = true;
2342 for p in &mut self.pools {
2343 p.last_acct = now;
2344 p.last_eval = now;
2345 }
2346 for r in &mut self.resources {
2347 r.last_refill = now;
2348 }
2349 }
2350 while let Some(Reverse((t, _, _))) = self.timers.peek() {
2351 if *t > now {
2352 break;
2353 }
2354 let Reverse((t, _, timer)) = self.timers.pop().unwrap();
2355 self.fire(t.max(Time::ZERO), now, timer);
2356 }
2357 self.evict_retained(now);
2358 }
2359
2360 fn fire(&mut self, _at: Time, now: Time, timer: Timer) {
2361 match timer {
2362 Timer::Retry(id) => {
2363 if let Some(k) = self.index.get(&id).copied() {
2364 let e = self.entry(k);
2365 if e.status == TaskStatus::Retrying && e.retry_at <= now {
2366 let (pool, class) = (e.pool as usize, e.class as usize);
2367 self.pools[pool].retrying[class] -= 1;
2368 if self.entry(k).unmet == 0 {
2369 self.make_eligible(k);
2370 } else {
2371 self.entry_mut(k).status = TaskStatus::Blocked;
2372 }
2373 }
2374 }
2375 }
2376 Timer::HoldAt(id) => {
2377 if let Some(k) = self.index.get(&id).copied() {
2378 let e = self.entry(k);
2379 let ok = matches!(
2380 e.status,
2381 TaskStatus::Blocked | TaskStatus::Scheduled | TaskStatus::Ready
2382 ) && e.fixed.as_ref().is_some_and(|f| !f.held && !f.hold_pending);
2383 if ok {
2384 let pool = e.pool;
2385 let f = e.fixed.as_ref().unwrap();
2386 let (at, tolerance, seq) = (f.at, f.tolerance, e.seq);
2387 let target = at - self.hold_lead(pool, tolerance);
2390 if now < target {
2391 let gap = target.saturating_sub(now);
2392 let next = if gap > Duration::from_secs(1) {
2393 now + gap / 2
2394 } else {
2395 target
2396 };
2397 self.schedule(next, Timer::HoldAt(id));
2398 return;
2399 }
2400 self.entry_mut(k).fixed.as_mut().unwrap().hold_pending = true;
2401 self.pools[pool as usize].pending_holds.insert((at, seq), k);
2402 self.mark_dirty(pool);
2403 }
2404 }
2405 }
2406 Timer::StartAt(id) => {
2407 if let Some(k) = self.index.get(&id).copied()
2408 && self.entry(k).status == TaskStatus::Scheduled
2409 {
2410 self.make_eligible(k);
2411 }
2412 }
2413 Timer::MissAt(id) => {
2414 if let Some(k) = self.index.get(&id).copied() {
2415 self.on_miss(k);
2416 }
2417 }
2418 Timer::AwaitTimeout(id) => {
2419 if let Some(k) = self.index.get(&id).copied()
2420 && self.entry(k).awaiting > 0
2421 && self.entry(k).status == TaskStatus::Blocked
2422 {
2423 self.finish(
2424 k,
2425 TaskStatus::Skipped,
2426 Some("timed out awaiting unknown dependency".into()),
2427 );
2428 }
2429 }
2430 Timer::AttemptTimeout(lease) => {
2431 if let Some(l) = self.leases.remove(&lease) {
2432 self.totals.timeouts += 1;
2433 self.end_attempt(&l);
2434 self.actions.push_back(Action::Abort {
2435 lease,
2436 id: l.id,
2437 reason: AbortReason::Timeout,
2438 });
2439 self.fail_attempt(now, l.key, true, None, "attempt timed out".into());
2440 }
2441 }
2442 Timer::LeaseExpiry(lease) => {
2443 let Some(l) = self.leases.get(&lease) else {
2444 return;
2445 };
2446 if l.expires > now {
2447 let at = l.expires;
2448 self.schedule(at, Timer::LeaseExpiry(lease));
2449 return;
2450 }
2451 let l = self.leases.remove(&lease).unwrap();
2452 self.totals.lease_expiries += 1;
2453 self.end_attempt(&l);
2454 self.actions.push_back(Action::Abort {
2455 lease,
2456 id: l.id,
2457 reason: AbortReason::LeaseExpired,
2458 });
2459 self.fail_attempt(now, l.key, true, None, "lease expired".into());
2460 }
2461 Timer::Refill(r) => {
2462 let rs = &mut self.resources[r as usize];
2463 rs.refill_at = None;
2464 rs.refill(now);
2465 self.wake_waiters(r);
2466 self.dirty_due_pools();
2468 }
2469 Timer::Autoscale(pool) => self.autoscale(now, pool),
2470 Timer::Recurring(name, at) => self.fire_recurring(now, name, at),
2471 }
2472 }
2473
2474 fn on_miss(&mut self, k: Key) {
2475 let (status, pool, policy, attempt, late) = {
2476 let e = self.entry(k);
2477 let Some(f) = e.fixed.as_ref() else { return };
2478 (
2479 e.status,
2480 e.pool,
2481 f.on_miss,
2482 e.attempt,
2483 self.now > f.at + f.tolerance,
2484 )
2485 };
2486 if attempt > 0
2489 || !late
2490 || matches!(status, TaskStatus::Running)
2491 || status.is_terminal()
2492 || status == TaskStatus::Retrying
2493 {
2494 return;
2495 }
2496 self.entry_mut(k).fixed.as_mut().unwrap().missed = true;
2497 match policy {
2498 MissPolicy::RunLate => {
2499 if status == TaskStatus::Blocked {
2500 self.release_hold(k);
2501 }
2502 self.mark_dirty(pool);
2503 }
2504 MissPolicy::Skip => {
2505 self.totals.missed += 1;
2506 self.finish(k, TaskStatus::Missed, Some("fixed start missed".into()));
2507 }
2508 MissPolicy::Fail => {
2509 self.finish(k, TaskStatus::Failed, Some("fixed start missed".into()))
2510 }
2511 }
2512 }
2513
2514 fn release_hold(&mut self, k: Key) {
2515 let pool = self.entry(k).pool as usize;
2516 let (seq, f) = {
2517 let e = self.entry_mut(k);
2518 (e.seq, e.fixed.as_mut().unwrap())
2519 };
2520 let at = f.at;
2521 let was_held = core::mem::replace(&mut f.held, false);
2522 let was_pending = core::mem::replace(&mut f.hold_pending, false);
2523 let p = &mut self.pools[pool];
2524 if was_held {
2525 p.held -= 1;
2526 p.held_tasks.remove(&(at, seq));
2527 p.dirty = true;
2528 self.dirty.push(pool as u16);
2529 }
2530 if was_pending {
2531 p.pending_holds.remove(&(at, seq));
2532 }
2533 }
2534
2535 fn make_eligible(&mut self, k: Key) {
2541 let now = self.now;
2542 let (pool, class, seq, fixed_at, has_deadline) = {
2543 let e = self.entry(k);
2544 (
2545 e.pool,
2546 e.class as usize,
2547 e.seq,
2548 e.fixed.as_ref().map(|f| f.at),
2549 e.deadline != Time::MAX,
2550 )
2551 };
2552 match fixed_at {
2553 Some(at) if now < at => {
2554 self.set_status(k, TaskStatus::Scheduled);
2555 }
2556 Some(at) => {
2557 self.entry_mut(k).loc = Loc::Due;
2558 self.pools[pool as usize].due.insert((at, seq), k);
2559 self.set_status(k, TaskStatus::Ready);
2560 self.mark_dirty(pool);
2561 }
2562 None => {
2563 if has_deadline {
2564 let lst = {
2565 let e = self.entry(k);
2566 e.deadline - self.estimate_raw(e.pool, Some(e.kind), e.hint_ns)
2567 };
2568 let e = self.entry_mut(k);
2569 e.lst = lst;
2570 e.loc = Loc::Edf;
2571 self.pools[pool as usize].edf[class].insert((lst, seq), k);
2572 } else if self.pools[pool as usize].fair.is_some() {
2573 let tenant = self.entry(k).tenant;
2574 let tag = self.fair_tag(pool, class, tenant);
2575 let e = self.entry_mut(k);
2576 e.vtag = tag;
2577 e.loc = Loc::Fair;
2578 let f = self.pools[pool as usize].fair.as_mut().unwrap();
2579 f.queues[class].insert((tag, seq), k);
2580 } else {
2581 self.entry_mut(k).loc = Loc::Lane;
2582 self.pools[pool as usize].lanes[class].insert(seq, k);
2583 }
2584 self.pools[pool as usize].arrivals += 1;
2585 self.set_status(k, TaskStatus::Ready);
2586 self.mark_dirty(pool);
2587 }
2588 }
2589 }
2590
2591 fn unqueue(&mut self, k: Key) {
2592 let (loc, pool, class, seq, lst, at, vtag) = {
2593 let e = self.entry(k);
2594 (
2595 e.loc,
2596 e.pool as usize,
2597 e.class as usize,
2598 e.seq,
2599 e.lst,
2600 e.fixed.as_ref().map(|f| f.at),
2601 e.vtag,
2602 )
2603 };
2604 let p = &mut self.pools[pool];
2605 match loc {
2606 Loc::None | Loc::Parked(_) | Loc::LabelWait => {}
2608 Loc::Lane => {
2609 p.lanes[class].remove(seq);
2610 }
2611 Loc::Edf => {
2612 p.edf[class].remove(&(lst, seq));
2613 }
2614 Loc::Due => {
2615 p.due.remove(&(at.unwrap(), seq));
2616 }
2617 Loc::Fair => {
2618 if let Some(f) = &mut p.fair {
2619 f.queues[class].remove(&(vtag, seq));
2620 }
2621 }
2622 }
2623 self.entry_mut(k).loc = Loc::None;
2624 }
2625
2626 fn finish(&mut self, k: Key, status: TaskStatus, message: Option<Arc<str>>) {
2628 debug_assert!(status.is_terminal());
2629 let mut work = vec![(k, status, message)];
2630 while let Some((k, status, message)) = work.pop() {
2631 if self.slot_entry(k).is_none() {
2632 continue;
2633 }
2634 let parked_on = match self.entry(k).loc {
2635 Loc::Parked(r) => Some(r),
2636 _ => None,
2637 };
2638 if self.entry(k).status == TaskStatus::Retrying {
2639 let e = self.entry(k);
2640 let (pool, class) = (e.pool as usize, e.class as usize);
2641 self.pools[pool].retrying[class] -= 1;
2642 }
2643 self.unqueue(k);
2644 self.remove_pending(k);
2645 if self.entry(k).fixed.is_some() {
2646 self.release_hold(k);
2647 let e = self.entry(k);
2648 let (pool, id, rs) = (e.pool as usize, e.id, e.fixed.as_ref().unwrap().res_start);
2649 self.pools[pool].reservations.remove(id, rs);
2650 self.pools[pool].unstarted.remove(id, rs);
2651 }
2652 match status {
2653 TaskStatus::Succeeded => self.totals.succeeded += 1,
2654 TaskStatus::Failed => self.totals.failed += 1,
2655 TaskStatus::DeadLettered => self.totals.dead_lettered += 1,
2656 TaskStatus::Cancelled => self.totals.cancelled += 1,
2657 TaskStatus::Skipped => self.totals.skipped += 1,
2658 TaskStatus::Shed => self.totals.shed += 1,
2659 _ => {}
2660 }
2661 let e = self.slab_remove(k);
2662 if e.has_awaits
2663 && let Some(keys) = self.awaited_by.remove(&e.id)
2664 {
2665 for key in keys.iter() {
2666 if let Some(list) = self.awaits.get_mut(key) {
2667 list.retain(|(cid, _)| *cid != e.id);
2668 if list.is_empty() {
2669 self.awaits.remove(key);
2670 }
2671 }
2672 }
2673 }
2674 if let Some(r) = parked_on {
2675 self.wake_waiters(r.0);
2677 }
2678 self.index.remove(&e.id);
2679 if let Some(key) = &e.key
2680 && self.keys.get(key) == Some(&e.id)
2681 {
2682 self.keys.remove(key);
2683 }
2684 self.retain(e.id, e.key.clone(), status, self.now);
2685 self.push_status(
2686 e.id,
2687 e.pool,
2688 e.key.clone(),
2689 status,
2690 e.attempt,
2691 message,
2692 true,
2693 );
2694 for (cid, cond) in e.dependents {
2695 let Some(ck) = self.index.get(&cid).copied() else {
2696 continue;
2697 };
2698 if edge_satisfied(cond, status) {
2699 let c = self.entry_mut(ck);
2700 c.unmet = c.unmet.saturating_sub(1);
2701 if c.unmet == 0 && c.status == TaskStatus::Blocked {
2702 self.make_eligible(ck);
2703 }
2704 } else {
2705 work.push((
2706 ck,
2707 TaskStatus::Skipped,
2708 Some(format!("dependency {} ended as {status}", e.id).into()),
2709 ));
2710 }
2711 }
2712 }
2713 }
2714
2715 fn retain(&mut self, id: TaskId, key: Option<Arc<str>>, status: TaskStatus, at: Time) {
2716 self.retained.insert(id, (status, at));
2717 if let Some(k) = &key {
2718 self.retained_keys.insert(k.clone(), id);
2719 }
2720 self.retention_q.push_back((at, id, key));
2721 }
2722
2723 fn evict_retained(&mut self, now: Time) {
2724 while let Some((t, _, _)) = self.retention_q.front() {
2725 if now.saturating_sub(*t) <= self.cfg.retention
2726 && self.retention_q.len() <= self.cfg.retention_max
2727 {
2728 break;
2729 }
2730 let (_, id, key) = self.retention_q.pop_front().unwrap();
2731 self.retained.remove(&id);
2732 if let Some(k) = key
2733 && self.retained_keys.get(&k) == Some(&id)
2734 {
2735 self.retained_keys.remove(&k);
2736 }
2737 }
2738 }
2739
2740 fn set_status(&mut self, k: Key, status: TaskStatus) {
2741 let emit = self.emit_transitions;
2742 let e = self.entry_mut(k);
2743 e.status = status;
2744 if emit {
2745 let (id, pool, key, at) = (e.id, e.pool, e.key.clone(), e.attempt);
2746 self.push_status(id, pool, key, status, at, None, false);
2747 }
2748 }
2749
2750 fn emit(&mut self, k: Key, status: TaskStatus, message: Option<Arc<str>>) {
2751 let emit = self.emit_transitions;
2752 let e = self.entry_mut(k);
2753 e.status = status;
2754 let (id, pool, key, at) = (e.id, e.pool, e.key.clone(), e.attempt);
2755 if emit {
2756 self.push_status(id, pool, key, status, at, message, false);
2757 }
2758 }
2759
2760 #[allow(clippy::too_many_arguments)]
2761 fn push_status(
2762 &mut self,
2763 id: TaskId,
2764 pool: u16,
2765 key: Option<Arc<str>>,
2766 status: TaskStatus,
2767 attempt: u32,
2768 message: Option<Arc<str>>,
2769 important: bool,
2770 ) {
2771 let persist = self.cfg.persist
2772 && (status.is_terminal()
2773 || matches!(status, TaskStatus::Running | TaskStatus::Retrying));
2774 if persist {
2775 self.actions
2776 .push_back(Action::Persist(Box::new(Mutation::Status {
2777 id,
2778 status,
2779 attempt,
2780 at: self.now,
2781 message: message.clone(),
2782 })));
2783 }
2784 if important || self.emit_transitions {
2785 self.actions.push_back(Action::Status(StatusEvent {
2786 id,
2787 pool: PoolId(pool),
2788 key,
2789 status,
2790 attempt,
2791 at: self.now,
2792 message,
2793 }));
2794 }
2795 }
2796
2797 fn mark_dirty(&mut self, pool: u16) {
2802 let p = &mut self.pools[pool as usize];
2803 if !p.dirty {
2804 p.dirty = true;
2805 self.dirty.push(pool);
2806 }
2807 }
2808
2809 fn pump(&mut self) {
2810 while let Some(pool) = self.dirty.pop() {
2811 if self.pools[pool as usize].dirty {
2812 self.pools[pool as usize].dirty = false;
2813 self.pump_pool(pool);
2814 }
2815 }
2816 }
2817
2818 fn pump_pool(&mut self, pool: u16) {
2819 let now = self.now;
2820 loop {
2822 let p = &mut self.pools[pool as usize];
2823 if p.free() == 0 {
2824 break;
2825 }
2826 let Some((hk, k)) = p.pending_holds.pop_first() else {
2827 break;
2828 };
2829 p.held += 1;
2830 p.held_tasks.insert(hk, k);
2831 let f = self.entry_mut(k).fixed.as_mut().unwrap();
2832 f.hold_pending = false;
2833 f.held = true;
2834 }
2835 if self.pools[pool as usize].due.is_empty() {
2836 self.pump_regular(now, pool);
2837 return;
2838 }
2839 if !self.pools[pool as usize].held_tasks.is_empty() {
2841 let held: Vec<Key> = self.pools[pool as usize]
2842 .held_tasks
2843 .values()
2844 .copied()
2845 .filter(|k| self.entry(*k).loc == Loc::Due)
2846 .collect();
2847 for k in held {
2848 self.start_due(now, pool, k);
2849 }
2850 }
2851 let mut cursor: Option<(Time, u64)> = None;
2854 while self.pools[pool as usize].free() > 0 {
2855 let p = &self.pools[pool as usize];
2856 let next = match cursor {
2857 None => p.due.iter().next(),
2858 Some(c) => p
2859 .due
2860 .range((core::ops::Bound::Excluded(c), core::ops::Bound::Unbounded))
2861 .next(),
2862 };
2863 let Some((&dk, &k)) = next else {
2864 break;
2865 };
2866 cursor = Some(dk);
2867 self.start_due(now, pool, k);
2868 }
2869 self.pump_regular(now, pool);
2870 }
2871
2872 fn pump_regular(&mut self, now: Time, pool: u16) {
2874 while self.pools[pool as usize].free() > 0 {
2875 let Some(k) = self.best_candidate(pool, now) else {
2876 break;
2877 };
2878 self.unqueue(k);
2879 let Some(group) = self.select_group(pool, self.entry(k).constraint) else {
2882 self.park_label(k);
2883 continue;
2884 };
2885 if !self.try_acquire(k) {
2886 self.park(k);
2887 continue;
2888 }
2889 self.dispatch(now, pool, k, group);
2890 }
2891 }
2892
2893 fn start_due(&mut self, now: Time, pool: u16, k: Key) {
2895 let (held, skip_if, at, seq) = {
2896 let e = self.entry(k);
2897 let f = e.fixed.as_ref().unwrap();
2898 (f.held, f.skip_if_live, f.at, e.seq)
2899 };
2900 if let Some(prev) = skip_if
2901 && self.index.contains_key(&prev)
2902 {
2903 let e = self.entry(k);
2906 let id = e.id;
2907 let name = e
2908 .tags
2909 .as_ref()
2910 .and_then(|t| t.iter().find(|(k, _)| &**k == "cronoq.recurring"))
2911 .map(|(_, v)| v.clone());
2912 if let Some(r) = name.and_then(|n| self.recurring.get_mut(&n))
2913 && r.last_task == Some(id)
2914 {
2915 r.last_task = Some(prev);
2916 }
2917 self.finish(
2918 k,
2919 TaskStatus::Skipped,
2920 Some("previous occurrence still running".into()),
2921 );
2922 return;
2923 }
2924 if !held && self.pools[pool as usize].free() == 0 {
2925 return;
2926 }
2927 let Some(group) = self.select_group(pool, self.entry(k).constraint) else {
2930 return;
2931 };
2932 if !self.try_acquire(k) {
2933 self.schedule_refills(k);
2934 return;
2935 }
2936 self.pools[pool as usize].due.remove(&(at, seq));
2937 self.entry_mut(k).loc = Loc::None;
2938 self.release_hold(k);
2940 self.dispatch(now, pool, k, group);
2941 }
2942
2943 fn score(&self, e: &Entry<P>, now: Time) -> f64 {
2944 let pc = &self.cfg.priority;
2945 let c = e.class as usize;
2946 let wait = now.diff_secs(e.arrival).max(0.0);
2947 let mut s = pc.base[c] + (pc.aging_rate[c] * wait).min(pc.age_cap[c]);
2948 if e.deadline != Time::MAX {
2949 let u = &self.cfg.urgency;
2950 let d = e.deadline.diff_secs(e.lst).max(0.0);
2951 let h = d.max(secs(u.window)).max(1e-9);
2952 let slack = e.lst.diff_secs(now);
2953 s += if slack >= h {
2954 0.0
2955 } else if slack >= 0.0 {
2956 u.max * (1.0 - slack / h)
2957 } else {
2958 u.max + (u.overdue_rate * -slack).min(u.overdue_cap)
2959 };
2960 }
2961 s
2962 }
2963
2964 fn best_candidate(&self, pool: u16, now: Time) -> Option<Key> {
2965 let p = &self.pools[pool as usize];
2966 let mut best: Option<(f64, u8, u64, Key)> = None;
2967 for c in 0..Priority::COUNT {
2968 let heads = [
2969 p.lanes[c].first(),
2970 p.edf[c].first_key_value().map(|(_, k)| *k),
2971 p.fair
2972 .as_ref()
2973 .and_then(|f| f.queues[c].first_key_value().map(|(_, k)| *k)),
2974 ];
2975 for k in heads.into_iter().flatten() {
2976 let e = self.entry(k);
2977 let s = self.score(e, now);
2978 let better = match best {
2979 None => true,
2980 Some((bs, bc, bseq, _)) => s > bs || (s == bs && (e.class, e.seq) < (bc, bseq)),
2981 };
2982 if better {
2983 best = Some((s, e.class, e.seq, k));
2984 }
2985 }
2986 }
2987 best.map(|b| b.3)
2988 }
2989
2990 fn try_acquire(&mut self, k: Key) -> bool {
2991 let Some(res) = self.entry(k).resources.clone() else {
2992 return true;
2993 };
2994 let now = self.now;
2995 for (r, amount) in res.iter() {
2996 let rs = &mut self.resources[r.0 as usize];
2997 rs.refill(now);
2998 if rs.available < *amount as f64 {
2999 return false;
3000 }
3001 }
3002 for (r, amount) in res.iter() {
3003 self.resources[r.0 as usize].available -= *amount as f64;
3004 }
3005 true
3006 }
3007
3008 fn park(&mut self, k: Key) {
3009 let res = self.entry(k).resources.clone().unwrap();
3010 let now = self.now;
3011 let (r, amount) = *res
3012 .iter()
3013 .find(|(r, a)| {
3014 let rs = &mut self.resources[r.0 as usize];
3015 rs.refill(now);
3016 rs.available < *a as f64
3017 })
3018 .unwrap_or(&res[0]);
3019 self.entry_mut(k).loc = Loc::Parked(r);
3020 self.resources[r.0 as usize].waiters.push_back(k);
3021 self.schedule_refill(r.0, amount);
3022 }
3023
3024 fn schedule_refills(&mut self, k: Key) {
3025 if let Some(res) = self.entry(k).resources.clone() {
3026 for (r, a) in res.iter() {
3027 self.schedule_refill(r.0, *a);
3028 }
3029 }
3030 }
3031
3032 fn schedule_refill(&mut self, r: u16, need: u32) {
3033 let now = self.now;
3034 let rs = &mut self.resources[r as usize];
3035 if let ResourceKind::TokenBucket { rate, .. } = rs.kind {
3036 if rate <= 0.0 {
3037 return;
3038 }
3039 let missing = (need as f64 - rs.available).max(0.0);
3040 let wait = Duration::try_from_secs_f64(missing / rate).unwrap_or(Duration::MAX);
3041 let at = now + wait.max(Duration::from_micros(1));
3042 if rs.refill_at.is_none_or(|t| at < t) {
3043 rs.refill_at = Some(at);
3044 self.schedule(at, Timer::Refill(r));
3045 }
3046 }
3047 }
3048
3049 fn wake_waiters(&mut self, r: u16) {
3051 let now = self.now;
3052 self.resources[r as usize].refill(now);
3053 let mut budget = self.resources[r as usize].available;
3054 while let Some(&k) = self.resources[r as usize].waiters.front() {
3055 let need = match self.slot_entry(k) {
3056 Some(e) if e.loc == Loc::Parked(ResourceId(r)) => e
3057 .resources
3058 .as_ref()
3059 .and_then(|v| v.iter().find(|(x, _)| x.0 == r))
3060 .map(|x| x.1)
3061 .unwrap_or(1),
3062 _ => {
3063 self.resources[r as usize].waiters.pop_front();
3064 continue;
3065 }
3066 };
3067 if (need as f64) > budget + 1e-9 {
3068 self.schedule_refill(r, need);
3069 break;
3070 }
3071 self.resources[r as usize].waiters.pop_front();
3072 budget -= need as f64;
3073 self.requeue_ready(k);
3074 }
3075 }
3076
3077 fn dispatch(&mut self, now: Time, pool: u16, k: Key, group: u16) {
3078 let lease = LeaseId(self.next_lease);
3079 self.next_lease += 1;
3080 self.remove_pending(k);
3081 let higher_waiting = {
3082 let p = &self.pools[pool as usize];
3083 let c = self.entry(k).class as usize;
3084 (0..c).any(|h| {
3085 !p.lanes[h].is_empty()
3086 || !p.edf[h].is_empty()
3087 || p.fair.as_ref().is_some_and(|f| !f.queues[h].is_empty())
3088 })
3089 };
3090 let (vtag, class) = {
3091 let e = self.entry(k);
3092 (e.vtag, e.class as usize)
3093 };
3094 if let Some(f) = &mut self.pools[pool as usize].fair {
3095 f.vtime[class] = f.vtime[class].max(vtag);
3096 }
3097 let (id, attempt, timeout_ns, first, arrival, fixed) = {
3098 let e = self.entry_mut(k);
3099 e.attempt += 1;
3100 e.status = TaskStatus::Running;
3101 e.lease = Some(lease);
3102 (
3103 e.id,
3104 e.attempt,
3105 e.timeout_ns,
3106 e.attempt == 1,
3107 e.arrival,
3108 e.fixed.as_ref().map(|f| (f.at, f.tolerance, f.res_start)),
3109 )
3110 };
3111 if let Some((_, _, res_start)) = fixed {
3112 self.pools[pool as usize].unstarted.remove(id, res_start);
3113 }
3114 let lease_ttl = {
3115 let p = &mut self.pools[pool as usize];
3116 p.account(now);
3117 p.busy += 1;
3118 p.groups[group as usize].busy += 1;
3119 let b = p.window.at(now);
3120 b.dispatched += 1;
3121 if higher_waiting {
3122 b.inversions += 1;
3123 }
3124 if first {
3125 b.wait.record(now.saturating_sub(arrival));
3126 }
3127 if let (Some((at, tol, _)), true) = (fixed, first) {
3128 b.fixed_total += 1;
3130 if now.diff_secs(at).abs() <= tol.as_secs_f64() {
3131 b.fixed_hits += 1;
3132 }
3133 }
3134 p.cfg.lease_ttl
3135 };
3136 self.totals.dispatched += 1;
3137 let expires = lease_ttl.map(|t| now + t).unwrap_or(Time::MAX);
3138 self.leases.insert(
3139 lease,
3140 Lease {
3141 key: k,
3142 id,
3143 pool,
3144 group,
3145 started: now,
3146 expires,
3147 },
3148 );
3149 if timeout_ns > 0 {
3150 self.schedule(
3151 now + Duration::from_nanos(timeout_ns),
3152 Timer::AttemptTimeout(lease),
3153 );
3154 }
3155 if expires != Time::MAX {
3156 self.schedule(expires, Timer::LeaseExpiry(lease));
3157 }
3158 let e = self.entry(k);
3159 let d = Dispatch {
3160 lease,
3161 id,
3162 pool: PoolId(pool),
3163 attempt,
3164 payload: e.payload.clone(),
3165 kind: self.kinds[e.kind as usize].clone(),
3166 key: e.key.clone(),
3167 timeout: (timeout_ns > 0).then(|| Duration::from_nanos(timeout_ns)),
3168 deadline: (e.deadline != Time::MAX).then_some(e.deadline),
3169 tags: e.tags.clone().unwrap_or_else(|| self.empty_tags.clone()),
3170 group: self.pools[pool as usize].groups[group as usize]
3171 .labels
3172 .clone(),
3173 };
3174 let key = e.key.clone();
3175 self.actions.push_back(Action::Dispatch(d));
3176 self.push_status(id, pool, key, TaskStatus::Running, attempt, None, false);
3177 }
3178
3179 fn autoscale(&mut self, now: Time, pool: u16) {
3184 let Some(cfg) = self.pools[pool as usize].cfg.autoscale.clone() else {
3185 return;
3186 };
3187 self.schedule(now + cfg.interval, Timer::Autoscale(pool));
3188 let p = &mut self.pools[pool as usize];
3189 p.account(now);
3190 let dt = if p.last_eval == Time::ZERO {
3191 0.0
3192 } else {
3193 now.diff_secs(p.last_eval)
3194 };
3195 p.last_eval = now;
3196 let mut backlog = [0u64; Priority::COUNT];
3197 for (c, b) in backlog.iter_mut().enumerate() {
3198 let fair = p.fair.as_ref().map_or(0, |f| f.queues[c].len());
3199 *b = (p.lanes[c].len() + p.edf[c].len() + fair + p.retrying[c]) as u64;
3200 }
3201 backlog[0] += p.due.len() as u64;
3202 let signals = Signals {
3203 arrivals: core::mem::take(&mut p.arrivals),
3204 dt,
3205 service_secs: p
3206 .stats
3207 .mean
3208 .get()
3209 .unwrap_or(p.cfg.default_duration.as_secs_f64()),
3210 backlog,
3211 busy: p.busy,
3212 capacity: p.capacity,
3213 reserved: p.unstarted.max_overlap(now, now + cfg.reservation_horizon),
3215 min: p.cfg.min_workers,
3216 max: p.cfg.max_workers,
3217 };
3218 if let Some(desired) = p.autoscale.as_mut().unwrap().evaluate(&cfg, &signals, now) {
3219 self.actions.push_back(Action::ScaleTo {
3220 pool: PoolId(pool),
3221 desired,
3222 });
3223 }
3224 }
3225
3226 pub fn snapshot(&mut self, now: Time) -> Snapshot {
3232 self.advance(now);
3233 self.pump();
3235 let mut pools = Vec::with_capacity(self.pools.len());
3236 for p in &mut self.pools {
3237 p.account(now);
3238 let w = p.window.total(now);
3239 let mut pending = [0usize; Priority::COUNT];
3240 pending.copy_from_slice(&p.pending_class);
3241 pools.push(PoolSnapshot {
3242 name: p.cfg.name.clone(),
3243 capacity: p.capacity,
3244 busy: p.busy,
3245 held: p.held,
3246 desired: p
3247 .autoscale
3248 .as_ref()
3249 .and_then(|a| a.target)
3250 .unwrap_or(p.capacity),
3251 pending_by_class: pending,
3252 ready: p.ready_len(),
3253 reservations: p.reservations.len(),
3254 window: p.window.span(),
3255 dispatched: w.dispatched,
3256 succeeded: w.succeeded,
3257 failed: w.failed,
3258 retried: w.retried,
3259 priority_adherence: if w.dispatched == 0 {
3260 1.0
3261 } else {
3262 1.0 - w.inversions as f64 / w.dispatched as f64
3263 },
3264 fixed_start_hit_rate: if w.fixed_total == 0 {
3265 1.0
3266 } else {
3267 w.fixed_hits as f64 / w.fixed_total as f64
3268 },
3269 utilization: if w.capacity_secs > 0.0 {
3270 (w.busy_secs / w.capacity_secs).min(1.0)
3271 } else {
3272 0.0
3273 },
3274 schedule_efficiency: if w.capacity_secs > 0.0 {
3275 (w.work_secs / w.capacity_secs).min(1.0)
3276 } else {
3277 0.0
3278 },
3279 wait_mean: w.wait.mean(),
3280 wait_p50: w.wait.quantile(0.5),
3281 wait_p90: w.wait.quantile(0.9),
3282 wait_p99: w.wait.quantile(0.99),
3283 run_p50: w.run.quantile(0.5),
3284 run_p99: w.run.quantile(0.99),
3285 worker_seconds: p.worker_secs,
3286 cost: p.cost,
3287 groups: p
3288 .groups
3289 .iter()
3290 .map(|g| GroupSnapshot {
3291 labels: g.labels.to_vec(),
3292 capacity: g.capacity,
3293 busy: g.busy,
3294 })
3295 .collect(),
3296 });
3297 }
3298 Snapshot {
3299 at: now,
3300 pools,
3301 live_tasks: self.index.len(),
3302 running: self.leases.len(),
3303 pending: self.total_pending,
3304 totals: self.totals.clone(),
3305 }
3306 }
3307
3308 fn slab_insert(&mut self, e: Entry<P>) -> Key {
3313 if let Some(idx) = self.free_slots.pop() {
3314 let s = &mut self.slots[idx as usize];
3315 s.entry = Some(e);
3316 Key {
3317 idx,
3318 generation: s.generation,
3319 }
3320 } else {
3321 let idx = self.slots.len() as u32;
3322 self.slots.push(Slot {
3323 generation: 0,
3324 entry: Some(e),
3325 });
3326 Key { idx, generation: 0 }
3327 }
3328 }
3329
3330 fn slab_remove(&mut self, k: Key) -> Entry<P> {
3331 let s = &mut self.slots[k.idx as usize];
3332 debug_assert_eq!(s.generation, k.generation);
3333 let e = s.entry.take().expect("live entry");
3334 s.generation = s.generation.wrapping_add(1);
3335 self.free_slots.push(k.idx);
3336 e
3337 }
3338
3339 #[inline]
3340 fn slot_entry(&self, k: Key) -> Option<&Entry<P>> {
3341 let s = self.slots.get(k.idx as usize)?;
3342 if s.generation != k.generation {
3343 return None;
3344 }
3345 s.entry.as_ref()
3346 }
3347
3348 #[inline]
3349 fn entry(&self, k: Key) -> &Entry<P> {
3350 self.slot_entry(k).expect("stale task key")
3351 }
3352
3353 #[inline]
3354 fn entry_mut(&mut self, k: Key) -> &mut Entry<P> {
3355 let s = &mut self.slots[k.idx as usize];
3356 debug_assert_eq!(s.generation, k.generation);
3357 s.entry.as_mut().expect("stale task key")
3358 }
3359
3360 fn entry_by_id(&self, id: TaskId) -> Option<&Entry<P>> {
3361 self.index.get(&id).and_then(|k| self.slot_entry(*k))
3362 }
3363
3364 #[doc(hidden)]
3370 pub fn check_invariants(&self) {
3371 let mut busy = vec![0u32; self.pools.len()];
3372 let mut group_busy: Vec<Vec<u32>> =
3373 self.pools.iter().map(|p| vec![0; p.groups.len()]).collect();
3374 for (lid, l) in &self.leases {
3375 let e = self.slot_entry(l.key).expect("lease points to live task");
3376 assert_eq!(e.status, TaskStatus::Running);
3377 assert_eq!(e.lease, Some(*lid));
3378 busy[l.pool as usize] += 1;
3379 group_busy[l.pool as usize][l.group as usize] += 1;
3380 }
3381 for (i, p) in self.pools.iter().enumerate() {
3382 assert_eq!(
3383 p.capacity,
3384 p.groups.iter().map(|g| g.capacity).sum::<u32>(),
3385 "pool capacity is the sum of its groups"
3386 );
3387 let gb: Vec<u32> = p.groups.iter().map(|g| g.busy).collect();
3388 assert_eq!(
3389 gb, group_busy[i],
3390 "group busy counts match running attempts"
3391 );
3392 }
3393 let mut held = vec![0u32; self.pools.len()];
3394 let mut pending = vec![0usize; self.pools.len()];
3395 let mut queued = vec![0usize; self.pools.len()];
3396 let mut pending_class = vec![[0usize; Priority::COUNT]; self.pools.len()];
3397 let mut retrying = vec![[0usize; Priority::COUNT]; self.pools.len()];
3398 let mut reservations = vec![0usize; self.pools.len()];
3399 let mut live_parents: FxHashMap<TaskId, usize> = FxHashMap::default();
3400 for k in self.index.values() {
3401 for (child, _) in &self.entry(*k).dependents {
3402 *live_parents.entry(*child).or_default() += 1;
3403 }
3404 }
3405 let mut awaiting: FxHashMap<TaskId, usize> = FxHashMap::default();
3406 for list in self.awaits.values() {
3407 for (id, _) in list {
3408 if self.index.contains_key(id) {
3409 *awaiting.entry(*id).or_default() += 1;
3410 }
3411 }
3412 }
3413 for (id, k) in &self.index {
3414 let e = self.slot_entry(*k).expect("index points to live slot");
3415 assert_eq!(e.id, *id);
3416 assert!(!e.status.is_terminal());
3417 if e.status == TaskStatus::Running {
3418 assert!(e.lease.is_some() && !e.pending);
3419 } else {
3420 assert!(e.pending, "non-running task {id} not counted as pending");
3421 pending[e.pool as usize] += 1;
3422 }
3423 if let Some(f) = &e.fixed
3424 && f.held
3425 {
3426 held[e.pool as usize] += 1;
3427 }
3428 let p = &self.pools[e.pool as usize];
3429 match e.loc {
3430 Loc::Lane => {
3431 assert_eq!(p.lanes[e.class as usize].get(e.seq), Some(*k));
3432 queued[e.pool as usize] += 1;
3433 }
3434 Loc::Edf => {
3435 assert_eq!(p.edf[e.class as usize].get(&(e.lst, e.seq)), Some(k));
3436 queued[e.pool as usize] += 1;
3437 }
3438 Loc::Due => {
3439 assert_eq!(p.due.get(&(e.fixed.as_ref().unwrap().at, e.seq)), Some(k));
3440 queued[e.pool as usize] += 1;
3441 }
3442 Loc::Fair => {
3443 let f = p.fair.as_ref().expect("fair queue in a fair pool");
3444 assert_eq!(f.queues[e.class as usize].get(&(e.vtag, e.seq)), Some(k));
3445 queued[e.pool as usize] += 1;
3446 }
3447 Loc::LabelWait => assert!(
3448 p.label_waiters
3449 .get(&e.constraint)
3450 .is_some_and(|q| q.contains(k)),
3451 "task {id} waiting for labels is missing from its waiter queue"
3452 ),
3453 Loc::Parked(_) | Loc::None => {}
3454 }
3455 if matches!(
3456 e.loc,
3457 Loc::Lane | Loc::Edf | Loc::Due | Loc::Parked(_) | Loc::LabelWait | Loc::Fair
3458 ) {
3459 assert_eq!(e.status, TaskStatus::Ready);
3460 }
3461 if e.status == TaskStatus::Blocked {
3462 assert!(
3463 e.unmet > 0 || e.fixed.is_some(),
3464 "blocked task {id} without unmet deps"
3465 );
3466 }
3467 if e.pending {
3468 pending_class[e.pool as usize][e.class as usize] += 1;
3469 }
3470 if e.status == TaskStatus::Retrying {
3471 retrying[e.pool as usize][e.class as usize] += 1;
3472 }
3473 if let Loc::Parked(r) = e.loc {
3474 assert!(
3475 self.resources[r.0 as usize].waiters.contains(k),
3476 "parked task {id} missing from its waiter queue"
3477 );
3478 }
3479 if let Some(f) = &e.fixed {
3480 assert!(
3481 !(f.held && f.hold_pending),
3482 "task {id} both held and pending a hold"
3483 );
3484 assert_eq!(
3485 p.pending_holds.get(&(f.at, e.seq)) == Some(k),
3486 f.hold_pending,
3487 "task {id} pending-hold mismatch"
3488 );
3489 assert_eq!(
3490 p.held_tasks.get(&(f.at, e.seq)) == Some(k),
3491 f.held,
3492 "task {id} held mismatch"
3493 );
3494 assert!(
3495 p.reservations.contains(e.id, f.res_start),
3496 "fixed task {id} lost its reservation"
3497 );
3498 assert_eq!(
3499 p.unstarted.contains(e.id, f.res_start),
3500 e.attempt == 0 && e.status != TaskStatus::Running,
3501 "task {id} unstarted-reservation mismatch"
3502 );
3503 reservations[e.pool as usize] += 1;
3504 }
3505 let parents = live_parents.get(id).copied().unwrap_or(0);
3507 let awaited = awaiting.get(id).copied().unwrap_or(0);
3508 assert_eq!(e.awaiting as usize, awaited, "task {id} awaiting mismatch");
3509 if e.status == TaskStatus::Blocked && e.fixed.is_none() {
3510 assert_eq!(
3511 e.unmet as usize,
3512 parents + awaited,
3513 "task {id} unmet mismatch"
3514 );
3515 }
3516 }
3517 for (i, p) in self.pools.iter().enumerate() {
3518 assert_eq!(p.busy, busy[i], "pool {} busy mismatch", p.cfg.name);
3519 assert_eq!(p.held, held[i], "pool {} held mismatch", p.cfg.name);
3520 assert_eq!(
3521 p.pending, pending[i],
3522 "pool {} pending mismatch",
3523 p.cfg.name
3524 );
3525 assert_eq!(
3526 p.ready_len(),
3527 queued[i],
3528 "pool {} queue mismatch",
3529 p.cfg.name
3530 );
3531 assert_eq!(
3532 p.pending_class, pending_class[i],
3533 "pool {} per-class pending",
3534 p.cfg.name
3535 );
3536 assert_eq!(
3537 p.retrying, retrying[i],
3538 "pool {} retrying mismatch",
3539 p.cfg.name
3540 );
3541 assert_eq!(
3542 p.reservations.len(),
3543 reservations[i],
3544 "pool {} stray reservations",
3545 p.cfg.name
3546 );
3547 assert_eq!(
3548 p.held_tasks.len() as u32,
3549 p.held,
3550 "pool {} held_tasks mismatch",
3551 p.cfg.name
3552 );
3553 }
3554 assert_eq!(self.total_pending, pending.iter().sum::<usize>());
3555 for c in 0..Priority::COUNT {
3556 let sum: usize = pending_class.iter().map(|pc| pc[c]).sum();
3557 assert_eq!(
3558 self.pending_class[c], sum,
3559 "global per-class pending mismatch"
3560 );
3561 }
3562 let mut in_use = vec![0u64; self.resources.len()];
3564 for l in self.leases.values() {
3565 if let Some(res) = &self.entry(l.key).resources {
3566 for (r, a) in res.iter() {
3567 in_use[r.0 as usize] += *a as u64;
3568 }
3569 }
3570 }
3571 for (i, r) in self.resources.iter().enumerate() {
3572 if let ResourceKind::Semaphore { permits } = r.kind {
3573 let used = permits as f64 - r.available;
3574 assert!(
3575 (used - in_use[i] as f64).abs() < 1e-6,
3576 "semaphore {} leaks permits: {used} in use, {} held",
3577 r.name,
3578 in_use[i]
3579 );
3580 }
3581 }
3582 for r in &self.resources {
3583 assert!(
3584 r.available <= r.capacity + 1e-9,
3585 "resource {} over capacity",
3586 r.name
3587 );
3588 assert!(r.available >= -1e-9, "resource {} negative", r.name);
3589 }
3590 }
3591}
3592
3593#[inline]
3595fn nanos(d: Duration) -> u64 {
3596 d.as_nanos().clamp(1, u64::MAX as u128) as u64
3597}
3598
3599#[inline]
3600fn edge_satisfied(cond: EdgeCond, parent: TaskStatus) -> bool {
3601 match cond {
3602 EdgeCond::OnSuccess => parent == TaskStatus::Succeeded,
3603 EdgeCond::OnComplete => parent.is_terminal(),
3604 EdgeCond::OnFailure => matches!(parent, TaskStatus::Failed | TaskStatus::DeadLettered),
3605 }
3606}
3607
3608fn occurrence_key(prefix: &str, epoch_unix_nanos: i64, at: Time) -> Arc<str> {
3610 let wall = epoch_unix_nanos as i128 + at.0 as i128;
3611 let secs = wall.div_euclid(1_000_000_000) as i64;
3612 match chrono::DateTime::from_timestamp(secs, 0) {
3613 Some(t) => format!("{prefix}@{}", t.format("%Y-%m-%dT%H:%M:%SZ")).into(),
3614 None => format!("{prefix}@{secs}").into(),
3615 }
3616}