1use super::backend::JobQueueBackend;
2use super::types::{
3 deduplication_expiration, page_repeat_entries, validate_job_priority, Job, JobEvent,
4 JobFinishedResult, JobFlow, JobFlowChildValues, JobFlowDependencies,
5 JobFlowDependencyCountOptions, JobFlowDependencyCounts, JobFlowDependencyIndex,
6 JobFlowDependencyKind, JobFlowDependencyPage, JobFlowDependencyPageItem,
7 JobFlowDependencyPageOptions, JobFlowDependencyPages, JobFlowDependencyPagesOptions,
8 JobFlowDependencySelectedCounts, JobFlowDependencyValues, JobFlowIgnoredFailures, JobId,
9 JobListOptions, JobListPage, JobLogEntry, JobLogPage, JobOptions, JobPriority,
10 JobPriorityCount, JobQueueSnapshot, JobQueueStats, JobRepeatEntry, JobRepeatListOptions,
11 JobRepeatPage, JobRetention, JobSpec, JobState, JobStateCount, JobWorkerId, QueueName,
12 DEFAULT_JOB_EVENT_RETENTION,
13};
14use crate::error::{LaneError, Result};
15use async_trait::async_trait;
16use chrono::{DateTime, Utc};
17use serde_json::Value;
18use std::cmp::Ordering;
19use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::sync::Mutex;
23use uuid::Uuid;
24
25#[derive(Debug, Default)]
26struct InMemoryJobQueueState {
27 paused: bool,
28 sequence: u64,
29 event_sequence: u64,
30 events: Vec<JobEvent>,
31 jobs: HashMap<JobId, Job>,
32 flow_dependencies: HashMap<JobId, InMemoryFlowDependencyIndex>,
33 deduplication_next: HashMap<String, Job>,
34 deduplication_next_flows: HashMap<String, JobFlow>,
35 released_deduplication_owners: HashSet<(String, JobId)>,
36}
37
38#[derive(Debug, Clone, Default)]
39struct InMemoryFlowDependencyIndex {
40 processed: BTreeMap<JobId, Value>,
41 ignored: BTreeMap<JobId, String>,
42 failed: BTreeSet<JobId>,
43}
44
45impl InMemoryFlowDependencyIndex {
46 fn is_empty(&self) -> bool {
47 self.processed.is_empty() && self.ignored.is_empty() && self.failed.is_empty()
48 }
49}
50
51#[derive(Debug, Clone)]
57pub struct InMemoryJobQueue {
58 queue: QueueName,
59 inner: Arc<Mutex<InMemoryJobQueueState>>,
60}
61
62impl InMemoryJobQueue {
63 pub fn new(queue: impl Into<String>) -> Self {
65 Self {
66 queue: queue.into(),
67 inner: Arc::new(Mutex::new(InMemoryJobQueueState::default())),
68 }
69 }
70
71 pub fn from_snapshot(snapshot: JobQueueSnapshot) -> Self {
73 let sequence = snapshot
74 .jobs
75 .iter()
76 .chain(snapshot.deduplication_next_jobs.iter())
77 .chain(
78 snapshot
79 .deduplication_next_flows
80 .iter()
81 .flat_map(|flow| std::iter::once(&flow.parent).chain(flow.children.iter())),
82 )
83 .map(|job| job.enqueued_seq)
84 .max()
85 .unwrap_or(0);
86 Self {
87 queue: snapshot.queue,
88 inner: Arc::new(Mutex::new(InMemoryJobQueueState {
89 paused: snapshot.paused,
90 event_sequence: snapshot.event_sequence,
91 events: snapshot.events,
92 sequence,
93 jobs: snapshot
94 .jobs
95 .into_iter()
96 .map(|job| (job.id.clone(), job))
97 .collect(),
98 flow_dependencies: snapshot
99 .flow_dependency_indexes
100 .into_iter()
101 .filter_map(|index| {
102 if index.parent_id.is_empty() {
103 return None;
104 }
105 let flow_index = InMemoryFlowDependencyIndex {
106 processed: index.processed,
107 ignored: index.ignored,
108 failed: index.failed.into_iter().collect(),
109 };
110 if flow_index.is_empty() {
111 None
112 } else {
113 Some((index.parent_id, flow_index))
114 }
115 })
116 .collect(),
117 deduplication_next: snapshot
118 .deduplication_next_jobs
119 .into_iter()
120 .filter_map(|job| {
121 let deduplication_id = job.options.deduplication.as_ref()?.id.clone();
122 Some((deduplication_id, job))
123 })
124 .collect(),
125 deduplication_next_flows: snapshot
126 .deduplication_next_flows
127 .into_iter()
128 .filter_map(|flow| {
129 let deduplication_id =
130 flow.parent.options.deduplication.as_ref()?.id.clone();
131 Some((deduplication_id, flow))
132 })
133 .collect(),
134 released_deduplication_owners: snapshot
135 .released_deduplication_owners
136 .into_iter()
137 .collect(),
138 })),
139 }
140 }
141
142 pub fn queue_name(&self) -> &str {
144 &self.queue
145 }
146
147 pub async fn snapshot(&self) -> JobQueueSnapshot {
149 let inner = self.inner.lock().await;
150 let mut jobs = inner.jobs.values().cloned().collect::<Vec<_>>();
151 jobs.sort_by(compare_list_order);
152 let mut deduplication_next_jobs = inner
153 .deduplication_next
154 .values()
155 .cloned()
156 .collect::<Vec<_>>();
157 deduplication_next_jobs.sort_by(compare_list_order);
158 let mut deduplication_next_flows = inner
159 .deduplication_next_flows
160 .values()
161 .cloned()
162 .collect::<Vec<_>>();
163 deduplication_next_flows
164 .sort_by(|left, right| compare_list_order(&left.parent, &right.parent));
165 JobQueueSnapshot {
166 queue: self.queue.clone(),
167 paused: inner.paused,
168 events: inner.events.clone(),
169 event_sequence: inner.event_sequence,
170 jobs,
171 deduplication_next_jobs,
172 deduplication_next_flows,
173 released_deduplication_owners: sorted_released_deduplication_owners(
174 &inner.released_deduplication_owners,
175 ),
176 flow_dependency_indexes: sorted_flow_dependency_indexes(&inner.flow_dependencies),
177 }
178 }
179
180 fn emit_job_created_events_locked(
181 inner: &mut InMemoryJobQueueState,
182 job: &Job,
183 now: DateTime<Utc>,
184 ) {
185 let mut fields = BTreeMap::new();
186 fields.insert("name".to_string(), Value::String(job.name.clone()));
187 emit_event_locked(inner, "added", Some(job), None, now, fields);
188 emit_event_locked(
189 inner,
190 job_event_for_state(job.state),
191 Some(job),
192 None,
193 now,
194 BTreeMap::new(),
195 );
196 }
197
198 async fn fail_active_job(
199 &self,
200 job_id: &str,
201 lock_token: &str,
202 error: String,
203 now: DateTime<Utc>,
204 allow_retry: bool,
205 ) -> Result<Job> {
206 let mut inner = self.inner.lock().await;
207 let failed = {
208 let job = inner
209 .jobs
210 .get_mut(job_id)
211 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
212 require_active(job, "fail")?;
213 require_lock_token(job, lock_token)?;
214 job.worker_id = None;
215 job.lock_token = None;
216 job.lease_expires_at = None;
217 job.deferred_failure = None;
218 job.failed_reason = Some(error);
219
220 if allow_retry && should_retry(job) {
221 let delay = job
222 .options
223 .retry_policy
224 .delay_for_attempt(job.attempts_made);
225 job.state = JobState::Delayed;
226 job.scheduled_at = add_duration(now, delay);
227 job.finished_at = None;
228 } else {
229 job.state = JobState::Failed;
230 job.finished_at = Some(now);
231 }
232
233 job.clone()
234 };
235 if failed.state == JobState::Failed {
236 if let Some(parent_id) = &failed.parent_id {
237 Self::record_terminal_child_dependency_locked(&mut inner, parent_id, &failed);
238 }
239 if failed.options.removes_failed_immediately() {
240 Self::remove_job_record_locked(&mut inner, job_id);
241 } else if let Some(retention) = failed.options.failed_retention() {
242 Self::apply_finished_retention_locked(&mut inner, JobState::Failed, retention, now);
243 }
244 if Self::enqueue_deduplicated_next_locked(&mut inner, &failed, now).is_none() {
245 Self::enqueue_deduplicated_next_flow_locked(&mut inner, &failed, now);
246 }
247 if let Some(parent_id) = &failed.parent_id {
248 if failed.options.fail_parent_on_failure {
249 Self::defer_parent_failure_after_child_failure_locked(
250 &mut inner, parent_id, &failed.id, now,
251 );
252 } else if failed.options.continue_parent_on_failure {
253 Self::continue_parent_after_child_failure_locked(&mut inner, parent_id, now);
254 } else if child_failure_releases_dependency(&failed) {
255 Self::release_parent_if_ready_locked(&mut inner, parent_id, now);
256 } else {
257 Self::fail_waiting_parent_locked(
258 &mut inner,
259 parent_id,
260 format!(
261 "child job {} failed: {}",
262 failed.id,
263 failed.failed_reason.as_deref().unwrap_or("unknown error")
264 ),
265 now,
266 );
267 }
268 }
269 }
270 let mut fields = BTreeMap::new();
271 if let Some(reason) = &failed.failed_reason {
272 fields.insert("failedReason".to_string(), Value::String(reason.clone()));
273 }
274 emit_event_locked(
275 &mut inner,
276 job_event_for_state(failed.state),
277 Some(&failed),
278 Some(JobState::Active),
279 now,
280 fields,
281 );
282 if retries_exhausted(&failed) {
283 emit_retries_exhausted_event_locked(&mut inner, &failed, now);
284 }
285 if failed.state == JobState::Failed {
286 emit_drained_event_if_needed_locked(&mut inner, now);
287 }
288 Ok(failed)
289 }
290
291 pub async fn add(
293 &self,
294 name: impl Into<String>,
295 payload: Value,
296 options: JobOptions,
297 ) -> Result<Job> {
298 self.add_at(name, payload, options, Utc::now()).await
299 }
300
301 pub async fn add_at(
303 &self,
304 name: impl Into<String>,
305 payload: Value,
306 options: JobOptions,
307 now: DateTime<Utc>,
308 ) -> Result<Job> {
309 validate_job_options_at(&options, now)?;
310 let mut job = Job::new(self.queue.clone(), name.into(), payload, options, now);
311 let mut inner = self.inner.lock().await;
312 if let Some(existing) = inner.jobs.get(&job.id) {
313 return Ok(existing.clone());
314 }
315 if let Some(existing) = find_active_deduplicated_job(
316 &inner.jobs,
317 &inner.released_deduplication_owners,
318 &job,
319 now,
320 )
321 .cloned()
322 {
323 if deduplication_replaces_delayed_owner(&job, &existing) {
324 preserve_replacement_deduplication_expiration(&mut job, &existing);
325 let existing_id = existing.id.clone();
326 if let Some(removed) = Self::remove_job_record_locked(&mut inner, &existing_id) {
327 emit_removed_event_locked(&mut inner, &removed, now);
328 emit_replaced_deduplicated_owner_events_locked(&mut inner, &job, &removed, now);
329 }
330 } else {
331 Self::store_deduplicated_next_locked(&mut inner, &job, &existing);
332 Self::extend_deduplication_expiration_locked(
333 &mut inner.jobs,
334 &job,
335 &existing.id,
336 now,
337 );
338 if deduplication_duplicate_emits_events(&job, &existing) {
339 emit_deduplicated_events_locked(&mut inner, &existing, &job, now);
340 }
341 return Ok(inner.jobs.get(&existing.id).cloned().unwrap_or(existing));
342 }
343 }
344 if let Some(existing) = find_active_repeat_job(&inner.jobs, &job) {
345 return Ok(existing.clone());
346 }
347 Self::forget_released_deduplication_owner_locked(&mut inner, &job);
348 assign_waiting_order(&mut inner.sequence, &mut job);
349 inner.jobs.insert(job.id.clone(), job.clone());
350 Self::emit_job_created_events_locked(&mut inner, &job, now);
351 Ok(job)
352 }
353
354 pub async fn add_many(&self, jobs: Vec<JobSpec>) -> Result<Vec<Job>> {
356 self.add_many_at(jobs, Utc::now()).await
357 }
358
359 pub async fn add_many_at(&self, jobs: Vec<JobSpec>, now: DateTime<Utc>) -> Result<Vec<Job>> {
361 for spec in &jobs {
362 validate_job_options_at(&spec.options, now)?;
363 }
364 let created = jobs
365 .into_iter()
366 .map(|spec| {
367 Job::new(
368 self.queue.clone(),
369 spec.name,
370 spec.payload,
371 spec.options,
372 now,
373 )
374 })
375 .collect::<Vec<_>>();
376
377 let mut inner = self.inner.lock().await;
378 let mut added = Vec::with_capacity(created.len());
379 for mut job in created {
380 if let Some(existing) = inner.jobs.get(&job.id) {
381 added.push(existing.clone());
382 continue;
383 }
384 if let Some(existing) = find_active_deduplicated_job(
385 &inner.jobs,
386 &inner.released_deduplication_owners,
387 &job,
388 now,
389 )
390 .cloned()
391 {
392 if deduplication_replaces_delayed_owner(&job, &existing) {
393 preserve_replacement_deduplication_expiration(&mut job, &existing);
394 let existing_id = existing.id.clone();
395 if let Some(removed) = Self::remove_job_record_locked(&mut inner, &existing_id)
396 {
397 emit_removed_event_locked(&mut inner, &removed, now);
398 emit_replaced_deduplicated_owner_events_locked(
399 &mut inner, &job, &removed, now,
400 );
401 }
402 } else {
403 Self::store_deduplicated_next_locked(&mut inner, &job, &existing);
404 Self::extend_deduplication_expiration_locked(
405 &mut inner.jobs,
406 &job,
407 &existing.id,
408 now,
409 );
410 if deduplication_duplicate_emits_events(&job, &existing) {
411 emit_deduplicated_events_locked(&mut inner, &existing, &job, now);
412 }
413 added.push(inner.jobs.get(&existing.id).cloned().unwrap_or(existing));
414 continue;
415 }
416 }
417 if let Some(existing) = find_active_repeat_job(&inner.jobs, &job) {
418 added.push(existing.clone());
419 continue;
420 }
421 assign_waiting_order(&mut inner.sequence, &mut job);
422 Self::forget_released_deduplication_owner_locked(&mut inner, &job);
423 inner.jobs.insert(job.id.clone(), job.clone());
424 Self::emit_job_created_events_locked(&mut inner, &job, now);
425 added.push(job);
426 }
427 Ok(added)
428 }
429
430 pub async fn add_flow(&self, parent: JobSpec, children: Vec<JobSpec>) -> Result<JobFlow> {
433 self.add_flow_at(parent, children, Utc::now()).await
434 }
435
436 pub async fn add_flow_at(
438 &self,
439 parent: JobSpec,
440 children: Vec<JobSpec>,
441 now: DateTime<Utc>,
442 ) -> Result<JobFlow> {
443 validate_job_options_at(&parent.options, now)?;
444 for child in &children {
445 validate_job_options_at(&child.options, now)?;
446 }
447 let mut parent_job = Job::new(
448 self.queue.clone(),
449 parent.name,
450 parent.payload,
451 parent.options,
452 now,
453 );
454 parent_job.state = if children.is_empty() {
455 state_after_dependencies(parent_job.scheduled_at, now)
456 } else {
457 JobState::WaitingChildren
458 };
459
460 let mut child_jobs = Vec::with_capacity(children.len());
461 for child in children {
462 let mut child_job = Job::new(
463 self.queue.clone(),
464 child.name,
465 child.payload,
466 child.options,
467 now,
468 );
469 child_job.parent_id = Some(parent_job.id.clone());
470 parent_job.child_ids.push(child_job.id.clone());
471 child_jobs.push(child_job);
472 }
473 validate_flow_job_ids(&parent_job, &child_jobs)?;
474
475 let mut inner = self.inner.lock().await;
476 if let Some(existing_parent) = inner.jobs.get(&parent_job.id).cloned() {
477 return Self::add_flow_for_existing_parent_locked(
478 &mut inner,
479 existing_parent,
480 child_jobs,
481 now,
482 );
483 }
484 let candidate_flow = JobFlow {
485 parent: parent_job.clone(),
486 children: child_jobs.clone(),
487 };
488 if let Some(existing) = find_active_deduplicated_job(
489 &inner.jobs,
490 &inner.released_deduplication_owners,
491 &parent_job,
492 now,
493 )
494 .cloned()
495 {
496 if Self::store_deduplicated_next_flow_locked(&mut inner, &candidate_flow, &existing) {
497 emit_deduplicated_events_locked(&mut inner, &existing, &parent_job, now);
498 return Ok(Self::flow_for_existing_owner_locked(&inner, &existing));
499 }
500 Self::extend_deduplication_expiration_locked(
501 &mut inner.jobs,
502 &parent_job,
503 &existing.id,
504 now,
505 );
506 emit_deduplicated_events_locked(&mut inner, &existing, &parent_job, now);
507 return Ok(Self::flow_for_existing_owner_locked(&inner, &existing));
508 }
509 let mut duplicated_children = HashSet::new();
510 for child in &child_jobs {
511 if inner.jobs.contains_key(&child.id) {
512 Self::validate_existing_flow_child_parent_locked(
513 &inner,
514 &parent_job.id,
515 &child.id,
516 )?;
517 duplicated_children.insert(child.id.clone());
518 }
519 }
520 let mut deduplicated_children = HashMap::new();
521 let mut deduplicated_next_children = HashMap::new();
522 let mut deduplicated_next_child_by_id = HashMap::new();
523 let mut flow_deduplication_ids = HashSet::new();
524 if let Some(deduplication_id) = active_deduplication_id(&parent_job, now) {
525 flow_deduplication_ids.insert(deduplication_id.to_string());
526 }
527 for child in &child_jobs {
528 if duplicated_children.contains(&child.id) {
529 continue;
530 }
531 if let Some(deduplication_id) = active_deduplication_id(child, now) {
532 if let Some(existing) = find_active_deduplication_id(
533 &inner.jobs,
534 &inner.released_deduplication_owners,
535 deduplication_id,
536 now,
537 )
538 .cloned()
539 {
540 if flow_child_deduplication_requires_next(child, &existing) {
541 if !Self::store_deduplicated_next_locked(&mut inner, child, &existing) {
542 return Err(LaneError::ConfigError(format!(
543 "flow child deduplication id `{deduplication_id}` is already active on job {}",
544 existing.id
545 )));
546 }
547 if let Some(previous_child_id) = deduplicated_next_child_by_id
548 .insert(deduplication_id.to_string(), child.id.clone())
549 {
550 parent_job
551 .child_ids
552 .retain(|child_id| child_id != &previous_child_id);
553 deduplicated_next_children.remove(&previous_child_id);
554 }
555 deduplicated_next_children.insert(child.id.clone(), existing);
556 continue;
557 }
558 Self::extend_deduplication_expiration_locked(
559 &mut inner.jobs,
560 child,
561 &existing.id,
562 now,
563 );
564 deduplicated_children.insert(child.id.clone(), existing);
565 continue;
566 }
567 if !flow_deduplication_ids.insert(deduplication_id.to_string()) {
568 return Err(LaneError::ConfigError(format!(
569 "flow deduplication id `{deduplication_id}` already active"
570 )));
571 }
572 }
573 }
574 parent_job
575 .child_ids
576 .retain(|child_id| !deduplicated_children.contains_key(child_id));
577 if parent_job.child_ids.is_empty() {
578 parent_job.state = state_after_dependencies(parent_job.scheduled_at, now);
579 }
580 let mut flow_repeat_keys = HashSet::new();
581 for job in std::iter::once(&parent_job).chain(child_jobs.iter().filter(|child| {
582 !duplicated_children.contains(&child.id)
583 && !deduplicated_children.contains_key(&child.id)
584 && !deduplicated_next_children.contains_key(&child.id)
585 })) {
586 if let Some(repeat_key) = active_repeat_key(job) {
587 if find_active_repeat_key(&inner.jobs, repeat_key).is_some()
588 || !flow_repeat_keys.insert(repeat_key.to_string())
589 {
590 return Err(LaneError::ConfigError(format!(
591 "flow repeat key `{repeat_key}` already active"
592 )));
593 }
594 }
595 }
596 assign_waiting_order(&mut inner.sequence, &mut parent_job);
597 Self::forget_released_deduplication_owner_locked(&mut inner, &parent_job);
598 inner.jobs.insert(parent_job.id.clone(), parent_job.clone());
599 Self::emit_job_created_events_locked(&mut inner, &parent_job, now);
600 let mut stored_child_jobs = Vec::with_capacity(child_jobs.len());
601 for mut child in child_jobs {
602 if let Some(existing) = deduplicated_children
603 .get(&child.id)
604 .or_else(|| deduplicated_next_children.get(&child.id))
605 {
606 emit_deduplicated_events_locked(&mut inner, existing, &child, now);
607 } else if duplicated_children.contains(&child.id) {
608 if let Some(existing) = inner.jobs.get_mut(&child.id) {
609 existing.parent_id = Some(parent_job.id.clone());
610 let existing = existing.clone();
611 Self::record_terminal_child_dependency_locked(
612 &mut inner,
613 &parent_job.id,
614 &existing,
615 );
616 emit_duplicated_event_locked(&mut inner, &existing, now);
617 stored_child_jobs.push(existing);
618 }
619 } else {
620 assign_waiting_order(&mut inner.sequence, &mut child);
621 Self::forget_released_deduplication_owner_locked(&mut inner, &child);
622 inner.jobs.insert(child.id.clone(), child.clone());
623 Self::emit_job_created_events_locked(&mut inner, &child, now);
624 stored_child_jobs.push(child);
625 }
626 }
627 if deduplicated_next_children.is_empty() {
628 Self::release_parent_if_ready_locked(&mut inner, &parent_job.id, now);
629 }
630 let parent_job = inner
631 .jobs
632 .get(&parent_job.id)
633 .cloned()
634 .unwrap_or(parent_job);
635
636 Ok(JobFlow {
637 parent: parent_job,
638 children: stored_child_jobs,
639 })
640 }
641
642 fn add_flow_for_existing_parent_locked(
643 inner: &mut InMemoryJobQueueState,
644 existing_parent: Job,
645 mut child_jobs: Vec<Job>,
646 now: DateTime<Utc>,
647 ) -> Result<JobFlow> {
648 let parent_id = existing_parent.id.clone();
649 emit_duplicated_event_locked(inner, &existing_parent, now);
650
651 let existing_child_ids = existing_parent
652 .child_ids
653 .iter()
654 .cloned()
655 .collect::<HashSet<_>>();
656 let mut duplicated_children = HashSet::new();
657 for child in &child_jobs {
658 if let Some(existing) = inner.jobs.get(&child.id) {
659 Self::validate_existing_flow_child_parent_locked(inner, &parent_id, &child.id)?;
660 if existing_child_ids.contains(&child.id)
661 && existing.parent_id.as_deref() != Some(parent_id.as_str())
662 {
663 return Err(LaneError::ConfigError(format!(
664 "flow child id `{}` already exists",
665 child.id
666 )));
667 }
668 duplicated_children.insert(child.id.clone());
669 } else if existing_child_ids.contains(&child.id) {
670 return Err(LaneError::ConfigError(format!(
671 "flow child id `{}` already exists",
672 child.id
673 )));
674 }
675 }
676
677 let mut deduplicated_children = HashMap::new();
678 let mut deduplicated_next_children = HashMap::new();
679 let mut deduplicated_next_child_by_id = HashMap::new();
680 let mut flow_deduplication_ids = HashSet::new();
681 for child in &child_jobs {
682 if duplicated_children.contains(&child.id) {
683 continue;
684 }
685 if let Some(deduplication_id) = active_deduplication_id(child, now) {
686 if let Some(existing) = find_active_deduplication_id(
687 &inner.jobs,
688 &inner.released_deduplication_owners,
689 deduplication_id,
690 now,
691 )
692 .cloned()
693 {
694 if flow_child_deduplication_requires_next(child, &existing) {
695 if !Self::store_deduplicated_next_locked(inner, child, &existing) {
696 return Err(LaneError::ConfigError(format!(
697 "flow child deduplication id `{deduplication_id}` is already active on job {}",
698 existing.id
699 )));
700 }
701 if let Some(previous_child_id) = deduplicated_next_child_by_id
702 .insert(deduplication_id.to_string(), child.id.clone())
703 {
704 deduplicated_next_children.remove(&previous_child_id);
705 }
706 deduplicated_children.insert(child.id.clone(), existing.clone());
707 deduplicated_next_children.insert(child.id.clone(), existing);
708 continue;
709 }
710 Self::extend_deduplication_expiration_locked(
711 &mut inner.jobs,
712 child,
713 &existing.id,
714 now,
715 );
716 deduplicated_children.insert(child.id.clone(), existing);
717 continue;
718 }
719 if !flow_deduplication_ids.insert(deduplication_id.to_string()) {
720 return Err(LaneError::ConfigError(format!(
721 "flow deduplication id `{deduplication_id}` already active"
722 )));
723 }
724 }
725 }
726
727 let mut flow_repeat_keys = HashSet::new();
728 for child in &child_jobs {
729 if duplicated_children.contains(&child.id)
730 || deduplicated_children.contains_key(&child.id)
731 || deduplicated_next_children.contains_key(&child.id)
732 {
733 continue;
734 }
735 if let Some(repeat_key) = active_repeat_key(child) {
736 if find_active_repeat_key(&inner.jobs, repeat_key).is_some()
737 || !flow_repeat_keys.insert(repeat_key.to_string())
738 {
739 return Err(LaneError::ConfigError(format!(
740 "flow repeat key `{repeat_key}` already active"
741 )));
742 }
743 }
744 }
745
746 if let Some(parent) = inner.jobs.get_mut(&parent_id) {
747 for child in &child_jobs {
748 if deduplicated_children.contains_key(&child.id)
749 && !deduplicated_next_children.contains_key(&child.id)
750 {
751 continue;
752 }
753 if !parent
754 .child_ids
755 .iter()
756 .any(|child_id| child_id == &child.id)
757 {
758 parent.child_ids.push(child.id.clone());
759 }
760 }
761 }
762
763 let mut stored_child_jobs = Vec::with_capacity(child_jobs.len());
764 for child in &mut child_jobs {
765 if let Some(existing) = deduplicated_children.get(&child.id) {
766 emit_deduplicated_events_locked(inner, existing, child, now);
767 } else if duplicated_children.contains(&child.id) {
768 if let Some(existing) = inner.jobs.get_mut(&child.id) {
769 existing.parent_id = Some(parent_id.clone());
770 let existing = existing.clone();
771 Self::record_terminal_child_dependency_locked(inner, &parent_id, &existing);
772 emit_duplicated_event_locked(inner, &existing, now);
773 stored_child_jobs.push(existing);
774 }
775 } else {
776 assign_waiting_order(&mut inner.sequence, child);
777 Self::forget_released_deduplication_owner_locked(inner, child);
778 inner.jobs.insert(child.id.clone(), child.clone());
779 Self::emit_job_created_events_locked(inner, child, now);
780 stored_child_jobs.push(child.clone());
781 }
782 }
783 if deduplicated_next_children.is_empty() {
784 Self::release_parent_if_ready_locked(inner, &parent_id, now);
785 }
786 let parent = inner
787 .jobs
788 .get(&parent_id)
789 .cloned()
790 .unwrap_or(existing_parent);
791
792 Ok(JobFlow {
793 parent,
794 children: stored_child_jobs,
795 })
796 }
797
798 pub async fn add_flow_children(
800 &self,
801 parent_id: &str,
802 lock_token: &str,
803 children: Vec<JobSpec>,
804 ) -> Result<Vec<Job>> {
805 self.add_flow_children_at(parent_id, lock_token, children, Utc::now())
806 .await
807 }
808
809 pub async fn add_flow_children_at(
811 &self,
812 parent_id: &str,
813 lock_token: &str,
814 children: Vec<JobSpec>,
815 now: DateTime<Utc>,
816 ) -> Result<Vec<Job>> {
817 if children.is_empty() {
818 return Err(LaneError::ConfigError(
819 "flow children cannot be empty".to_string(),
820 ));
821 }
822 for child in &children {
823 validate_job_options_at(&child.options, now)?;
824 }
825
826 let mut child_jobs = Vec::with_capacity(children.len());
827 for child in children {
828 let mut child_job = Job::new(
829 self.queue.clone(),
830 child.name,
831 child.payload,
832 child.options,
833 now,
834 );
835 child_job.parent_id = Some(parent_id.to_string());
836 child_jobs.push(child_job);
837 }
838 validate_flow_child_jobs(parent_id, &child_jobs)?;
839
840 let mut inner = self.inner.lock().await;
841 {
842 let parent = inner
843 .jobs
844 .get(parent_id)
845 .ok_or_else(|| LaneError::JobNotFound(parent_id.to_string()))?;
846 require_active(parent, "add flow children")?;
847 require_lock_token(parent, lock_token)?;
848 ensure_flow_dependencies_have_not_failed(
849 parent,
850 &inner.jobs,
851 inner.flow_dependencies.get(parent_id),
852 "add flow children",
853 )?;
854 }
855
856 let existing_child_ids = inner
857 .jobs
858 .get(parent_id)
859 .map(|parent| parent.child_ids.iter().cloned().collect::<HashSet<_>>())
860 .unwrap_or_default();
861 let mut duplicated_children = HashSet::new();
862 for child in &child_jobs {
863 if let Some(existing) = inner.jobs.get(&child.id) {
864 Self::validate_existing_flow_child_parent_locked(&inner, parent_id, &child.id)?;
865 if existing_child_ids.contains(&child.id)
866 && existing.parent_id.as_deref() != Some(parent_id)
867 {
868 return Err(LaneError::ConfigError(format!(
869 "flow child id `{}` already exists",
870 child.id
871 )));
872 }
873 duplicated_children.insert(child.id.clone());
874 } else if existing_child_ids.contains(&child.id) {
875 return Err(LaneError::ConfigError(format!(
876 "flow child id `{}` already exists",
877 child.id
878 )));
879 }
880 }
881
882 let mut deduplicated_children = HashMap::new();
883 let mut deduplicated_next_children = HashMap::new();
884 let mut deduplicated_next_child_by_id = HashMap::new();
885 let mut flow_deduplication_ids = HashSet::new();
886 for child in &child_jobs {
887 if duplicated_children.contains(&child.id) {
888 continue;
889 }
890 if let Some(deduplication_id) = active_deduplication_id(child, now) {
891 if let Some(existing) = find_active_deduplication_id(
892 &inner.jobs,
893 &inner.released_deduplication_owners,
894 deduplication_id,
895 now,
896 )
897 .cloned()
898 {
899 if flow_child_deduplication_requires_next(child, &existing) {
900 if !Self::store_deduplicated_next_locked(&mut inner, child, &existing) {
901 return Err(LaneError::ConfigError(format!(
902 "flow child deduplication id `{deduplication_id}` is already active on job {}",
903 existing.id
904 )));
905 }
906 if let Some(previous_child_id) = deduplicated_next_child_by_id
907 .insert(deduplication_id.to_string(), child.id.clone())
908 {
909 deduplicated_next_children.remove(&previous_child_id);
910 }
911 deduplicated_children.insert(child.id.clone(), existing.clone());
912 deduplicated_next_children.insert(child.id.clone(), existing);
913 continue;
914 }
915 Self::extend_deduplication_expiration_locked(
916 &mut inner.jobs,
917 child,
918 &existing.id,
919 now,
920 );
921 deduplicated_children.insert(child.id.clone(), existing);
922 continue;
923 }
924 if !flow_deduplication_ids.insert(deduplication_id.to_string()) {
925 return Err(LaneError::ConfigError(format!(
926 "flow deduplication id `{deduplication_id}` already active"
927 )));
928 }
929 }
930 }
931
932 let mut flow_repeat_keys = HashSet::new();
933 for child in &child_jobs {
934 if duplicated_children.contains(&child.id)
935 || deduplicated_children.contains_key(&child.id)
936 || deduplicated_next_children.contains_key(&child.id)
937 {
938 continue;
939 }
940 if let Some(repeat_key) = active_repeat_key(child) {
941 if find_active_repeat_key(&inner.jobs, repeat_key).is_some()
942 || !flow_repeat_keys.insert(repeat_key.to_string())
943 {
944 return Err(LaneError::ConfigError(format!(
945 "flow repeat key `{repeat_key}` already active"
946 )));
947 }
948 }
949 }
950
951 let parent = {
952 let parent = inner
953 .jobs
954 .get_mut(parent_id)
955 .ok_or_else(|| LaneError::JobNotFound(parent_id.to_string()))?;
956 parent.state = JobState::WaitingChildren;
957 parent.processed_at = None;
958 parent.finished_at = None;
959 parent.worker_id = None;
960 parent.lock_token = None;
961 parent.lease_expires_at = None;
962 parent.deferred_failure = None;
963 parent.failed_reason = None;
964 for child in &child_jobs {
965 if deduplicated_children.contains_key(&child.id)
966 && !deduplicated_next_children.contains_key(&child.id)
967 {
968 continue;
969 }
970 if !parent
971 .child_ids
972 .iter()
973 .any(|child_id| child_id == &child.id)
974 {
975 parent.child_ids.push(child.id.clone());
976 }
977 }
978 parent.clone()
979 };
980
981 let mut stored_child_jobs = Vec::with_capacity(child_jobs.len());
982 for child in &mut child_jobs {
983 if let Some(existing) = deduplicated_children.get(&child.id) {
984 emit_deduplicated_events_locked(&mut inner, existing, child, now);
985 } else if duplicated_children.contains(&child.id) {
986 if let Some(existing) = inner.jobs.get_mut(&child.id) {
987 existing.parent_id = Some(parent_id.to_string());
988 let existing = existing.clone();
989 Self::record_terminal_child_dependency_locked(&mut inner, parent_id, &existing);
990 emit_duplicated_event_locked(&mut inner, &existing, now);
991 stored_child_jobs.push(existing);
992 }
993 } else {
994 assign_waiting_order(&mut inner.sequence, child);
995 Self::forget_released_deduplication_owner_locked(&mut inner, child);
996 inner.jobs.insert(child.id.clone(), child.clone());
997 Self::emit_job_created_events_locked(&mut inner, child, now);
998 stored_child_jobs.push(child.clone());
999 }
1000 }
1001 emit_event_locked(
1002 &mut inner,
1003 "waiting-children",
1004 Some(&parent),
1005 Some(JobState::Active),
1006 now,
1007 BTreeMap::new(),
1008 );
1009 if deduplicated_next_children.is_empty() {
1010 Self::release_parent_if_ready_locked(&mut inner, parent_id, now);
1011 }
1012
1013 Ok(stored_child_jobs)
1014 }
1015
1016 pub async fn get_flow_dependencies(
1018 &self,
1019 parent_id: &str,
1020 ) -> Result<Option<JobFlowDependencies>> {
1021 let inner = self.inner.lock().await;
1022 let Some(parent) = inner.jobs.get(parent_id).cloned() else {
1023 return Ok(None);
1024 };
1025
1026 let mut children = Vec::new();
1027 let mut pending_child_ids = Vec::new();
1028 let mut missing_child_ids = Vec::new();
1029 for child_id in &parent.child_ids {
1030 match inner.jobs.get(child_id).cloned() {
1031 Some(child) => {
1032 if !child.state.is_terminal() {
1033 pending_child_ids.push(child.id.clone());
1034 }
1035 children.push(child);
1036 }
1037 None => missing_child_ids.push(child_id.clone()),
1038 }
1039 }
1040
1041 Ok(Some(JobFlowDependencies {
1042 parent,
1043 children,
1044 pending_child_ids,
1045 missing_child_ids,
1046 }))
1047 }
1048
1049 pub async fn get_flow_dependency_counts(
1051 &self,
1052 parent_id: &str,
1053 ) -> Result<Option<JobFlowDependencyCounts>> {
1054 let inner = self.inner.lock().await;
1055 let Some(parent) = inner.jobs.get(parent_id) else {
1056 return Ok(None);
1057 };
1058
1059 Ok(Some(flow_dependency_counts(
1060 parent,
1061 &inner.jobs,
1062 inner.flow_dependencies.get(parent_id),
1063 )))
1064 }
1065
1066 pub async fn get_flow_dependency_selected_counts(
1068 &self,
1069 parent_id: &str,
1070 options: JobFlowDependencyCountOptions,
1071 ) -> Result<Option<JobFlowDependencySelectedCounts>> {
1072 let inner = self.inner.lock().await;
1073 let Some(parent) = inner.jobs.get(parent_id) else {
1074 return Ok(None);
1075 };
1076
1077 Ok(Some(flow_dependency_selected_counts(
1078 parent,
1079 &inner.jobs,
1080 inner.flow_dependencies.get(parent_id),
1081 options,
1082 )))
1083 }
1084
1085 pub async fn get_flow_dependency_values(
1087 &self,
1088 parent_id: &str,
1089 ) -> Result<Option<JobFlowDependencyValues>> {
1090 let inner = self.inner.lock().await;
1091 let Some(parent) = inner.jobs.get(parent_id) else {
1092 return Ok(None);
1093 };
1094
1095 Ok(Some(flow_dependency_values(
1096 parent,
1097 &inner.jobs,
1098 inner.flow_dependencies.get(parent_id),
1099 )))
1100 }
1101
1102 pub async fn get_flow_dependency_page(
1104 &self,
1105 parent_id: &str,
1106 options: JobFlowDependencyPageOptions,
1107 ) -> Result<Option<JobFlowDependencyPage>> {
1108 let inner = self.inner.lock().await;
1109 let Some(parent) = inner.jobs.get(parent_id) else {
1110 return Ok(None);
1111 };
1112
1113 Ok(Some(flow_dependency_page(
1114 parent,
1115 &inner.jobs,
1116 inner.flow_dependencies.get(parent_id),
1117 options,
1118 )))
1119 }
1120
1121 pub async fn get_flow_dependency_pages(
1123 &self,
1124 parent_id: &str,
1125 options: JobFlowDependencyPagesOptions,
1126 ) -> Result<Option<JobFlowDependencyPages>> {
1127 let inner = self.inner.lock().await;
1128 let Some(parent) = inner.jobs.get(parent_id) else {
1129 return Ok(None);
1130 };
1131
1132 Ok(Some(flow_dependency_pages(
1133 parent,
1134 &inner.jobs,
1135 inner.flow_dependencies.get(parent_id),
1136 options,
1137 )))
1138 }
1139
1140 pub async fn get_flow_children_values(
1142 &self,
1143 parent_id: &str,
1144 ) -> Result<Option<JobFlowChildValues>> {
1145 let inner = self.inner.lock().await;
1146 let Some(parent) = inner.jobs.get(parent_id) else {
1147 return Ok(None);
1148 };
1149
1150 Ok(Some(flow_children_values(
1151 parent,
1152 &inner.jobs,
1153 inner.flow_dependencies.get(parent_id),
1154 )))
1155 }
1156
1157 pub async fn get_flow_ignored_children_failures(
1159 &self,
1160 parent_id: &str,
1161 ) -> Result<Option<JobFlowIgnoredFailures>> {
1162 let inner = self.inner.lock().await;
1163 let Some(parent) = inner.jobs.get(parent_id) else {
1164 return Ok(None);
1165 };
1166
1167 Ok(Some(flow_ignored_children_failures(
1168 parent,
1169 &inner.jobs,
1170 inner.flow_dependencies.get(parent_id),
1171 )))
1172 }
1173
1174 pub async fn remove_unprocessed_children(
1176 &self,
1177 parent_id: &str,
1178 now: DateTime<Utc>,
1179 ) -> Result<Option<Vec<Job>>> {
1180 let mut inner = self.inner.lock().await;
1181 let Some(parent) = inner.jobs.get(parent_id) else {
1182 return Ok(None);
1183 };
1184
1185 let mut ids = Vec::new();
1186 let mut seen = HashSet::new();
1187 collect_removable_unprocessed_children(&inner.jobs, &parent.child_ids, &mut ids, &mut seen);
1188
1189 let mut removed = Vec::with_capacity(ids.len());
1190 for id in ids {
1191 if let Some(job) = Self::remove_job_record_locked(&mut inner, &id) {
1192 emit_removed_event_locked(&mut inner, &job, now);
1193 removed.push(job);
1194 }
1195 }
1196 Self::release_parent_if_ready_locked(&mut inner, parent_id, now);
1197
1198 Ok(Some(removed))
1199 }
1200
1201 pub async fn remove_child_dependency(
1203 &self,
1204 child_id: &str,
1205 now: DateTime<Utc>,
1206 ) -> Result<bool> {
1207 let mut inner = self.inner.lock().await;
1208 let Some(child) = inner.jobs.get(child_id) else {
1209 return Err(LaneError::JobNotFound(child_id.to_string()));
1210 };
1211 let Some(parent_id) = child.parent_id.clone() else {
1212 return Ok(false);
1213 };
1214 let Some(parent) = inner.jobs.get(&parent_id) else {
1215 return Err(LaneError::JobNotFound(parent_id));
1216 };
1217 let parent_lists_child = parent.child_ids.iter().any(|id| id == child_id);
1218 let parent_indexes_child =
1219 Self::has_child_dependency_side_index_locked(&inner, &parent_id, child_id);
1220 if !parent_lists_child && !parent_indexes_child {
1221 return Ok(false);
1222 }
1223
1224 if let Some(parent) = inner.jobs.get_mut(&parent_id) {
1225 parent.child_ids.retain(|id| id != child_id);
1226 }
1227 Self::clear_child_dependency_side_index_locked(&mut inner, &parent_id, child_id);
1228 if let Some(child) = inner.jobs.get_mut(child_id) {
1229 child.parent_id = None;
1230 }
1231 Self::release_parent_if_ready_locked(&mut inner, &parent_id, now);
1232 Ok(true)
1233 }
1234
1235 pub async fn get_state(&self, job_id: &str) -> Result<Option<JobState>> {
1237 let inner = self.inner.lock().await;
1238 Ok(inner.jobs.get(job_id).map(|job| job.state))
1239 }
1240
1241 pub async fn get_finished_result(&self, job_id: &str) -> Result<Option<JobFinishedResult>> {
1243 let inner = self.inner.lock().await;
1244 Ok(inner.jobs.get(job_id).map(job_finished_result))
1245 }
1246
1247 pub async fn remove(&self, job_id: &str) -> Result<Option<Job>> {
1249 let mut inner = self.inner.lock().await;
1250 let now = Utc::now();
1251 if let Some(job) = inner.jobs.get(job_id) {
1252 require_removable(job)?;
1253 }
1254 let removed = Self::remove_job_record_locked(&mut inner, job_id);
1255 if let Some(parent_id) = removed.as_ref().and_then(|job| job.parent_id.clone()) {
1256 Self::release_parent_if_ready_locked(&mut inner, &parent_id, now);
1257 }
1258 if let Some(removed) = &removed {
1259 emit_removed_event_locked(&mut inner, removed, now);
1260 }
1261 Ok(removed)
1262 }
1263
1264 pub async fn remove_deduplication_key(&self, deduplication_id: &str) -> Result<bool> {
1266 if deduplication_id.is_empty() {
1267 return Ok(false);
1268 }
1269
1270 let mut inner = self.inner.lock().await;
1271 let Some(owner) = find_active_deduplication_id(
1272 &inner.jobs,
1273 &inner.released_deduplication_owners,
1274 deduplication_id,
1275 Utc::now(),
1276 ) else {
1277 return Ok(false);
1278 };
1279 let owner_id = owner.id.clone();
1280 inner
1281 .released_deduplication_owners
1282 .insert((deduplication_id.to_string(), owner_id));
1283 inner.deduplication_next.remove(deduplication_id);
1284 inner.deduplication_next_flows.remove(deduplication_id);
1285 Ok(true)
1286 }
1287
1288 pub async fn get_deduplication_job_id(&self, deduplication_id: &str) -> Result<Option<JobId>> {
1290 if deduplication_id.is_empty() {
1291 return Ok(None);
1292 }
1293
1294 let inner = self.inner.lock().await;
1295 Ok(find_active_deduplication_id(
1296 &inner.jobs,
1297 &inner.released_deduplication_owners,
1298 deduplication_id,
1299 Utc::now(),
1300 )
1301 .map(|job| job.id.clone()))
1302 }
1303
1304 pub async fn list_repeats(&self) -> Result<Vec<JobRepeatEntry>> {
1306 let inner = self.inner.lock().await;
1307 let mut repeats = inner
1308 .jobs
1309 .values()
1310 .filter_map(repeat_entry)
1311 .collect::<Vec<_>>();
1312 repeats.sort_by(|a, b| a.key.cmp(&b.key).then_with(|| a.job_id.cmp(&b.job_id)));
1313 Ok(repeats)
1314 }
1315
1316 pub async fn get_repeat(&self, repeat_key: &str) -> Result<Option<JobRepeatEntry>> {
1318 Ok(self
1319 .list_repeats()
1320 .await?
1321 .into_iter()
1322 .find(|entry| entry.key == repeat_key))
1323 }
1324
1325 pub async fn count_repeats(&self) -> Result<usize> {
1327 Ok(self.list_repeats().await?.len())
1328 }
1329
1330 pub async fn list_repeats_page(&self, options: JobRepeatListOptions) -> Result<JobRepeatPage> {
1332 Ok(page_repeat_entries(self.list_repeats().await?, options))
1333 }
1334
1335 pub async fn upsert_repeat(&self, spec: JobSpec, now: DateTime<Utc>) -> Result<Job> {
1337 validate_job_options_at(&spec.options, now)?;
1338 if spec.options.repeat.is_none() {
1339 return Err(LaneError::ConfigError(
1340 "repeat options are required to upsert a repeat series".to_string(),
1341 ));
1342 }
1343
1344 let mut job = Job::new(
1345 self.queue.clone(),
1346 spec.name,
1347 spec.payload,
1348 spec.options,
1349 now,
1350 );
1351 let repeat_key = job
1352 .repeat_key
1353 .as_deref()
1354 .filter(|key| !key.is_empty())
1355 .ok_or_else(|| LaneError::ConfigError("repeat key must not be empty".to_string()))?
1356 .to_string();
1357
1358 let mut inner = self.inner.lock().await;
1359 let current_owner = find_active_repeat_key(&inner.jobs, &repeat_key).cloned();
1360 if let Some(owner) = ¤t_owner {
1361 require_removable(owner)?;
1362 if owner.parent_id.is_some() || !owner.child_ids.is_empty() {
1363 return Err(LaneError::JobStateConflict(format!(
1364 "cannot upsert repeat key `{repeat_key}`; current owner {} has flow dependencies",
1365 owner.id
1366 )));
1367 }
1368 }
1369
1370 if let Some(existing) = inner.jobs.get(&job.id) {
1371 if current_owner.as_ref().map(|owner| owner.id.as_str()) != Some(existing.id.as_str()) {
1372 return Err(LaneError::ConfigError(format!(
1373 "job id `{}` already exists",
1374 job.id
1375 )));
1376 }
1377 }
1378
1379 if let Some(deduplication_id) = job_deduplication_id(&job) {
1380 if let Some(existing) = find_active_deduplication_id(
1381 &inner.jobs,
1382 &inner.released_deduplication_owners,
1383 deduplication_id,
1384 now,
1385 ) {
1386 if current_owner.as_ref().map(|owner| owner.id.as_str())
1387 != Some(existing.id.as_str())
1388 {
1389 return Err(LaneError::JobStateConflict(format!(
1390 "cannot upsert repeat key `{repeat_key}`; deduplication id `{deduplication_id}` is active on job {}",
1391 existing.id
1392 )));
1393 }
1394 }
1395 }
1396
1397 if let Some(owner) = current_owner {
1398 Self::remove_job_record_locked(&mut inner, &owner.id);
1399 }
1400
1401 Self::forget_released_deduplication_owner_locked(&mut inner, &job);
1402 assign_waiting_order(&mut inner.sequence, &mut job);
1403 inner.jobs.insert(job.id.clone(), job.clone());
1404 Self::emit_job_created_events_locked(&mut inner, &job, now);
1405 Ok(job)
1406 }
1407
1408 pub async fn remove_repeat(&self, repeat_key: &str) -> Result<Option<Job>> {
1410 let mut inner = self.inner.lock().await;
1411 let Some(job_id) =
1412 find_active_repeat_key(&inner.jobs, repeat_key).map(|job| job.id.clone())
1413 else {
1414 return Ok(None);
1415 };
1416 if let Some(job) = inner.jobs.get(&job_id) {
1417 require_removable(job)?;
1418 }
1419 let removed = Self::remove_job_record_locked(&mut inner, &job_id);
1420 if let Some(parent_id) = removed.as_ref().and_then(|job| job.parent_id.clone()) {
1421 let now = Utc::now();
1422 Self::release_parent_if_ready_locked(&mut inner, &parent_id, now);
1423 if let Some(removed) = &removed {
1424 emit_removed_event_locked(&mut inner, removed, now);
1425 }
1426 } else if let Some(removed) = &removed {
1427 emit_removed_event_locked(&mut inner, removed, Utc::now());
1428 }
1429 Ok(removed)
1430 }
1431
1432 pub async fn promote(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job> {
1434 let mut inner = self.inner.lock().await;
1435 let current = inner
1436 .jobs
1437 .get(job_id)
1438 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1439 if current.state != JobState::Delayed {
1440 return Err(LaneError::JobStateConflict(format!(
1441 "cannot promote job {} from state {:?}",
1442 current.id, current.state
1443 )));
1444 }
1445 let enqueued_seq = next_waiting_sequence(&mut inner.sequence);
1446 let job = inner
1447 .jobs
1448 .get_mut(job_id)
1449 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1450 job.state = JobState::Waiting;
1451 job.scheduled_at = now;
1452 job.enqueued_seq = enqueued_seq;
1453 let job = job.clone();
1454 emit_event_locked(
1455 &mut inner,
1456 "waiting",
1457 Some(&job),
1458 Some(JobState::Delayed),
1459 now,
1460 BTreeMap::new(),
1461 );
1462 Ok(job)
1463 }
1464
1465 pub async fn reschedule(
1467 &self,
1468 job_id: &str,
1469 delay: Duration,
1470 now: DateTime<Utc>,
1471 ) -> Result<Job> {
1472 if delay.is_zero() {
1473 return Err(LaneError::ConfigError(
1474 "job delay must be greater than zero".to_string(),
1475 ));
1476 }
1477
1478 let mut inner = self.inner.lock().await;
1479 let job = inner
1480 .jobs
1481 .get_mut(job_id)
1482 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1483 if job.state != JobState::Delayed {
1484 return Err(LaneError::JobStateConflict(format!(
1485 "cannot reschedule job {} from state {:?}",
1486 job.id, job.state
1487 )));
1488 }
1489
1490 job.options.delay = Some(delay);
1491 job.scheduled_at = add_duration(now, delay);
1492 let scheduled_millis = job.scheduled_at.timestamp_millis();
1493 let job = job.clone();
1494 let mut fields = BTreeMap::new();
1495 fields.insert("delay".to_string(), Value::from(scheduled_millis));
1496 emit_event_locked(&mut inner, "delayed", Some(&job), None, now, fields);
1497 Ok(job)
1498 }
1499
1500 pub async fn retry(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job> {
1502 let mut inner = self.inner.lock().await;
1503 let current = inner
1504 .jobs
1505 .get(job_id)
1506 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1507 if !matches!(current.state, JobState::Failed | JobState::Completed) {
1508 return Err(LaneError::JobStateConflict(format!(
1509 "cannot retry job {} from state {:?}",
1510 current.id, current.state
1511 )));
1512 }
1513 let previous_state = current.state;
1514 let retry_deduplication_id = current
1515 .options
1516 .deduplication
1517 .as_ref()
1518 .map(|value| value.id.clone());
1519 let retry_repeat_key = current.repeat_key.clone();
1520 let parent_id = current.parent_id.clone();
1521
1522 if let Some(deduplication_id) = retry_deduplication_id.as_deref() {
1523 if let Some(existing) = find_active_deduplication_id_except(
1524 &inner.jobs,
1525 &inner.released_deduplication_owners,
1526 deduplication_id,
1527 job_id,
1528 now,
1529 ) {
1530 return Err(LaneError::JobStateConflict(format!(
1531 "cannot retry job {job_id}; deduplication id `{deduplication_id}` is active on job {}",
1532 existing.id
1533 )));
1534 }
1535 }
1536 if let Some(repeat_key) = retry_repeat_key.as_deref() {
1537 if let Some(existing) = find_active_repeat_key_except(&inner.jobs, repeat_key, job_id) {
1538 return Err(LaneError::JobStateConflict(format!(
1539 "cannot retry job {job_id}; repeat key `{repeat_key}` is active on job {}",
1540 existing.id
1541 )));
1542 }
1543 }
1544 if let Some(deduplication_id) = retry_deduplication_id {
1545 inner
1546 .released_deduplication_owners
1547 .remove(&(deduplication_id, job_id.to_string()));
1548 }
1549
1550 let enqueued_seq = next_waiting_sequence(&mut inner.sequence);
1551 let job = inner
1552 .jobs
1553 .get_mut(job_id)
1554 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1555 job.state = JobState::Waiting;
1556 job.scheduled_at = now;
1557 job.deduplication_expires_at = deduplication_expiration(&job.options, now);
1558 job.processed_at = None;
1559 job.finished_at = None;
1560 job.worker_id = None;
1561 job.lock_token = None;
1562 job.lease_expires_at = None;
1563 job.deferred_failure = None;
1564 job.failed_reason = None;
1565 job.return_value = None;
1566 job.enqueued_seq = enqueued_seq;
1567 let job = job.clone();
1568 emit_event_locked(
1569 &mut inner,
1570 "waiting",
1571 Some(&job),
1572 Some(previous_state),
1573 now,
1574 BTreeMap::new(),
1575 );
1576 if let Some(parent_id) = parent_id {
1577 Self::restore_parent_dependency_after_child_retry_locked(
1578 &mut inner, &parent_id, job_id, now,
1579 );
1580 }
1581 Ok(job)
1582 }
1583
1584 pub async fn renew(
1586 &self,
1587 job_id: &str,
1588 lock_token: &str,
1589 lease_for: Duration,
1590 now: DateTime<Utc>,
1591 ) -> Result<Job> {
1592 let mut inner = self.inner.lock().await;
1593 let job = inner
1594 .jobs
1595 .get_mut(job_id)
1596 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1597 require_active(job, "renew lease")?;
1598 require_lock_token(job, lock_token)?;
1599 job.lease_expires_at = Some(add_duration(now, lease_for));
1600 Ok(job.clone())
1601 }
1602
1603 pub async fn delay_active(
1605 &self,
1606 job_id: &str,
1607 lock_token: &str,
1608 delay: Duration,
1609 now: DateTime<Utc>,
1610 ) -> Result<Job> {
1611 let mut inner = self.inner.lock().await;
1612 let job = inner
1613 .jobs
1614 .get_mut(job_id)
1615 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1616 require_active(job, "delay active")?;
1617 require_lock_token(job, lock_token)?;
1618 job.state = JobState::Delayed;
1619 job.options.delay = Some(delay);
1620 job.scheduled_at = add_duration(now, delay);
1621 job.processed_at = None;
1622 job.worker_id = None;
1623 job.lock_token = None;
1624 job.lease_expires_at = None;
1625 job.deferred_failure = None;
1626 job.failed_reason = None;
1627 let scheduled_millis = job.scheduled_at.timestamp_millis();
1628 let job = job.clone();
1629 let mut fields = BTreeMap::new();
1630 fields.insert("delay".to_string(), Value::from(scheduled_millis));
1631 emit_event_locked(
1632 &mut inner,
1633 "delayed",
1634 Some(&job),
1635 Some(JobState::Active),
1636 now,
1637 fields,
1638 );
1639 Ok(job)
1640 }
1641
1642 pub async fn release_active(
1644 &self,
1645 job_id: &str,
1646 lock_token: &str,
1647 now: DateTime<Utc>,
1648 ) -> Result<Job> {
1649 let mut inner = self.inner.lock().await;
1650 {
1651 let job = inner
1652 .jobs
1653 .get(job_id)
1654 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1655 require_active(job, "release active")?;
1656 require_lock_token(job, lock_token)?;
1657 }
1658 let job = inner
1659 .jobs
1660 .get_mut(job_id)
1661 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1662 job.state = JobState::Waiting;
1663 job.scheduled_at = now;
1664 job.processed_at = None;
1665 job.worker_id = None;
1666 job.lock_token = None;
1667 job.lease_expires_at = None;
1668 job.deferred_failure = None;
1669 job.failed_reason = None;
1670 job.enqueued_seq = 0;
1671 let job = job.clone();
1672 emit_event_locked(
1673 &mut inner,
1674 "waiting",
1675 Some(&job),
1676 Some(JobState::Active),
1677 now,
1678 BTreeMap::new(),
1679 );
1680 Ok(job)
1681 }
1682
1683 pub async fn set_priority(&self, job_id: &str, priority: JobPriority) -> Result<Job> {
1685 self.set_priority_order(job_id, priority, None).await
1686 }
1687
1688 pub async fn set_priority_with_lifo(
1690 &self,
1691 job_id: &str,
1692 priority: JobPriority,
1693 lifo: bool,
1694 ) -> Result<Job> {
1695 self.set_priority_order(job_id, priority, Some(lifo)).await
1696 }
1697
1698 async fn set_priority_order(
1699 &self,
1700 job_id: &str,
1701 priority: JobPriority,
1702 lifo: Option<bool>,
1703 ) -> Result<Job> {
1704 validate_job_priority(priority)?;
1705
1706 let mut inner = self.inner.lock().await;
1707 let should_requeue = {
1708 let job = inner
1709 .jobs
1710 .get(job_id)
1711 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1712 job.state == JobState::Waiting
1713 };
1714 let enqueued_seq = should_requeue.then(|| next_waiting_sequence(&mut inner.sequence));
1715 let job = inner
1716 .jobs
1717 .get_mut(job_id)
1718 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1719 job.priority = priority;
1720 job.options.priority = priority;
1721 if let Some(enqueued_seq) = enqueued_seq {
1722 job.enqueued_seq = enqueued_seq;
1723 }
1724 if let Some(lifo) = lifo {
1725 job.options.lifo = lifo;
1726 }
1727 Ok(job.clone())
1728 }
1729
1730 pub async fn list(&self, options: JobListOptions) -> Result<JobListPage> {
1732 let inner = self.inner.lock().await;
1733 let states = options.selected_states();
1734 let mut jobs = inner
1735 .jobs
1736 .values()
1737 .filter(|job| states.contains(&job.state))
1738 .cloned()
1739 .collect::<Vec<_>>();
1740 jobs.sort_by(compare_list_order);
1741 if !options.ascending {
1742 jobs.reverse();
1743 }
1744
1745 let total = jobs.len();
1746 let start = options.offset.min(total);
1747 let end = start.saturating_add(options.limit).min(total);
1748 let jobs = if options.limit == 0 {
1749 Vec::new()
1750 } else {
1751 jobs[start..end].to_vec()
1752 };
1753
1754 Ok(JobListPage {
1755 jobs,
1756 total,
1757 offset: options.offset,
1758 limit: options.limit,
1759 })
1760 }
1761
1762 pub async fn counts_by_state(&self, states: &[JobState]) -> Result<Vec<JobStateCount>> {
1764 let states = unique_states(states);
1765 let inner = self.inner.lock().await;
1766 Ok(states
1767 .into_iter()
1768 .map(|state| {
1769 let count = inner.jobs.values().filter(|job| job.state == state).count();
1770 JobStateCount { state, count }
1771 })
1772 .collect())
1773 }
1774
1775 pub async fn counts_per_priority(
1777 &self,
1778 priorities: &[JobPriority],
1779 ) -> Result<Vec<JobPriorityCount>> {
1780 let priorities = unique_priorities(priorities);
1781 let inner = self.inner.lock().await;
1782 Ok(priorities
1783 .into_iter()
1784 .map(|priority| {
1785 let count = inner
1786 .jobs
1787 .values()
1788 .filter(|job| job.state == JobState::Waiting && job.priority == priority)
1789 .count();
1790 JobPriorityCount { priority, count }
1791 })
1792 .collect())
1793 }
1794
1795 pub async fn clean(
1797 &self,
1798 state: JobState,
1799 grace: Duration,
1800 limit: usize,
1801 now: DateTime<Utc>,
1802 ) -> Result<Vec<Job>> {
1803 if limit == 0 {
1804 return Ok(Vec::new());
1805 }
1806
1807 let cutoff = subtract_duration(now, grace);
1808 let mut inner = self.inner.lock().await;
1809 let mut jobs = inner
1810 .jobs
1811 .values()
1812 .filter(|job| {
1813 job.state == state
1814 && job_reference_time(job) <= cutoff
1815 && (state != JobState::Active || job.lock_token.is_none())
1816 })
1817 .cloned()
1818 .collect::<Vec<_>>();
1819 jobs.sort_by(|a, b| {
1820 job_reference_time(a)
1821 .cmp(&job_reference_time(b))
1822 .then_with(|| a.id.cmp(&b.id))
1823 });
1824 jobs.truncate(limit);
1825
1826 let parent_ids = jobs
1827 .iter()
1828 .filter_map(|job| job.parent_id.clone())
1829 .collect::<Vec<_>>();
1830
1831 for job in &jobs {
1832 Self::remove_job_record_locked(&mut inner, &job.id);
1833 }
1834 for parent_id in parent_ids {
1835 Self::release_parent_if_ready_locked(&mut inner, &parent_id, now);
1836 }
1837 emit_cleaned_event_locked(&mut inner, jobs.len(), now);
1838
1839 Ok(jobs)
1840 }
1841
1842 pub async fn drain(&self, include_delayed: bool) -> Result<Vec<Job>> {
1844 let mut inner = self.inner.lock().await;
1845 let mut jobs = inner
1846 .jobs
1847 .values()
1848 .filter(|job| {
1849 job.state == JobState::Waiting
1850 || (include_delayed
1851 && job.state == JobState::Delayed
1852 && !is_delayed_repeat_owner(&inner.jobs, job))
1853 })
1854 .cloned()
1855 .collect::<Vec<_>>();
1856 jobs.sort_by(compare_list_order);
1857
1858 let parent_ids = jobs
1859 .iter()
1860 .filter_map(|job| job.parent_id.clone())
1861 .collect::<Vec<_>>();
1862
1863 for job in &jobs {
1864 Self::remove_job_record_locked(&mut inner, &job.id);
1865 }
1866 for parent_id in parent_ids {
1867 Self::release_parent_if_ready_locked(&mut inner, &parent_id, Utc::now());
1868 }
1869
1870 Ok(jobs)
1871 }
1872
1873 pub async fn obliterate(&self, force: bool) -> Result<usize> {
1875 let mut inner = self.inner.lock().await;
1876 inner.paused = true;
1877 let active = inner.jobs.values().any(|job| job.state == JobState::Active);
1878 if active && !force {
1879 return Err(LaneError::JobStateConflict(
1880 "cannot obliterate queue with active jobs".to_string(),
1881 ));
1882 }
1883
1884 let removed = inner.jobs.len();
1885 inner.jobs.clear();
1886 inner.flow_dependencies.clear();
1887 inner.deduplication_next.clear();
1888 inner.deduplication_next_flows.clear();
1889 inner.released_deduplication_owners.clear();
1890 inner.paused = false;
1891 Ok(removed)
1892 }
1893
1894 pub async fn set_data(&self, job_id: &str, payload: Value) -> Result<Job> {
1896 let mut inner = self.inner.lock().await;
1897 let job = inner
1898 .jobs
1899 .get_mut(job_id)
1900 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1901 job.payload = payload;
1902 Ok(job.clone())
1903 }
1904
1905 pub async fn set_progress(&self, job_id: &str, progress: Value) -> Result<Job> {
1907 let mut inner = self.inner.lock().await;
1908 let job = inner
1909 .jobs
1910 .get_mut(job_id)
1911 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1912 job.progress = Some(progress.clone());
1913 let job = job.clone();
1914 let mut fields = BTreeMap::new();
1915 fields.insert("data".to_string(), progress);
1916 emit_event_locked(&mut inner, "progress", Some(&job), None, Utc::now(), fields);
1917 Ok(job)
1918 }
1919
1920 pub async fn save_stacktrace(
1922 &self,
1923 job_id: &str,
1924 stacktrace: Vec<String>,
1925 failed_reason: String,
1926 ) -> Result<Job> {
1927 let mut inner = self.inner.lock().await;
1928 let job = inner
1929 .jobs
1930 .get_mut(job_id)
1931 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1932 job.stacktrace = stacktrace;
1933 job.failed_reason = Some(failed_reason);
1934 Ok(job.clone())
1935 }
1936
1937 pub async fn log(
1939 &self,
1940 job_id: &str,
1941 line: String,
1942 keep: usize,
1943 now: DateTime<Utc>,
1944 ) -> Result<Job> {
1945 let mut inner = self.inner.lock().await;
1946 let job = inner
1947 .jobs
1948 .get_mut(job_id)
1949 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
1950 job.logs.push(JobLogEntry {
1951 timestamp: now,
1952 line,
1953 });
1954 if keep > 0 && job.logs.len() > keep {
1955 let remove_count = job.logs.len() - keep;
1956 job.logs.drain(0..remove_count);
1957 }
1958 Ok(job.clone())
1959 }
1960
1961 pub async fn get_logs(
1963 &self,
1964 job_id: &str,
1965 start: isize,
1966 end: isize,
1967 ascending: bool,
1968 ) -> Result<JobLogPage> {
1969 let inner = self.inner.lock().await;
1970 Ok(inner
1971 .jobs
1972 .get(job_id)
1973 .map(|job| log_page(&job.logs, start, end, ascending))
1974 .unwrap_or_else(|| JobLogPage {
1975 logs: Vec::new(),
1976 count: 0,
1977 }))
1978 }
1979
1980 pub async fn clear_logs(&self, job_id: &str, keep: usize) -> Result<JobLogPage> {
1982 let mut inner = self.inner.lock().await;
1983 let Some(job) = inner.jobs.get_mut(job_id) else {
1984 return Ok(JobLogPage {
1985 logs: Vec::new(),
1986 count: 0,
1987 });
1988 };
1989
1990 if keep == 0 {
1991 job.logs.clear();
1992 } else if job.logs.len() > keep {
1993 let remove_count = job.logs.len() - keep;
1994 job.logs.drain(0..remove_count);
1995 }
1996
1997 Ok(log_page(&job.logs, 0, -1, true))
1998 }
1999
2000 fn promote_due_locked(inner: &mut InMemoryJobQueueState, now: DateTime<Utc>) -> usize {
2001 let mut promoted = 0;
2002 let mut due_jobs = inner
2003 .jobs
2004 .values()
2005 .filter(|job| job.state == JobState::Delayed && job.scheduled_at <= now)
2006 .cloned()
2007 .collect::<Vec<_>>();
2008 due_jobs.sort_by(|a, b| {
2009 a.scheduled_at
2010 .cmp(&b.scheduled_at)
2011 .then_with(|| a.created_at.cmp(&b.created_at))
2012 .then_with(|| a.id.cmp(&b.id))
2013 });
2014 for due_job in due_jobs {
2015 let enqueued_seq = next_waiting_sequence(&mut inner.sequence);
2016 let promoted_job = if let Some(job) = inner.jobs.get_mut(&due_job.id) {
2017 job.state = JobState::Waiting;
2018 job.enqueued_seq = enqueued_seq;
2019 promoted += 1;
2020 Some(job.clone())
2021 } else {
2022 None
2023 };
2024 if let Some(job) = promoted_job {
2025 emit_event_locked(
2026 inner,
2027 "waiting",
2028 Some(&job),
2029 Some(JobState::Delayed),
2030 now,
2031 BTreeMap::new(),
2032 );
2033 }
2034 }
2035 promoted
2036 }
2037
2038 fn record_terminal_child_dependency_locked(
2039 inner: &mut InMemoryJobQueueState,
2040 parent_id: &str,
2041 child: &Job,
2042 ) {
2043 let Some(parent) = inner.jobs.get(parent_id) else {
2044 return;
2045 };
2046 if !parent
2047 .child_ids
2048 .iter()
2049 .any(|child_id| child_id == &child.id)
2050 {
2051 return;
2052 }
2053 match child.state {
2054 JobState::Completed => {
2055 Self::record_completed_child_dependency_locked(
2056 inner,
2057 parent_id,
2058 &child.id,
2059 child.return_value.clone().unwrap_or(Value::Null),
2060 );
2061 }
2062 JobState::Failed
2063 if child.options.ignore_dependency_on_failure
2064 || child.options.continue_parent_on_failure =>
2065 {
2066 Self::record_ignored_child_dependency_locked(
2067 inner,
2068 parent_id,
2069 &child.id,
2070 child.failed_reason.clone().unwrap_or_default(),
2071 );
2072 }
2073 JobState::Failed if child.options.fail_parent_on_failure => {
2074 Self::record_failed_child_dependency_locked(inner, parent_id, &child.id);
2075 }
2076 JobState::Failed if child.options.remove_dependency_on_failure => {
2077 Self::clear_child_dependency_side_index_locked(inner, parent_id, &child.id);
2078 }
2079 _ => {}
2080 }
2081 }
2082
2083 fn record_completed_child_dependency_locked(
2084 inner: &mut InMemoryJobQueueState,
2085 parent_id: &str,
2086 child_id: &str,
2087 value: Value,
2088 ) {
2089 let index = inner
2090 .flow_dependencies
2091 .entry(parent_id.to_string())
2092 .or_default();
2093 index.ignored.remove(child_id);
2094 index.failed.remove(child_id);
2095 index.processed.insert(child_id.to_string(), value);
2096 }
2097
2098 fn record_ignored_child_dependency_locked(
2099 inner: &mut InMemoryJobQueueState,
2100 parent_id: &str,
2101 child_id: &str,
2102 failed_reason: String,
2103 ) {
2104 let index = inner
2105 .flow_dependencies
2106 .entry(parent_id.to_string())
2107 .or_default();
2108 index.processed.remove(child_id);
2109 index.failed.remove(child_id);
2110 index.ignored.insert(child_id.to_string(), failed_reason);
2111 }
2112
2113 fn record_failed_child_dependency_locked(
2114 inner: &mut InMemoryJobQueueState,
2115 parent_id: &str,
2116 child_id: &str,
2117 ) {
2118 let index = inner
2119 .flow_dependencies
2120 .entry(parent_id.to_string())
2121 .or_default();
2122 index.processed.remove(child_id);
2123 index.ignored.remove(child_id);
2124 index.failed.insert(child_id.to_string());
2125 }
2126
2127 fn clear_child_dependency_side_index_locked(
2128 inner: &mut InMemoryJobQueueState,
2129 parent_id: &str,
2130 child_id: &str,
2131 ) -> bool {
2132 let Some(index) = inner.flow_dependencies.get_mut(parent_id) else {
2133 return false;
2134 };
2135 let removed = index.processed.remove(child_id).is_some()
2136 || index.ignored.remove(child_id).is_some()
2137 || index.failed.remove(child_id);
2138 let empty = index.is_empty();
2139 if empty {
2140 inner.flow_dependencies.remove(parent_id);
2141 }
2142 removed
2143 }
2144
2145 fn has_child_dependency_side_index_locked(
2146 inner: &InMemoryJobQueueState,
2147 parent_id: &str,
2148 child_id: &str,
2149 ) -> bool {
2150 inner
2151 .flow_dependencies
2152 .get(parent_id)
2153 .map(|index| {
2154 index.processed.contains_key(child_id)
2155 || index.ignored.contains_key(child_id)
2156 || index.failed.contains(child_id)
2157 })
2158 .unwrap_or(false)
2159 }
2160
2161 fn remove_job_record_locked(inner: &mut InMemoryJobQueueState, job_id: &str) -> Option<Job> {
2162 let removed = inner.jobs.remove(job_id)?;
2163 inner.flow_dependencies.remove(job_id);
2164 Self::forget_released_deduplication_owner_locked(inner, &removed);
2165 Some(removed)
2166 }
2167
2168 fn apply_finished_retention_locked(
2169 inner: &mut InMemoryJobQueueState,
2170 state: JobState,
2171 retention: &JobRetention,
2172 now: DateTime<Utc>,
2173 ) {
2174 if let Some(age) = retention.age {
2175 let cutoff = subtract_duration(now, age);
2176 let limit = retention.limit.unwrap_or(1_000);
2177 let mut expired = inner
2178 .jobs
2179 .values()
2180 .filter(|job| job.state == state)
2181 .filter_map(|job| Some((job.id.clone(), job.finished_at?)))
2182 .filter(|(_, finished_at)| *finished_at <= cutoff)
2183 .collect::<Vec<_>>();
2184 expired.sort_by(
2185 |(left_id, left_finished_at), (right_id, right_finished_at)| {
2186 right_finished_at
2187 .cmp(left_finished_at)
2188 .then_with(|| right_id.cmp(left_id))
2189 },
2190 );
2191 for (job_id, _) in expired.into_iter().take(limit) {
2192 Self::remove_job_record_locked(inner, &job_id);
2193 }
2194 }
2195
2196 if let Some(count) = retention.count {
2197 let mut retained = inner
2198 .jobs
2199 .values()
2200 .filter(|job| job.state == state)
2201 .filter_map(|job| Some((job.id.clone(), job.finished_at?)))
2202 .collect::<Vec<_>>();
2203 retained.sort_by(
2204 |(left_id, left_finished_at), (right_id, right_finished_at)| {
2205 right_finished_at
2206 .cmp(left_finished_at)
2207 .then_with(|| right_id.cmp(left_id))
2208 },
2209 );
2210 for (job_id, _) in retained.into_iter().skip(count) {
2211 Self::remove_job_record_locked(inner, &job_id);
2212 }
2213 }
2214 }
2215
2216 fn forget_released_deduplication_owner_locked(inner: &mut InMemoryJobQueueState, job: &Job) {
2217 if let Some(deduplication_id) = job_deduplication_id(job) {
2218 inner
2219 .released_deduplication_owners
2220 .remove(&(deduplication_id.to_string(), job.id.clone()));
2221 }
2222 }
2223
2224 fn store_deduplicated_next_locked(
2225 inner: &mut InMemoryJobQueueState,
2226 candidate: &Job,
2227 existing: &Job,
2228 ) -> bool {
2229 if !deduplication_stores_next_if_active(candidate, existing) {
2230 return false;
2231 }
2232 let Some(deduplication_id) = job_deduplication_id(candidate) else {
2233 return false;
2234 };
2235 inner
2236 .deduplication_next
2237 .insert(deduplication_id.to_string(), candidate.clone());
2238 inner.deduplication_next_flows.remove(deduplication_id);
2239 if let Some(owner) = inner.jobs.get_mut(&existing.id) {
2240 owner.deduplication_expires_at = None;
2241 }
2242 true
2243 }
2244
2245 fn store_deduplicated_next_flow_locked(
2246 inner: &mut InMemoryJobQueueState,
2247 candidate: &JobFlow,
2248 existing: &Job,
2249 ) -> bool {
2250 if !deduplication_stores_next_flow_if_active(candidate, existing) {
2251 return false;
2252 }
2253 let Some(deduplication_id) = job_deduplication_id(&candidate.parent) else {
2254 return false;
2255 };
2256 inner
2257 .deduplication_next_flows
2258 .insert(deduplication_id.to_string(), candidate.clone());
2259 inner.deduplication_next.remove(deduplication_id);
2260 if let Some(owner) = inner.jobs.get_mut(&existing.id) {
2261 owner.deduplication_expires_at = None;
2262 }
2263 true
2264 }
2265
2266 fn enqueue_deduplicated_next_locked(
2267 inner: &mut InMemoryJobQueueState,
2268 owner: &Job,
2269 now: DateTime<Utc>,
2270 ) -> Option<Job> {
2271 let deduplication_id = job_deduplication_id(owner)?;
2272 let mut next = inner.deduplication_next.remove(deduplication_id)?;
2273 let repeat_next_count = repeat_keep_last_next_count(owner, &next)?;
2274 prepare_deduplicated_next_job(&mut next, now);
2275 if let Some(next_count) = repeat_next_count {
2276 next.repeat_key = owner.repeat_key.clone();
2277 next.repeat_count = next_count;
2278 }
2279 if inner.jobs.contains_key(&next.id) {
2280 return None;
2281 }
2282 if let Some(parent_id) = next.parent_id.as_deref() {
2283 if let Some(parent) = inner.jobs.get_mut(parent_id) {
2284 if !parent.child_ids.iter().any(|child_id| child_id == &next.id) {
2285 parent.child_ids.push(next.id.clone());
2286 }
2287 }
2288 }
2289 Self::forget_released_deduplication_owner_locked(inner, &next);
2290 assign_waiting_order(&mut inner.sequence, &mut next);
2291 inner.jobs.insert(next.id.clone(), next.clone());
2292 Self::emit_job_created_events_locked(inner, &next, now);
2293 Some(next)
2294 }
2295
2296 fn enqueue_deduplicated_next_flow_locked(
2297 inner: &mut InMemoryJobQueueState,
2298 owner: &Job,
2299 now: DateTime<Utc>,
2300 ) -> Option<JobFlow> {
2301 let deduplication_id = job_deduplication_id(owner)?;
2302 let mut flow = inner.deduplication_next_flows.remove(deduplication_id)?;
2303 if inner.jobs.contains_key(&flow.parent.id)
2304 || flow
2305 .children
2306 .iter()
2307 .any(|child| inner.jobs.contains_key(&child.id))
2308 {
2309 return None;
2310 }
2311
2312 prepare_deduplicated_next_flow(&mut flow, now);
2313 Self::forget_released_deduplication_owner_locked(inner, &flow.parent);
2314 assign_waiting_order(&mut inner.sequence, &mut flow.parent);
2315 inner
2316 .jobs
2317 .insert(flow.parent.id.clone(), flow.parent.clone());
2318 Self::emit_job_created_events_locked(inner, &flow.parent, now);
2319 for child in &mut flow.children {
2320 Self::forget_released_deduplication_owner_locked(inner, child);
2321 assign_waiting_order(&mut inner.sequence, child);
2322 inner.jobs.insert(child.id.clone(), child.clone());
2323 Self::emit_job_created_events_locked(inner, child, now);
2324 }
2325 Some(flow)
2326 }
2327
2328 fn flow_for_existing_owner_locked(inner: &InMemoryJobQueueState, owner: &Job) -> JobFlow {
2329 let children = owner
2330 .child_ids
2331 .iter()
2332 .filter_map(|child_id| inner.jobs.get(child_id).cloned())
2333 .collect();
2334 JobFlow {
2335 parent: owner.clone(),
2336 children,
2337 }
2338 }
2339
2340 fn extend_deduplication_expiration_locked(
2341 jobs: &mut HashMap<JobId, Job>,
2342 candidate: &Job,
2343 existing_id: &str,
2344 now: DateTime<Utc>,
2345 ) -> bool {
2346 if !deduplication_extends_ttl(candidate) {
2347 return false;
2348 }
2349 let Some(existing) = jobs.get_mut(existing_id) else {
2350 return false;
2351 };
2352 existing.deduplication_expires_at = deduplication_expiration(&candidate.options, now);
2353 true
2354 }
2355
2356 fn release_parent_if_ready_locked(
2357 inner: &mut InMemoryJobQueueState,
2358 parent_id: &str,
2359 now: DateTime<Utc>,
2360 ) -> Option<Job> {
2361 let parent = inner.jobs.get(parent_id)?;
2362 if parent.state != JobState::WaitingChildren {
2363 return Some(parent.clone());
2364 }
2365
2366 let child_ids = parent.child_ids.clone();
2367 let mut child_failure = None;
2368 let mut deferred_child_failure = None;
2369 for child_id in &child_ids {
2370 let Some(child) = inner.jobs.get(child_id) else {
2371 continue;
2372 };
2373 match child.state {
2374 JobState::Completed => {}
2375 JobState::Failed if child.options.fail_parent_on_failure => {
2376 deferred_child_failure = Some(child.id.clone())
2377 }
2378 JobState::Failed if child_failure_releases_dependency(child) => {}
2379 JobState::Failed => {
2380 child_failure = Some((child.id.clone(), child.failed_reason.clone()))
2381 }
2382 _ => return Some(parent.clone()),
2383 }
2384 }
2385 if let Some(child_id) = deferred_child_failure {
2386 return Self::defer_parent_failure_after_child_failure_locked(
2387 inner, parent_id, &child_id, now,
2388 );
2389 }
2390 if let Some((child_id, reason)) = child_failure {
2391 return Self::fail_waiting_parent_locked(
2392 inner,
2393 parent_id,
2394 format!(
2395 "child job {child_id} failed: {}",
2396 reason.as_deref().unwrap_or("unknown error")
2397 ),
2398 now,
2399 );
2400 }
2401
2402 let parent_scheduled_at = inner.jobs.get(parent_id)?.scheduled_at;
2403 let parent_state = state_after_dependencies(parent_scheduled_at, now);
2404 let enqueued_seq =
2405 (parent_state == JobState::Waiting).then(|| next_waiting_sequence(&mut inner.sequence));
2406 let parent = inner.jobs.get_mut(parent_id)?;
2407 parent.state = parent_state;
2408 parent.processed_at = None;
2409 parent.finished_at = None;
2410 parent.worker_id = None;
2411 parent.lock_token = None;
2412 parent.lease_expires_at = None;
2413 parent.deferred_failure = None;
2414 parent.failed_reason = None;
2415 if let Some(enqueued_seq) = enqueued_seq {
2416 parent.enqueued_seq = enqueued_seq;
2417 }
2418 let parent = parent.clone();
2419 emit_event_locked(
2420 inner,
2421 job_event_for_state(parent.state),
2422 Some(&parent),
2423 Some(JobState::WaitingChildren),
2424 now,
2425 BTreeMap::new(),
2426 );
2427 Some(parent)
2428 }
2429
2430 fn validate_existing_flow_child_parent_locked(
2431 inner: &InMemoryJobQueueState,
2432 parent_id: &str,
2433 child_id: &str,
2434 ) -> Result<()> {
2435 let Some(child) = inner.jobs.get(child_id) else {
2436 return Ok(());
2437 };
2438 if let Some(existing_parent_id) = &child.parent_id {
2439 if existing_parent_id != parent_id && inner.jobs.contains_key(existing_parent_id) {
2440 return Err(LaneError::JobStateConflict(format!(
2441 "flow child id `{child_id}` already belongs to parent `{existing_parent_id}`"
2442 )));
2443 }
2444 }
2445 Ok(())
2446 }
2447
2448 fn fail_waiting_parent_locked(
2449 inner: &mut InMemoryJobQueueState,
2450 parent_id: &str,
2451 reason: String,
2452 now: DateTime<Utc>,
2453 ) -> Option<Job> {
2454 let parent = inner.jobs.get_mut(parent_id)?;
2455 if parent.state.is_terminal() {
2456 return Some(parent.clone());
2457 }
2458 parent.state = JobState::Failed;
2459 parent.finished_at = Some(now);
2460 parent.worker_id = None;
2461 parent.lock_token = None;
2462 parent.lease_expires_at = None;
2463 parent.deferred_failure = None;
2464 parent.failed_reason = Some(reason);
2465 let failed = parent.clone();
2466 if failed.options.removes_failed_immediately() {
2467 Self::remove_job_record_locked(inner, parent_id);
2468 } else if let Some(retention) = failed.options.failed_retention() {
2469 Self::apply_finished_retention_locked(inner, JobState::Failed, retention, now);
2470 }
2471 let mut fields = BTreeMap::new();
2472 if let Some(reason) = &failed.failed_reason {
2473 fields.insert("failedReason".to_string(), Value::String(reason.clone()));
2474 }
2475 emit_event_locked(
2476 inner,
2477 "failed",
2478 Some(&failed),
2479 Some(JobState::WaitingChildren),
2480 now,
2481 fields,
2482 );
2483 Some(failed)
2484 }
2485
2486 fn continue_parent_after_child_failure_locked(
2487 inner: &mut InMemoryJobQueueState,
2488 parent_id: &str,
2489 now: DateTime<Utc>,
2490 ) -> Option<Job> {
2491 let parent = inner.jobs.get(parent_id)?;
2492 if parent.state != JobState::WaitingChildren {
2493 return Some(parent.clone());
2494 }
2495
2496 let parent_scheduled_at = parent.scheduled_at;
2497 let parent_state = state_after_dependencies(parent_scheduled_at, now);
2498 let enqueued_seq =
2499 (parent_state == JobState::Waiting).then(|| next_waiting_sequence(&mut inner.sequence));
2500 let parent = inner.jobs.get_mut(parent_id)?;
2501 parent.state = parent_state;
2502 parent.processed_at = None;
2503 parent.finished_at = None;
2504 parent.worker_id = None;
2505 parent.lock_token = None;
2506 parent.lease_expires_at = None;
2507 parent.deferred_failure = None;
2508 parent.failed_reason = None;
2509 if let Some(enqueued_seq) = enqueued_seq {
2510 parent.enqueued_seq = enqueued_seq;
2511 }
2512 let parent = parent.clone();
2513 emit_event_locked(
2514 inner,
2515 job_event_for_state(parent.state),
2516 Some(&parent),
2517 Some(JobState::WaitingChildren),
2518 now,
2519 BTreeMap::new(),
2520 );
2521 Some(parent)
2522 }
2523
2524 fn defer_parent_failure_after_child_failure_locked(
2525 inner: &mut InMemoryJobQueueState,
2526 parent_id: &str,
2527 child_id: &str,
2528 now: DateTime<Utc>,
2529 ) -> Option<Job> {
2530 let parent = inner.jobs.get(parent_id)?;
2531 if parent.state.is_terminal() {
2532 return Some(parent.clone());
2533 }
2534
2535 let parent_scheduled_at = parent.scheduled_at;
2536 let parent_state = state_after_dependencies(parent_scheduled_at, now);
2537 let previous_state = parent.state;
2538 let enqueued_seq =
2539 (parent_state == JobState::Waiting).then(|| next_waiting_sequence(&mut inner.sequence));
2540 let parent = inner.jobs.get_mut(parent_id)?;
2541 parent.state = parent_state;
2542 parent.processed_at = None;
2543 parent.finished_at = None;
2544 parent.worker_id = None;
2545 parent.lock_token = None;
2546 parent.lease_expires_at = None;
2547 parent.deferred_failure = Some(format!("child job {child_id} failed"));
2548 parent.failed_reason = None;
2549 if let Some(enqueued_seq) = enqueued_seq {
2550 parent.enqueued_seq = enqueued_seq;
2551 }
2552 let parent = parent.clone();
2553 emit_event_locked(
2554 inner,
2555 job_event_for_state(parent.state),
2556 Some(&parent),
2557 Some(previous_state),
2558 now,
2559 BTreeMap::new(),
2560 );
2561 Some(parent)
2562 }
2563
2564 fn restore_parent_dependency_after_child_retry_locked(
2565 inner: &mut InMemoryJobQueueState,
2566 parent_id: &str,
2567 child_id: &str,
2568 now: DateTime<Utc>,
2569 ) -> Option<Job> {
2570 let parent = inner.jobs.get(parent_id)?;
2571 if parent.state.is_terminal() || parent.state == JobState::Active {
2572 return Some(parent.clone());
2573 }
2574 if !parent.child_ids.iter().any(|id| id == child_id) {
2575 return Some(parent.clone());
2576 }
2577
2578 let previous_state = parent.state;
2579 Self::clear_child_dependency_side_index_locked(inner, parent_id, child_id);
2580 let parent = inner.jobs.get_mut(parent_id)?;
2581 parent.state = JobState::WaitingChildren;
2582 parent.processed_at = None;
2583 parent.finished_at = None;
2584 parent.worker_id = None;
2585 parent.lock_token = None;
2586 parent.lease_expires_at = None;
2587 parent.deferred_failure = None;
2588 parent.failed_reason = None;
2589 let parent = parent.clone();
2590 if previous_state != JobState::WaitingChildren {
2591 emit_event_locked(
2592 inner,
2593 "waiting-children",
2594 Some(&parent),
2595 Some(previous_state),
2596 now,
2597 BTreeMap::new(),
2598 );
2599 }
2600 Some(parent)
2601 }
2602}
2603#[async_trait]
2604impl JobQueueBackend for InMemoryJobQueue {
2605 async fn add_job(&self, name: String, payload: Value, options: JobOptions) -> Result<Job> {
2606 self.add(name, payload, options).await
2607 }
2608
2609 async fn add_jobs(&self, jobs: Vec<JobSpec>, now: DateTime<Utc>) -> Result<Vec<Job>> {
2610 self.add_many_at(jobs, now).await
2611 }
2612
2613 async fn add_flow(
2614 &self,
2615 parent: JobSpec,
2616 children: Vec<JobSpec>,
2617 now: DateTime<Utc>,
2618 ) -> Result<JobFlow> {
2619 self.add_flow_at(parent, children, now).await
2620 }
2621
2622 async fn add_flow_children(
2623 &self,
2624 parent_id: &str,
2625 lock_token: &str,
2626 children: Vec<JobSpec>,
2627 now: DateTime<Utc>,
2628 ) -> Result<Vec<Job>> {
2629 self.add_flow_children_at(parent_id, lock_token, children, now)
2630 .await
2631 }
2632
2633 async fn get_flow_dependencies(&self, parent_id: &str) -> Result<Option<JobFlowDependencies>> {
2634 InMemoryJobQueue::get_flow_dependencies(self, parent_id).await
2635 }
2636
2637 async fn get_flow_dependency_counts(
2638 &self,
2639 parent_id: &str,
2640 ) -> Result<Option<JobFlowDependencyCounts>> {
2641 InMemoryJobQueue::get_flow_dependency_counts(self, parent_id).await
2642 }
2643
2644 async fn get_flow_dependency_selected_counts(
2645 &self,
2646 parent_id: &str,
2647 options: JobFlowDependencyCountOptions,
2648 ) -> Result<Option<JobFlowDependencySelectedCounts>> {
2649 InMemoryJobQueue::get_flow_dependency_selected_counts(self, parent_id, options).await
2650 }
2651
2652 async fn get_flow_dependency_values(
2653 &self,
2654 parent_id: &str,
2655 ) -> Result<Option<JobFlowDependencyValues>> {
2656 InMemoryJobQueue::get_flow_dependency_values(self, parent_id).await
2657 }
2658
2659 async fn get_flow_dependency_page(
2660 &self,
2661 parent_id: &str,
2662 options: JobFlowDependencyPageOptions,
2663 ) -> Result<Option<JobFlowDependencyPage>> {
2664 InMemoryJobQueue::get_flow_dependency_page(self, parent_id, options).await
2665 }
2666
2667 async fn get_flow_dependency_pages(
2668 &self,
2669 parent_id: &str,
2670 options: JobFlowDependencyPagesOptions,
2671 ) -> Result<Option<JobFlowDependencyPages>> {
2672 InMemoryJobQueue::get_flow_dependency_pages(self, parent_id, options).await
2673 }
2674
2675 async fn get_flow_children_values(
2676 &self,
2677 parent_id: &str,
2678 ) -> Result<Option<JobFlowChildValues>> {
2679 InMemoryJobQueue::get_flow_children_values(self, parent_id).await
2680 }
2681
2682 async fn get_flow_ignored_children_failures(
2683 &self,
2684 parent_id: &str,
2685 ) -> Result<Option<JobFlowIgnoredFailures>> {
2686 InMemoryJobQueue::get_flow_ignored_children_failures(self, parent_id).await
2687 }
2688
2689 async fn remove_unprocessed_children(
2690 &self,
2691 parent_id: &str,
2692 now: DateTime<Utc>,
2693 ) -> Result<Option<Vec<Job>>> {
2694 InMemoryJobQueue::remove_unprocessed_children(self, parent_id, now).await
2695 }
2696
2697 async fn remove_child_dependency(&self, child_id: &str, now: DateTime<Utc>) -> Result<bool> {
2698 InMemoryJobQueue::remove_child_dependency(self, child_id, now).await
2699 }
2700
2701 async fn claim_next(
2702 &self,
2703 worker_id: JobWorkerId,
2704 lease_for: Duration,
2705 now: DateTime<Utc>,
2706 ) -> Result<Option<Job>> {
2707 let mut inner = self.inner.lock().await;
2708 if inner.paused {
2709 return Ok(None);
2710 }
2711 Self::promote_due_locked(&mut inner, now);
2712
2713 let selected_id = inner
2714 .jobs
2715 .values()
2716 .filter(|job| job.state == JobState::Waiting)
2717 .min_by(compare_claim_order)
2718 .map(|job| job.id.clone());
2719
2720 let Some(job_id) = selected_id else {
2721 return Ok(None);
2722 };
2723
2724 let claimed = {
2725 let Some(job) = inner.jobs.get_mut(&job_id) else {
2726 return Ok(None);
2727 };
2728 job.state = JobState::Active;
2729 job.attempts_made = job.attempts_made.saturating_add(1);
2730 job.processed_at = Some(now);
2731 job.worker_id = Some(worker_id);
2732 job.lock_token = Some(Uuid::new_v4().to_string());
2733 job.lease_expires_at = Some(add_duration(now, lease_for));
2734 job.failed_reason = None;
2735 job.clone()
2736 };
2737 emit_event_locked(
2738 &mut inner,
2739 "active",
2740 Some(&claimed),
2741 Some(JobState::Waiting),
2742 now,
2743 BTreeMap::new(),
2744 );
2745 Ok(Some(claimed))
2746 }
2747
2748 async fn complete_job(
2749 &self,
2750 job_id: &str,
2751 lock_token: &str,
2752 value: Value,
2753 now: DateTime<Utc>,
2754 ) -> Result<Job> {
2755 let mut inner = self.inner.lock().await;
2756 {
2757 let job = inner
2758 .jobs
2759 .get(job_id)
2760 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
2761 require_active(job, "complete")?;
2762 require_lock_token(job, lock_token)?;
2763 ensure_flow_dependencies_are_resolved(
2764 job,
2765 &inner.jobs,
2766 inner.flow_dependencies.get(job_id),
2767 "complete",
2768 )?;
2769 }
2770 let completed = {
2771 let job = inner
2772 .jobs
2773 .get_mut(job_id)
2774 .ok_or_else(|| LaneError::JobNotFound(job_id.to_string()))?;
2775 job.state = JobState::Completed;
2776 job.finished_at = Some(now);
2777 job.worker_id = None;
2778 job.lock_token = None;
2779 job.lease_expires_at = None;
2780 job.deferred_failure = None;
2781 job.return_value = Some(value);
2782 job.clone()
2783 };
2784 if let Some(parent_id) = &completed.parent_id {
2785 Self::record_terminal_child_dependency_locked(&mut inner, parent_id, &completed);
2786 }
2787 if completed.options.removes_completed_immediately() {
2788 Self::remove_job_record_locked(&mut inner, job_id);
2789 } else if let Some(retention) = completed.options.completed_retention() {
2790 Self::apply_finished_retention_locked(&mut inner, JobState::Completed, retention, now);
2791 }
2792 let enqueued_deduplicated_next =
2793 Self::enqueue_deduplicated_next_locked(&mut inner, &completed, now).is_some()
2794 || Self::enqueue_deduplicated_next_flow_locked(&mut inner, &completed, now)
2795 .is_some();
2796 if !enqueued_deduplicated_next {
2797 if let Some(next_job) = next_repeat_job(&completed, now)? {
2798 let mut next_job = next_job;
2799 Self::forget_released_deduplication_owner_locked(&mut inner, &next_job);
2800 assign_waiting_order(&mut inner.sequence, &mut next_job);
2801 inner.jobs.insert(next_job.id.clone(), next_job.clone());
2802 Self::emit_job_created_events_locked(&mut inner, &next_job, now);
2803 }
2804 }
2805 if let Some(parent_id) = &completed.parent_id {
2806 Self::release_parent_if_ready_locked(&mut inner, parent_id, now);
2807 }
2808 let mut fields = BTreeMap::new();
2809 if let Some(value) = &completed.return_value {
2810 fields.insert("returnvalue".to_string(), value.clone());
2811 }
2812 emit_event_locked(
2813 &mut inner,
2814 "completed",
2815 Some(&completed),
2816 Some(JobState::Active),
2817 now,
2818 fields,
2819 );
2820 emit_drained_event_if_needed_locked(&mut inner, now);
2821 Ok(completed)
2822 }
2823
2824 async fn fail_job(
2825 &self,
2826 job_id: &str,
2827 lock_token: &str,
2828 error: String,
2829 now: DateTime<Utc>,
2830 ) -> Result<Job> {
2831 self.fail_active_job(job_id, lock_token, error, now, true)
2832 .await
2833 }
2834
2835 async fn fail_job_discarding_retry(
2836 &self,
2837 job_id: &str,
2838 lock_token: &str,
2839 error: String,
2840 now: DateTime<Utc>,
2841 ) -> Result<Job> {
2842 self.fail_active_job(job_id, lock_token, error, now, false)
2843 .await
2844 }
2845
2846 async fn renew_lease(
2847 &self,
2848 job_id: &str,
2849 lock_token: &str,
2850 lease_for: Duration,
2851 now: DateTime<Utc>,
2852 ) -> Result<Job> {
2853 self.renew(job_id, lock_token, lease_for, now).await
2854 }
2855
2856 async fn delay_active_job(
2857 &self,
2858 job_id: &str,
2859 lock_token: &str,
2860 delay: Duration,
2861 now: DateTime<Utc>,
2862 ) -> Result<Job> {
2863 self.delay_active(job_id, lock_token, delay, now).await
2864 }
2865
2866 async fn release_active_job(
2867 &self,
2868 job_id: &str,
2869 lock_token: &str,
2870 now: DateTime<Utc>,
2871 ) -> Result<Job> {
2872 self.release_active(job_id, lock_token, now).await
2873 }
2874
2875 async fn promote_job(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job> {
2876 self.promote(job_id, now).await
2877 }
2878
2879 async fn reschedule_job(
2880 &self,
2881 job_id: &str,
2882 delay: Duration,
2883 now: DateTime<Utc>,
2884 ) -> Result<Job> {
2885 self.reschedule(job_id, delay, now).await
2886 }
2887
2888 async fn retry_job(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job> {
2889 self.retry(job_id, now).await
2890 }
2891
2892 async fn update_priority(&self, job_id: &str, priority: JobPriority) -> Result<Job> {
2893 self.set_priority(job_id, priority).await
2894 }
2895
2896 async fn update_priority_with_lifo(
2897 &self,
2898 job_id: &str,
2899 priority: JobPriority,
2900 lifo: bool,
2901 ) -> Result<Job> {
2902 self.set_priority_with_lifo(job_id, priority, lifo).await
2903 }
2904
2905 async fn remove_job(&self, job_id: &str) -> Result<Option<Job>> {
2906 self.remove(job_id).await
2907 }
2908
2909 async fn remove_repeat(&self, repeat_key: &str) -> Result<Option<Job>> {
2910 InMemoryJobQueue::remove_repeat(self, repeat_key).await
2911 }
2912
2913 async fn remove_deduplication_key(&self, deduplication_id: &str) -> Result<bool> {
2914 InMemoryJobQueue::remove_deduplication_key(self, deduplication_id).await
2915 }
2916
2917 async fn get_deduplication_job_id(&self, deduplication_id: &str) -> Result<Option<JobId>> {
2918 InMemoryJobQueue::get_deduplication_job_id(self, deduplication_id).await
2919 }
2920
2921 async fn list_repeats(&self) -> Result<Vec<JobRepeatEntry>> {
2922 InMemoryJobQueue::list_repeats(self).await
2923 }
2924
2925 async fn upsert_repeat(&self, spec: JobSpec, now: DateTime<Utc>) -> Result<Job> {
2926 InMemoryJobQueue::upsert_repeat(self, spec, now).await
2927 }
2928
2929 async fn clean_jobs(
2930 &self,
2931 state: JobState,
2932 grace: Duration,
2933 limit: usize,
2934 now: DateTime<Utc>,
2935 ) -> Result<Vec<Job>> {
2936 self.clean(state, grace, limit, now).await
2937 }
2938
2939 async fn drain_jobs(&self, include_delayed: bool) -> Result<Vec<Job>> {
2940 self.drain(include_delayed).await
2941 }
2942
2943 async fn obliterate(&self, force: bool) -> Result<usize> {
2944 InMemoryJobQueue::obliterate(self, force).await
2945 }
2946
2947 async fn list_jobs(&self, options: JobListOptions) -> Result<JobListPage> {
2948 self.list(options).await
2949 }
2950
2951 async fn get_job_counts(&self, states: &[JobState]) -> Result<Vec<JobStateCount>> {
2952 self.counts_by_state(states).await
2953 }
2954
2955 async fn get_counts_per_priority(
2956 &self,
2957 priorities: &[JobPriority],
2958 ) -> Result<Vec<JobPriorityCount>> {
2959 self.counts_per_priority(priorities).await
2960 }
2961
2962 async fn update_data(&self, job_id: &str, payload: Value) -> Result<Job> {
2963 self.set_data(job_id, payload).await
2964 }
2965
2966 async fn update_progress(&self, job_id: &str, progress: Value) -> Result<Job> {
2967 self.set_progress(job_id, progress).await
2968 }
2969
2970 async fn save_stacktrace(
2971 &self,
2972 job_id: &str,
2973 stacktrace: Vec<String>,
2974 failed_reason: String,
2975 ) -> Result<Job> {
2976 InMemoryJobQueue::save_stacktrace(self, job_id, stacktrace, failed_reason).await
2977 }
2978
2979 async fn add_log(
2980 &self,
2981 job_id: &str,
2982 line: String,
2983 keep: usize,
2984 now: DateTime<Utc>,
2985 ) -> Result<Job> {
2986 self.log(job_id, line, keep, now).await
2987 }
2988
2989 async fn get_job_logs(
2990 &self,
2991 job_id: &str,
2992 start: isize,
2993 end: isize,
2994 ascending: bool,
2995 ) -> Result<JobLogPage> {
2996 self.get_logs(job_id, start, end, ascending).await
2997 }
2998
2999 async fn clear_job_logs(&self, job_id: &str, keep: usize) -> Result<JobLogPage> {
3000 self.clear_logs(job_id, keep).await
3001 }
3002
3003 async fn read_events(&self, start: &str, end: &str, limit: usize) -> Result<Vec<JobEvent>> {
3004 if limit == 0 {
3005 return Ok(Vec::new());
3006 }
3007 let inner = self.inner.lock().await;
3008 Ok(inner
3009 .events
3010 .iter()
3011 .filter(|event| event_in_range(event, start, end))
3012 .take(limit)
3013 .cloned()
3014 .collect())
3015 }
3016
3017 async fn trim_events(&self, max_len: usize) -> Result<usize> {
3018 let mut inner = self.inner.lock().await;
3019 Ok(trim_events_locked(&mut inner, max_len))
3020 }
3021
3022 async fn promote_due_jobs(&self, now: DateTime<Utc>) -> Result<usize> {
3023 let mut inner = self.inner.lock().await;
3024 Ok(Self::promote_due_locked(&mut inner, now))
3025 }
3026
3027 async fn recover_stalled_jobs(&self, now: DateTime<Utc>) -> Result<usize> {
3028 let mut inner = self.inner.lock().await;
3029 let mut recovered = 0;
3030 let mut remove_ids = Vec::new();
3031 let mut deferred_parent_failures = Vec::new();
3032 let mut continuing_failed_children = Vec::new();
3033 let mut dependency_releasing_failed_children = Vec::new();
3034 let mut failed_children = Vec::new();
3035 let mut terminal_failures = Vec::new();
3036 let mut recovered_events = Vec::new();
3037
3038 let mut stalled_jobs = inner
3039 .jobs
3040 .values()
3041 .filter(|job| {
3042 job.state == JobState::Active
3043 && matches!(job.lease_expires_at, Some(expires_at) if expires_at <= now)
3044 })
3045 .cloned()
3046 .collect::<Vec<_>>();
3047 stalled_jobs.sort_by(|a, b| {
3048 a.lease_expires_at
3049 .cmp(&b.lease_expires_at)
3050 .then_with(|| a.processed_at.cmp(&b.processed_at))
3051 .then_with(|| a.id.cmp(&b.id))
3052 });
3053
3054 for stalled_job in stalled_jobs {
3055 let id = stalled_job.id;
3056 let mut requeue = false;
3057 if let Some(job) = inner.jobs.get_mut(&id) {
3058 job.stalled_count = job.stalled_count.saturating_add(1);
3059 job.worker_id = None;
3060 job.lock_token = None;
3061 job.lease_expires_at = None;
3062 job.deferred_failure = None;
3063 job.failed_reason = Some("job stalled after worker lease expired".to_string());
3064 if job.stalled_count > job.options.max_stalled_count
3065 && !is_repeat_scheduler_job(job)
3066 {
3067 job.state = JobState::Failed;
3068 job.finished_at = Some(now);
3069 if let Some(parent_id) = &job.parent_id {
3070 if job.options.fail_parent_on_failure {
3071 deferred_parent_failures.push((parent_id.clone(), job.id.clone()));
3072 } else if job.options.continue_parent_on_failure {
3073 continuing_failed_children.push(parent_id.clone());
3074 } else if child_failure_releases_dependency(job) {
3075 dependency_releasing_failed_children.push(parent_id.clone());
3076 } else {
3077 failed_children.push((
3078 parent_id.clone(),
3079 job.id.clone(),
3080 job.failed_reason.clone(),
3081 ));
3082 }
3083 }
3084 let failed = job.clone();
3085 terminal_failures.push(failed.clone());
3086 recovered_events.push(failed);
3087 if job.options.removes_failed_immediately() {
3088 remove_ids.push(job.id.clone());
3089 }
3090 } else {
3091 job.state = JobState::Waiting;
3092 job.processed_at = None;
3093 requeue = true;
3094 }
3095 }
3096 if requeue {
3097 let enqueued_seq = next_waiting_sequence(&mut inner.sequence);
3098 if let Some(job) = inner.jobs.get_mut(&id) {
3099 job.enqueued_seq = enqueued_seq;
3100 recovered_events.push(job.clone());
3101 }
3102 }
3103 recovered += 1;
3104 }
3105
3106 for id in remove_ids {
3107 Self::remove_job_record_locked(&mut inner, &id);
3108 }
3109 for failed in terminal_failures {
3110 if let Some(parent_id) = &failed.parent_id {
3111 Self::record_terminal_child_dependency_locked(&mut inner, parent_id, &failed);
3112 }
3113 if Self::enqueue_deduplicated_next_locked(&mut inner, &failed, now).is_none() {
3114 Self::enqueue_deduplicated_next_flow_locked(&mut inner, &failed, now);
3115 }
3116 if let Some(retention) = failed.options.failed_retention() {
3117 Self::apply_finished_retention_locked(&mut inner, JobState::Failed, retention, now);
3118 }
3119 }
3120 for (parent_id, child_id) in deferred_parent_failures {
3121 Self::defer_parent_failure_after_child_failure_locked(
3122 &mut inner, &parent_id, &child_id, now,
3123 );
3124 }
3125 for parent_id in continuing_failed_children {
3126 Self::continue_parent_after_child_failure_locked(&mut inner, &parent_id, now);
3127 }
3128 for parent_id in dependency_releasing_failed_children {
3129 Self::release_parent_if_ready_locked(&mut inner, &parent_id, now);
3130 }
3131 for (parent_id, child_id, reason) in failed_children {
3132 Self::fail_waiting_parent_locked(
3133 &mut inner,
3134 &parent_id,
3135 format!(
3136 "child job {child_id} failed: {}",
3137 reason.as_deref().unwrap_or("unknown error")
3138 ),
3139 now,
3140 );
3141 }
3142 for job in recovered_events {
3143 let mut fields = BTreeMap::new();
3144 if let Some(reason) = &job.failed_reason {
3145 fields.insert("failedReason".to_string(), Value::String(reason.clone()));
3146 }
3147 emit_event_locked(
3148 &mut inner,
3149 "stalled",
3150 Some(&job),
3151 Some(JobState::Active),
3152 now,
3153 fields.clone(),
3154 );
3155 emit_event_locked(
3156 &mut inner,
3157 job_event_for_state(job.state),
3158 Some(&job),
3159 Some(JobState::Active),
3160 now,
3161 fields,
3162 );
3163 }
3164 Ok(recovered)
3165 }
3166
3167 async fn pause(&self) -> Result<()> {
3168 let mut inner = self.inner.lock().await;
3169 inner.paused = true;
3170 emit_event_locked(
3171 &mut inner,
3172 "paused",
3173 None,
3174 None,
3175 Utc::now(),
3176 BTreeMap::new(),
3177 );
3178 Ok(())
3179 }
3180
3181 async fn resume(&self) -> Result<()> {
3182 let mut inner = self.inner.lock().await;
3183 inner.paused = false;
3184 emit_event_locked(
3185 &mut inner,
3186 "resumed",
3187 None,
3188 None,
3189 Utc::now(),
3190 BTreeMap::new(),
3191 );
3192 Ok(())
3193 }
3194
3195 async fn is_paused(&self) -> Result<bool> {
3196 let inner = self.inner.lock().await;
3197 Ok(inner.paused)
3198 }
3199
3200 async fn get_job(&self, job_id: &str) -> Result<Option<Job>> {
3201 let inner = self.inner.lock().await;
3202 Ok(inner.jobs.get(job_id).cloned())
3203 }
3204
3205 async fn get_job_state(&self, job_id: &str) -> Result<Option<JobState>> {
3206 self.get_state(job_id).await
3207 }
3208
3209 async fn get_job_finished_result(&self, job_id: &str) -> Result<Option<JobFinishedResult>> {
3210 self.get_finished_result(job_id).await
3211 }
3212
3213 async fn stats(&self) -> Result<JobQueueStats> {
3214 let inner = self.inner.lock().await;
3215 let mut stats = JobQueueStats {
3216 total: inner.jobs.len(),
3217 paused: inner.paused,
3218 ..JobQueueStats::default()
3219 };
3220 for job in inner.jobs.values() {
3221 match job.state {
3222 JobState::Waiting => stats.waiting += 1,
3223 JobState::Delayed => stats.delayed += 1,
3224 JobState::Active => stats.active += 1,
3225 JobState::WaitingChildren => stats.waiting_children += 1,
3226 JobState::Completed => stats.completed += 1,
3227 JobState::Failed => stats.failed += 1,
3228 }
3229 }
3230 Ok(stats)
3231 }
3232}
3233fn compare_claim_order(a: &&Job, b: &&Job) -> Ordering {
3234 a.priority
3235 .cmp(&b.priority)
3236 .then_with(|| compare_waiting_order(a, b))
3237 .then_with(|| a.scheduled_at.cmp(&b.scheduled_at))
3238 .then_with(|| a.created_at.cmp(&b.created_at))
3239 .then_with(|| a.id.cmp(&b.id))
3240}
3241
3242fn emit_event_locked(
3243 inner: &mut InMemoryJobQueueState,
3244 event: impl Into<String>,
3245 job: Option<&Job>,
3246 prev: Option<JobState>,
3247 timestamp: DateTime<Utc>,
3248 fields: BTreeMap<String, Value>,
3249) {
3250 inner.event_sequence = inner.event_sequence.saturating_add(1);
3251 inner.events.push(JobEvent {
3252 id: format!("{}-0", inner.event_sequence),
3253 event: event.into(),
3254 timestamp,
3255 job_id: job.map(|job| job.id.clone()),
3256 prev,
3257 fields,
3258 });
3259 trim_events_locked(inner, DEFAULT_JOB_EVENT_RETENTION);
3260}
3261
3262fn emit_retries_exhausted_event_locked(
3263 inner: &mut InMemoryJobQueueState,
3264 job: &Job,
3265 timestamp: DateTime<Utc>,
3266) {
3267 let mut fields = BTreeMap::new();
3268 fields.insert(
3269 "attemptsMade".to_string(),
3270 Value::from(u64::from(job.attempts_made)),
3271 );
3272 emit_event_locked(
3273 inner,
3274 "retries-exhausted",
3275 Some(job),
3276 None,
3277 timestamp,
3278 fields,
3279 );
3280}
3281
3282fn emit_removed_event_locked(
3283 inner: &mut InMemoryJobQueueState,
3284 job: &Job,
3285 timestamp: DateTime<Utc>,
3286) {
3287 emit_event_locked(
3288 inner,
3289 "removed",
3290 Some(job),
3291 Some(job.state),
3292 timestamp,
3293 BTreeMap::new(),
3294 );
3295}
3296
3297fn emit_duplicated_event_locked(
3298 inner: &mut InMemoryJobQueueState,
3299 job: &Job,
3300 timestamp: DateTime<Utc>,
3301) {
3302 emit_event_locked(
3303 inner,
3304 "duplicated",
3305 Some(job),
3306 None,
3307 timestamp,
3308 BTreeMap::new(),
3309 );
3310}
3311
3312fn emit_cleaned_event_locked(
3313 inner: &mut InMemoryJobQueueState,
3314 count: usize,
3315 timestamp: DateTime<Utc>,
3316) {
3317 let mut fields = BTreeMap::new();
3318 fields.insert("count".to_string(), Value::from(count as u64));
3319 emit_event_locked(inner, "cleaned", None, None, timestamp, fields);
3320}
3321
3322fn emit_deduplicated_events_locked(
3323 inner: &mut InMemoryJobQueueState,
3324 owner: &Job,
3325 candidate: &Job,
3326 timestamp: DateTime<Utc>,
3327) {
3328 let Some(deduplication_id) = job_deduplication_id(candidate) else {
3329 return;
3330 };
3331
3332 emit_deduplication_events_locked(inner, owner, deduplication_id, &candidate.id, timestamp);
3333}
3334
3335fn emit_replaced_deduplicated_owner_events_locked(
3336 inner: &mut InMemoryJobQueueState,
3337 replacement: &Job,
3338 removed_owner: &Job,
3339 timestamp: DateTime<Utc>,
3340) {
3341 let Some(deduplication_id) = job_deduplication_id(replacement) else {
3342 return;
3343 };
3344
3345 emit_deduplication_events_locked(
3346 inner,
3347 replacement,
3348 deduplication_id,
3349 &removed_owner.id,
3350 timestamp,
3351 );
3352}
3353
3354fn emit_deduplication_events_locked(
3355 inner: &mut InMemoryJobQueueState,
3356 event_job: &Job,
3357 deduplication_id: &str,
3358 deduplicated_job_id: &str,
3359 timestamp: DateTime<Utc>,
3360) {
3361 let mut debounced_fields = BTreeMap::new();
3362 debounced_fields.insert(
3363 "debounceId".to_string(),
3364 Value::String(deduplication_id.to_string()),
3365 );
3366 emit_event_locked(
3367 inner,
3368 "debounced",
3369 Some(event_job),
3370 None,
3371 timestamp,
3372 debounced_fields,
3373 );
3374
3375 let mut deduplicated_fields = BTreeMap::new();
3376 deduplicated_fields.insert(
3377 "deduplicationId".to_string(),
3378 Value::String(deduplication_id.to_string()),
3379 );
3380 deduplicated_fields.insert(
3381 "deduplicatedJobId".to_string(),
3382 Value::String(deduplicated_job_id.to_string()),
3383 );
3384 emit_event_locked(
3385 inner,
3386 "deduplicated",
3387 Some(event_job),
3388 None,
3389 timestamp,
3390 deduplicated_fields,
3391 );
3392}
3393
3394fn emit_drained_event_if_needed_locked(
3395 inner: &mut InMemoryJobQueueState,
3396 timestamp: DateTime<Utc>,
3397) {
3398 let drained = inner
3399 .jobs
3400 .values()
3401 .all(|job| !matches!(job.state, JobState::Waiting | JobState::Active));
3402 if drained {
3403 emit_event_locked(inner, "drained", None, None, timestamp, BTreeMap::new());
3404 }
3405}
3406
3407fn trim_events_locked(inner: &mut InMemoryJobQueueState, max_len: usize) -> usize {
3408 if inner.events.len() <= max_len {
3409 return 0;
3410 }
3411 let remove_count = inner.events.len() - max_len;
3412 inner.events.drain(0..remove_count);
3413 remove_count
3414}
3415
3416fn event_in_range(event: &JobEvent, start: &str, end: &str) -> bool {
3417 let Some(event_id) = parse_stream_id(&event.id) else {
3418 return false;
3419 };
3420 let after_start = match start {
3421 "-" => true,
3422 other => match parse_stream_id(other) {
3423 Some(start_id) => event_id >= start_id,
3424 None => true,
3425 },
3426 };
3427 let before_end = match end {
3428 "+" => true,
3429 other => match parse_stream_id(other) {
3430 Some(end_id) => event_id <= end_id,
3431 None => true,
3432 },
3433 };
3434 after_start && before_end
3435}
3436
3437fn parse_stream_id(id: &str) -> Option<(i64, u64)> {
3438 let (millis, sequence) = id.split_once('-')?;
3439 Some((millis.parse().ok()?, sequence.parse().ok()?))
3440}
3441
3442fn job_event_for_state(state: JobState) -> &'static str {
3443 match state {
3444 JobState::Waiting => "waiting",
3445 JobState::Delayed => "delayed",
3446 JobState::Active => "active",
3447 JobState::WaitingChildren => "waiting-children",
3448 JobState::Completed => "completed",
3449 JobState::Failed => "failed",
3450 }
3451}
3452
3453fn assign_waiting_order(sequence: &mut u64, job: &mut Job) {
3454 if job.state == JobState::Waiting {
3455 job.enqueued_seq = next_waiting_sequence(sequence);
3456 }
3457}
3458
3459fn next_waiting_sequence(sequence: &mut u64) -> u64 {
3460 *sequence = sequence.saturating_add(1);
3461 *sequence
3462}
3463
3464fn compare_waiting_order(a: &Job, b: &Job) -> Ordering {
3465 match (a.enqueued_seq == 0, b.enqueued_seq == 0) {
3466 (true, true) => return a.id.cmp(&b.id),
3467 (true, false) => return Ordering::Less,
3468 (false, true) => return Ordering::Greater,
3469 (false, false) => {}
3470 }
3471
3472 match (a.options.lifo, b.options.lifo) {
3473 (true, true) => b.enqueued_seq.cmp(&a.enqueued_seq),
3474 (true, false) => Ordering::Less,
3475 (false, true) => Ordering::Greater,
3476 (false, false) => a.enqueued_seq.cmp(&b.enqueued_seq),
3477 }
3478}
3479
3480fn log_page(logs: &[JobLogEntry], start: isize, end: isize, ascending: bool) -> JobLogPage {
3481 let count = logs.len();
3482 let selected = if ascending {
3483 redis_range(logs, start, end)
3484 } else {
3485 let reverse_start = end.saturating_add(1).saturating_neg();
3486 let reverse_end = start.saturating_add(1).saturating_neg();
3487 let mut logs = redis_range(logs, reverse_start, reverse_end);
3488 logs.reverse();
3489 logs
3490 };
3491
3492 JobLogPage {
3493 logs: selected,
3494 count,
3495 }
3496}
3497
3498fn redis_range<T: Clone>(items: &[T], start: isize, end: isize) -> Vec<T> {
3499 let len = items.len();
3500 if len == 0 {
3501 return Vec::new();
3502 }
3503
3504 let start = normalize_redis_index(start, len);
3505 let end = normalize_redis_index(end, len);
3506 if start > end || start >= len {
3507 return Vec::new();
3508 }
3509 let end = end.min(len - 1);
3510 items[start..=end].to_vec()
3511}
3512
3513fn normalize_redis_index(index: isize, len: usize) -> usize {
3514 if index >= 0 {
3515 return index as usize;
3516 }
3517
3518 let normalized = len as isize + index;
3519 if normalized < 0 {
3520 0
3521 } else {
3522 normalized as usize
3523 }
3524}
3525
3526fn unique_priorities(priorities: &[JobPriority]) -> Vec<JobPriority> {
3527 let mut unique = Vec::new();
3528 for &priority in priorities {
3529 if !unique.contains(&priority) {
3530 unique.push(priority);
3531 }
3532 }
3533 unique
3534}
3535
3536fn unique_states(states: &[JobState]) -> Vec<JobState> {
3537 let states = if states.is_empty() {
3538 JobState::ALL.as_slice()
3539 } else {
3540 states
3541 };
3542 let mut unique = Vec::new();
3543 for &state in states {
3544 if !unique.contains(&state) {
3545 unique.push(state);
3546 }
3547 }
3548 unique
3549}
3550
3551fn compare_list_order(a: &Job, b: &Job) -> Ordering {
3552 state_rank(a.state)
3553 .cmp(&state_rank(b.state))
3554 .then_with(|| a.priority.cmp(&b.priority))
3555 .then_with(|| a.scheduled_at.cmp(&b.scheduled_at))
3556 .then_with(|| a.created_at.cmp(&b.created_at))
3557 .then_with(|| a.id.cmp(&b.id))
3558}
3559
3560fn state_rank(state: JobState) -> u8 {
3561 match state {
3562 JobState::Waiting => 0,
3563 JobState::Delayed => 1,
3564 JobState::Active => 2,
3565 JobState::WaitingChildren => 3,
3566 JobState::Completed => 4,
3567 JobState::Failed => 5,
3568 }
3569}
3570
3571fn sorted_released_deduplication_owners(
3572 released_owners: &HashSet<(String, JobId)>,
3573) -> Vec<(String, JobId)> {
3574 let mut owners = released_owners.iter().cloned().collect::<Vec<_>>();
3575 owners.sort();
3576 owners
3577}
3578
3579fn sorted_flow_dependency_indexes(
3580 indexes: &HashMap<JobId, InMemoryFlowDependencyIndex>,
3581) -> Vec<JobFlowDependencyIndex> {
3582 let mut indexes = indexes
3583 .iter()
3584 .filter(|(_, index)| !index.is_empty())
3585 .map(|(parent_id, index)| JobFlowDependencyIndex {
3586 parent_id: parent_id.clone(),
3587 processed: index.processed.clone(),
3588 ignored: index.ignored.clone(),
3589 failed: index.failed.iter().cloned().collect(),
3590 })
3591 .collect::<Vec<_>>();
3592 indexes.sort_by(|left, right| left.parent_id.cmp(&right.parent_id));
3593 indexes
3594}
3595
3596fn job_finished_result(job: &Job) -> JobFinishedResult {
3597 match job.state {
3598 JobState::Completed => JobFinishedResult::Completed {
3599 return_value: job.return_value.clone(),
3600 },
3601 JobState::Failed => JobFinishedResult::Failed {
3602 failed_reason: job.failed_reason.clone(),
3603 },
3604 _ => JobFinishedResult::NotFinished,
3605 }
3606}
3607
3608fn flow_dependency_counts(
3609 parent: &Job,
3610 jobs: &HashMap<JobId, Job>,
3611 index: Option<&InMemoryFlowDependencyIndex>,
3612) -> JobFlowDependencyCounts {
3613 let mut counts = JobFlowDependencyCounts {
3614 processed: index.map(|index| index.processed.len()).unwrap_or(0),
3615 unprocessed: 0,
3616 failed: index.map(|index| index.failed.len()).unwrap_or(0),
3617 ignored: index.map(|index| index.ignored.len()).unwrap_or(0),
3618 missing: 0,
3619 };
3620
3621 for child_id in &parent.child_ids {
3622 if flow_dependency_index_contains(index, child_id) {
3623 continue;
3624 }
3625 match jobs.get(child_id) {
3626 Some(child) if child.state == JobState::Completed => counts.processed += 1,
3627 Some(child)
3628 if child.state == JobState::Failed
3629 && (child.options.ignore_dependency_on_failure
3630 || child.options.continue_parent_on_failure) =>
3631 {
3632 counts.ignored += 1
3633 }
3634 Some(child)
3635 if child.state == JobState::Failed
3636 && child.options.remove_dependency_on_failure => {}
3637 Some(child) if child.state == JobState::Failed => counts.failed += 1,
3638 Some(_) => counts.unprocessed += 1,
3639 None => counts.missing += 1,
3640 }
3641 }
3642
3643 counts
3644}
3645
3646fn flow_dependency_selected_counts(
3647 parent: &Job,
3648 jobs: &HashMap<JobId, Job>,
3649 index: Option<&InMemoryFlowDependencyIndex>,
3650 options: JobFlowDependencyCountOptions,
3651) -> JobFlowDependencySelectedCounts {
3652 let counts = flow_dependency_counts(parent, jobs, index);
3653 let mut selected = JobFlowDependencySelectedCounts::default();
3654 for kind in options.selected() {
3655 let count = match kind {
3656 JobFlowDependencyKind::Processed => counts.processed,
3657 JobFlowDependencyKind::Unprocessed => counts.unprocessed,
3658 JobFlowDependencyKind::Ignored => counts.ignored,
3659 JobFlowDependencyKind::Failed => counts.failed,
3660 };
3661 selected.insert(kind, count);
3662 }
3663 selected
3664}
3665
3666fn flow_dependency_values(
3667 parent: &Job,
3668 jobs: &HashMap<JobId, Job>,
3669 index: Option<&InMemoryFlowDependencyIndex>,
3670) -> JobFlowDependencyValues {
3671 let mut values = JobFlowDependencyValues::default();
3672 let mut indexed_children = HashSet::new();
3673 for child_id in &parent.child_ids {
3674 if let Some(index) = index {
3675 if let Some(value) = index.processed.get(child_id) {
3676 values.processed.insert(child_id.clone(), value.clone());
3677 indexed_children.insert(child_id.clone());
3678 continue;
3679 }
3680 if let Some(failed_reason) = index.ignored.get(child_id) {
3681 values
3682 .ignored
3683 .insert(child_id.clone(), failed_reason.clone());
3684 indexed_children.insert(child_id.clone());
3685 continue;
3686 }
3687 if index.failed.contains(child_id) {
3688 values.failed.push(child_id.clone());
3689 indexed_children.insert(child_id.clone());
3690 continue;
3691 }
3692 }
3693 if flow_dependency_index_contains(index, child_id) {
3694 continue;
3695 }
3696 let Some(child) = jobs.get(child_id) else {
3697 continue;
3698 };
3699 if child.state == JobState::Completed {
3700 if let Some(return_value) = &child.return_value {
3701 values
3702 .processed
3703 .insert(child.id.clone(), return_value.clone());
3704 }
3705 } else if !child.state.is_terminal() {
3706 values.unprocessed.push(child.id.clone());
3707 } else if child.state == JobState::Failed {
3708 if child.options.ignore_dependency_on_failure
3709 || child.options.continue_parent_on_failure
3710 {
3711 values.ignored.insert(
3712 child.id.clone(),
3713 child.failed_reason.clone().unwrap_or_default(),
3714 );
3715 } else if !child.options.remove_dependency_on_failure {
3716 values.failed.push(child.id.clone());
3717 }
3718 }
3719 }
3720 if let Some(index) = index {
3721 for (child_id, value) in &index.processed {
3722 if indexed_children.insert(child_id.clone()) {
3723 values.processed.insert(child_id.clone(), value.clone());
3724 }
3725 }
3726 for (child_id, failed_reason) in &index.ignored {
3727 if indexed_children.insert(child_id.clone()) {
3728 values
3729 .ignored
3730 .insert(child_id.clone(), failed_reason.clone());
3731 }
3732 }
3733 for child_id in &index.failed {
3734 if indexed_children.insert(child_id.clone()) {
3735 values.failed.push(child_id.clone());
3736 }
3737 }
3738 }
3739 values
3740}
3741
3742fn flow_dependency_page(
3743 parent: &Job,
3744 jobs: &HashMap<JobId, Job>,
3745 index: Option<&InMemoryFlowDependencyIndex>,
3746 options: JobFlowDependencyPageOptions,
3747) -> JobFlowDependencyPage {
3748 let bucket = flow_dependency_page_items(parent, jobs, index, options.kind);
3749
3750 let count = options.count.max(1);
3751 let start = (options.cursor as usize).min(bucket.len());
3752 let end = start.saturating_add(count).min(bucket.len());
3753 let items = bucket[start..end].to_vec();
3754 let next_cursor = if end < bucket.len() { end as u64 } else { 0 };
3755
3756 JobFlowDependencyPage {
3757 kind: options.kind,
3758 items,
3759 next_cursor,
3760 count,
3761 }
3762}
3763
3764fn flow_dependency_page_items(
3765 parent: &Job,
3766 jobs: &HashMap<JobId, Job>,
3767 index: Option<&InMemoryFlowDependencyIndex>,
3768 kind: JobFlowDependencyKind,
3769) -> Vec<JobFlowDependencyPageItem> {
3770 let mut bucket = Vec::new();
3771 let mut indexed_children = HashSet::new();
3772 for child_id in &parent.child_ids {
3773 if let Some(item) = indexed_flow_dependency_page_item(index, child_id, kind) {
3774 bucket.push(item);
3775 indexed_children.insert(child_id.clone());
3776 continue;
3777 }
3778 if flow_dependency_index_contains(index, child_id) {
3779 continue;
3780 }
3781 let Some(child) = jobs.get(child_id) else {
3782 continue;
3783 };
3784 if let Some(item) = retained_flow_dependency_page_item(child, kind) {
3785 bucket.push(item);
3786 }
3787 }
3788
3789 if let Some(index) = index {
3790 match kind {
3791 JobFlowDependencyKind::Processed => {
3792 for (child_id, value) in &index.processed {
3793 if indexed_children.insert(child_id.clone()) {
3794 bucket.push(JobFlowDependencyPageItem::Processed {
3795 child_id: child_id.clone(),
3796 value: value.clone(),
3797 });
3798 }
3799 }
3800 }
3801 JobFlowDependencyKind::Ignored => {
3802 for (child_id, failed_reason) in &index.ignored {
3803 if indexed_children.insert(child_id.clone()) {
3804 bucket.push(JobFlowDependencyPageItem::Ignored {
3805 child_id: child_id.clone(),
3806 failed_reason: failed_reason.clone(),
3807 });
3808 }
3809 }
3810 }
3811 JobFlowDependencyKind::Failed => {
3812 for child_id in &index.failed {
3813 if indexed_children.insert(child_id.clone()) {
3814 bucket.push(JobFlowDependencyPageItem::Failed {
3815 child_id: child_id.clone(),
3816 });
3817 }
3818 }
3819 }
3820 JobFlowDependencyKind::Unprocessed => {}
3821 }
3822 }
3823
3824 bucket
3825}
3826
3827fn indexed_flow_dependency_page_item(
3828 index: Option<&InMemoryFlowDependencyIndex>,
3829 child_id: &str,
3830 kind: JobFlowDependencyKind,
3831) -> Option<JobFlowDependencyPageItem> {
3832 let index = index?;
3833 match kind {
3834 JobFlowDependencyKind::Processed => {
3835 index
3836 .processed
3837 .get(child_id)
3838 .map(|value| JobFlowDependencyPageItem::Processed {
3839 child_id: child_id.to_string(),
3840 value: value.clone(),
3841 })
3842 }
3843 JobFlowDependencyKind::Ignored => {
3844 index
3845 .ignored
3846 .get(child_id)
3847 .map(|failed_reason| JobFlowDependencyPageItem::Ignored {
3848 child_id: child_id.to_string(),
3849 failed_reason: failed_reason.clone(),
3850 })
3851 }
3852 JobFlowDependencyKind::Failed if index.failed.contains(child_id) => {
3853 Some(JobFlowDependencyPageItem::Failed {
3854 child_id: child_id.to_string(),
3855 })
3856 }
3857 _ => None,
3858 }
3859}
3860
3861fn retained_flow_dependency_page_item(
3862 child: &Job,
3863 kind: JobFlowDependencyKind,
3864) -> Option<JobFlowDependencyPageItem> {
3865 match kind {
3866 JobFlowDependencyKind::Processed if child.state == JobState::Completed => child
3867 .return_value
3868 .as_ref()
3869 .map(|value| JobFlowDependencyPageItem::Processed {
3870 child_id: child.id.clone(),
3871 value: value.clone(),
3872 }),
3873 JobFlowDependencyKind::Unprocessed if !child.state.is_terminal() => {
3874 Some(JobFlowDependencyPageItem::Unprocessed {
3875 child_id: child.id.clone(),
3876 })
3877 }
3878 JobFlowDependencyKind::Ignored
3879 if child.state == JobState::Failed
3880 && (child.options.ignore_dependency_on_failure
3881 || child.options.continue_parent_on_failure) =>
3882 {
3883 Some(JobFlowDependencyPageItem::Ignored {
3884 child_id: child.id.clone(),
3885 failed_reason: child.failed_reason.clone().unwrap_or_default(),
3886 })
3887 }
3888 JobFlowDependencyKind::Failed
3889 if child.state == JobState::Failed
3890 && !child.options.ignore_dependency_on_failure
3891 && !child.options.continue_parent_on_failure
3892 && !child.options.remove_dependency_on_failure =>
3893 {
3894 Some(JobFlowDependencyPageItem::Failed {
3895 child_id: child.id.clone(),
3896 })
3897 }
3898 _ => None,
3899 }
3900}
3901
3902fn flow_dependency_pages(
3903 parent: &Job,
3904 jobs: &HashMap<JobId, Job>,
3905 index: Option<&InMemoryFlowDependencyIndex>,
3906 options: JobFlowDependencyPagesOptions,
3907) -> JobFlowDependencyPages {
3908 let mut pages = JobFlowDependencyPages::default();
3909 for page_options in options.selected() {
3910 pages.insert(flow_dependency_page(parent, jobs, index, page_options));
3911 }
3912 pages
3913}
3914
3915fn flow_children_values(
3916 parent: &Job,
3917 jobs: &HashMap<JobId, Job>,
3918 index: Option<&InMemoryFlowDependencyIndex>,
3919) -> JobFlowChildValues {
3920 let mut values = index
3921 .map(|index| index.processed.clone())
3922 .unwrap_or_default();
3923 for child_id in &parent.child_ids {
3924 if flow_dependency_index_contains(index, child_id) {
3925 continue;
3926 }
3927 let Some(child) = jobs.get(child_id) else {
3928 continue;
3929 };
3930 if child.state == JobState::Completed {
3931 if let Some(return_value) = &child.return_value {
3932 values.insert(child.id.clone(), return_value.clone());
3933 }
3934 }
3935 }
3936 values
3937}
3938
3939fn flow_ignored_children_failures(
3940 parent: &Job,
3941 jobs: &HashMap<JobId, Job>,
3942 index: Option<&InMemoryFlowDependencyIndex>,
3943) -> JobFlowIgnoredFailures {
3944 let mut failures = index.map(|index| index.ignored.clone()).unwrap_or_default();
3945 for child_id in &parent.child_ids {
3946 if flow_dependency_index_contains(index, child_id) {
3947 continue;
3948 }
3949 let Some(child) = jobs.get(child_id) else {
3950 continue;
3951 };
3952 if child.state == JobState::Failed
3953 && (child.options.ignore_dependency_on_failure
3954 || child.options.continue_parent_on_failure)
3955 {
3956 failures.insert(
3957 child.id.clone(),
3958 child.failed_reason.clone().unwrap_or_default(),
3959 );
3960 }
3961 }
3962 failures
3963}
3964
3965fn flow_dependency_index_contains(
3966 index: Option<&InMemoryFlowDependencyIndex>,
3967 child_id: &str,
3968) -> bool {
3969 index
3970 .map(|index| {
3971 index.processed.contains_key(child_id)
3972 || index.ignored.contains_key(child_id)
3973 || index.failed.contains(child_id)
3974 })
3975 .unwrap_or(false)
3976}
3977
3978fn child_failure_releases_dependency(child: &Job) -> bool {
3979 child.options.ignore_dependency_on_failure
3980 || child.options.remove_dependency_on_failure
3981 || child.options.continue_parent_on_failure
3982}
3983
3984fn collect_removable_unprocessed_children(
3985 jobs: &HashMap<JobId, Job>,
3986 child_ids: &[JobId],
3987 out: &mut Vec<JobId>,
3988 seen: &mut HashSet<JobId>,
3989) {
3990 for child_id in child_ids {
3991 if !seen.insert(child_id.clone()) {
3992 continue;
3993 }
3994 let Some(child) = jobs.get(child_id) else {
3995 continue;
3996 };
3997 if child.state == JobState::Active || child.state.is_terminal() {
3998 continue;
3999 }
4000 collect_removable_unprocessed_children(jobs, &child.child_ids, out, seen);
4001 out.push(child.id.clone());
4002 }
4003}
4004
4005fn active_deduplication_id(job: &Job, now: DateTime<Utc>) -> Option<&str> {
4006 if matches!(job.deduplication_expires_at, Some(expires_at) if expires_at <= now) {
4007 return None;
4008 }
4009 if job.state.is_terminal() && job.deduplication_expires_at.is_none() {
4010 return None;
4011 }
4012
4013 job.options
4014 .deduplication
4015 .as_ref()
4016 .map(|deduplication| deduplication.id.as_str())
4017}
4018
4019fn job_deduplication_id(job: &Job) -> Option<&str> {
4020 job.options
4021 .deduplication
4022 .as_ref()
4023 .map(|deduplication| deduplication.id.as_str())
4024}
4025
4026fn find_active_deduplicated_job<'a>(
4027 jobs: &'a HashMap<JobId, Job>,
4028 released_owners: &HashSet<(String, JobId)>,
4029 candidate: &Job,
4030 now: DateTime<Utc>,
4031) -> Option<&'a Job> {
4032 let deduplication_id = active_deduplication_id(candidate, now)?;
4033 find_active_deduplication_id(jobs, released_owners, deduplication_id, now)
4034}
4035
4036fn find_active_deduplication_id<'a>(
4037 jobs: &'a HashMap<JobId, Job>,
4038 released_owners: &HashSet<(String, JobId)>,
4039 deduplication_id: &str,
4040 now: DateTime<Utc>,
4041) -> Option<&'a Job> {
4042 jobs.values().find(|job| {
4043 active_deduplication_id(job, now) == Some(deduplication_id)
4044 && !deduplication_owner_released(released_owners, deduplication_id, &job.id)
4045 })
4046}
4047
4048fn find_active_deduplication_id_except<'a>(
4049 jobs: &'a HashMap<JobId, Job>,
4050 released_owners: &HashSet<(String, JobId)>,
4051 deduplication_id: &str,
4052 excluded_job_id: &str,
4053 now: DateTime<Utc>,
4054) -> Option<&'a Job> {
4055 jobs.values().find(|job| {
4056 job.id != excluded_job_id
4057 && active_deduplication_id(job, now) == Some(deduplication_id)
4058 && !deduplication_owner_released(released_owners, deduplication_id, &job.id)
4059 })
4060}
4061
4062fn deduplication_owner_released(
4063 released_owners: &HashSet<(String, JobId)>,
4064 deduplication_id: &str,
4065 job_id: &str,
4066) -> bool {
4067 released_owners.contains(&(deduplication_id.to_string(), job_id.to_string()))
4068}
4069
4070fn deduplication_replaces_delayed_owner(candidate: &Job, existing: &Job) -> bool {
4071 matches!(
4072 candidate
4073 .options
4074 .deduplication
4075 .as_ref()
4076 .map(|deduplication| deduplication.replace),
4077 Some(true)
4078 ) && existing.state == JobState::Delayed
4079 && candidate.repeat_key.is_none()
4080 && existing.parent_id.is_none()
4081 && existing.child_ids.is_empty()
4082 && existing.repeat_key.is_none()
4083}
4084
4085fn deduplication_stores_next_if_active(candidate: &Job, existing: &Job) -> bool {
4086 matches!(
4087 candidate
4088 .options
4089 .deduplication
4090 .as_ref()
4091 .map(|deduplication| deduplication.keep_last_if_active),
4092 Some(true)
4093 ) && existing.state == JobState::Active
4094 && candidate.child_ids.is_empty()
4095 && candidate.repeat_key.as_deref() == existing.repeat_key.as_deref()
4096}
4097
4098fn deduplication_duplicate_emits_events(candidate: &Job, existing: &Job) -> bool {
4099 let Some(deduplication) = candidate.options.deduplication.as_ref() else {
4100 return false;
4101 };
4102 !deduplication.replace || deduplication_stores_next_if_active(candidate, existing)
4103}
4104
4105fn deduplication_stores_next_flow_if_active(candidate: &JobFlow, existing: &Job) -> bool {
4106 matches!(
4107 candidate.parent.options.deduplication.as_ref(),
4108 Some(deduplication) if deduplication.keep_last_if_active
4109 ) && existing.state == JobState::Active
4110 && candidate.parent.parent_id.is_none()
4111 && candidate.parent.repeat_key.is_none()
4112 && existing.repeat_key.is_none()
4113}
4114
4115fn flow_child_deduplication_requires_next(candidate: &Job, existing: &Job) -> bool {
4116 matches!(
4117 candidate.options.deduplication.as_ref(),
4118 Some(deduplication) if deduplication.keep_last_if_active
4119 ) && existing.state == JobState::Active
4120}
4121
4122fn deduplication_extends_ttl(candidate: &Job) -> bool {
4123 matches!(
4124 candidate.options.deduplication.as_ref(),
4125 Some(deduplication)
4126 if deduplication.extend
4127 && deduplication.ttl.is_some()
4128 && !deduplication.keep_last_if_active
4129 )
4130}
4131
4132fn preserve_replacement_deduplication_expiration(candidate: &mut Job, existing: &Job) {
4133 let preserves_ttl = candidate
4134 .options
4135 .deduplication
4136 .as_ref()
4137 .and_then(|deduplication| deduplication.ttl)
4138 .is_some();
4139 if preserves_ttl {
4140 candidate.deduplication_expires_at = existing.deduplication_expires_at;
4141 }
4142}
4143
4144fn prepare_deduplicated_next_job(job: &mut Job, now: DateTime<Utc>) {
4145 job.created_at = now;
4146 job.scheduled_at = job
4147 .options
4148 .delay
4149 .map(|delay| add_duration(now, delay))
4150 .unwrap_or(now);
4151 job.state = state_after_dependencies(job.scheduled_at, now);
4152 job.attempts_made = 0;
4153 job.stalled_count = 0;
4154 job.processed_at = None;
4155 job.finished_at = None;
4156 job.worker_id = None;
4157 job.lock_token = None;
4158 job.lease_expires_at = None;
4159 job.deferred_failure = None;
4160 job.failed_reason = None;
4161 job.return_value = None;
4162 job.progress = None;
4163 job.logs.clear();
4164 job.deduplication_expires_at = deduplication_expiration(&job.options, now);
4165}
4166
4167fn prepare_deduplicated_next_flow(flow: &mut JobFlow, now: DateTime<Utc>) {
4168 prepare_deduplicated_next_job(&mut flow.parent, now);
4169 flow.parent.child_ids = flow
4170 .children
4171 .iter()
4172 .map(|child| child.id.clone())
4173 .collect::<Vec<_>>();
4174 if !flow.children.is_empty() {
4175 flow.parent.state = JobState::WaitingChildren;
4176 }
4177 for child in &mut flow.children {
4178 prepare_deduplicated_next_job(child, now);
4179 child.parent_id = Some(flow.parent.id.clone());
4180 child.child_ids.clear();
4181 }
4182}
4183
4184fn repeat_keep_last_next_count(owner: &Job, candidate: &Job) -> Option<Option<u32>> {
4185 let Some(owner_repeat_key) = owner.repeat_key.as_deref() else {
4186 return Some(None);
4187 };
4188 if candidate.repeat_key.as_deref() != Some(owner_repeat_key) {
4189 return None;
4190 }
4191
4192 let next_count = owner.repeat_count.saturating_add(1);
4193 if matches!(
4194 owner.options.repeat.as_ref().and_then(|repeat| repeat.limit),
4195 Some(limit) if next_count >= limit
4196 ) {
4197 return None;
4198 }
4199
4200 Some(Some(next_count))
4201}
4202
4203fn active_repeat_key(job: &Job) -> Option<&str> {
4204 if job.state.is_terminal() {
4205 return None;
4206 }
4207
4208 job.repeat_key.as_deref()
4209}
4210
4211fn is_repeat_scheduler_job(job: &Job) -> bool {
4212 active_repeat_key(job).is_some() && job.options.repeat.is_some()
4213}
4214
4215fn repeat_entry(job: &Job) -> Option<JobRepeatEntry> {
4216 let key = active_repeat_key(job)?.to_string();
4217 let options = job.options.repeat.clone()?;
4218 Some(JobRepeatEntry {
4219 key,
4220 job_id: job.id.clone(),
4221 name: job.name.clone(),
4222 state: job.state,
4223 scheduled_at: job.scheduled_at,
4224 repeat_count: job.repeat_count,
4225 options,
4226 })
4227}
4228
4229fn find_active_repeat_job<'a>(jobs: &'a HashMap<JobId, Job>, candidate: &Job) -> Option<&'a Job> {
4230 let repeat_key = active_repeat_key(candidate)?;
4231 find_active_repeat_key(jobs, repeat_key)
4232}
4233
4234fn find_active_repeat_key<'a>(jobs: &'a HashMap<JobId, Job>, repeat_key: &str) -> Option<&'a Job> {
4235 jobs.values()
4236 .find(|job| active_repeat_key(job) == Some(repeat_key))
4237}
4238
4239fn find_active_repeat_key_except<'a>(
4240 jobs: &'a HashMap<JobId, Job>,
4241 repeat_key: &str,
4242 excluded_job_id: &str,
4243) -> Option<&'a Job> {
4244 jobs.values()
4245 .find(|job| job.id != excluded_job_id && active_repeat_key(job) == Some(repeat_key))
4246}
4247
4248fn is_delayed_repeat_owner(jobs: &HashMap<JobId, Job>, job: &Job) -> bool {
4249 if job.state != JobState::Delayed {
4250 return false;
4251 }
4252 let Some(repeat_key) = job.repeat_key.as_deref() else {
4253 return false;
4254 };
4255 find_active_repeat_key(jobs, repeat_key).is_some_and(|owner| owner.id == job.id)
4256}
4257
4258fn state_after_dependencies(scheduled_at: DateTime<Utc>, now: DateTime<Utc>) -> JobState {
4259 if scheduled_at > now {
4260 JobState::Delayed
4261 } else {
4262 JobState::Waiting
4263 }
4264}
4265
4266fn validate_job_options(options: &JobOptions) -> Result<()> {
4267 options.validate()
4268}
4269
4270fn validate_job_options_at(options: &JobOptions, now: DateTime<Utc>) -> Result<()> {
4271 validate_job_options(options)?;
4272 if matches!(options.repeat.as_ref().and_then(|repeat| repeat.end_at), Some(end_at) if end_at < now)
4273 {
4274 return Err(LaneError::ConfigError(
4275 "repeat end_at must not be earlier than the add timestamp".to_string(),
4276 ));
4277 }
4278 Ok(())
4279}
4280
4281fn validate_flow_job_ids(parent: &Job, children: &[Job]) -> Result<()> {
4282 let mut ids = HashSet::with_capacity(children.len() + 1);
4283 for id in std::iter::once(&parent.id).chain(children.iter().map(|job| &job.id)) {
4284 if !ids.insert(id) {
4285 return Err(LaneError::ConfigError(format!(
4286 "flow contains duplicate job id `{id}`"
4287 )));
4288 }
4289 }
4290 Ok(())
4291}
4292
4293fn validate_flow_child_jobs(parent_id: &str, children: &[Job]) -> Result<()> {
4294 let mut ids = HashSet::with_capacity(children.len() + 1);
4295 ids.insert(parent_id);
4296 for child in children {
4297 if !ids.insert(child.id.as_str()) {
4298 return Err(LaneError::ConfigError(format!(
4299 "flow contains duplicate job id `{}`",
4300 child.id
4301 )));
4302 }
4303 }
4304 Ok(())
4305}
4306
4307fn next_repeat_job(job: &Job, now: DateTime<Utc>) -> Result<Option<Job>> {
4308 let Some(repeat) = job.options.repeat.as_ref() else {
4309 return Ok(None);
4310 };
4311 let next_count = job.repeat_count.saturating_add(1);
4312 if matches!(repeat.limit, Some(limit) if next_count >= limit) {
4313 return Ok(None);
4314 }
4315
4316 let Some(scheduled_at) = repeat.next_scheduled_at(now)? else {
4317 return Ok(None);
4318 };
4319 if matches!(repeat.end_at, Some(end_at) if scheduled_at > end_at) {
4320 return Ok(None);
4321 }
4322
4323 let mut options = job.options.clone();
4324 options.job_id = None;
4325 let mut next = Job::new(
4326 job.queue.clone(),
4327 job.name.clone(),
4328 job.payload.clone(),
4329 options,
4330 now,
4331 );
4332 next.scheduled_at = scheduled_at;
4333 next.state = state_after_dependencies(scheduled_at, now);
4334 next.repeat_key = job.repeat_key.clone();
4335 next.repeat_count = next_count;
4336 Ok(Some(next))
4337}
4338
4339fn require_active(job: &Job, action: &str) -> Result<()> {
4340 if job.state == JobState::Active {
4341 Ok(())
4342 } else {
4343 Err(LaneError::JobStateConflict(format!(
4344 "cannot {action} job {} from state {:?}",
4345 job.id, job.state
4346 )))
4347 }
4348}
4349
4350fn require_lock_token(job: &Job, lock_token: &str) -> Result<()> {
4351 if job.lock_token.as_deref() == Some(lock_token) {
4352 Ok(())
4353 } else {
4354 Err(LaneError::JobLeaseConflict(format!(
4355 "lock token does not own job {}",
4356 job.id
4357 )))
4358 }
4359}
4360
4361fn ensure_flow_dependencies_are_resolved(
4362 job: &Job,
4363 jobs: &HashMap<JobId, Job>,
4364 index: Option<&InMemoryFlowDependencyIndex>,
4365 action: &str,
4366) -> Result<()> {
4367 let counts = flow_dependency_counts(job, jobs, index);
4368 if counts.unprocessed > 0 {
4369 return Err(LaneError::JobStateConflict(format!(
4370 "cannot {action} job {}; it has {} pending flow dependencies",
4371 job.id, counts.unprocessed
4372 )));
4373 }
4374 if counts.failed > 0 {
4375 return Err(LaneError::JobStateConflict(format!(
4376 "cannot {action} job {}; it has {} failed flow dependencies",
4377 job.id, counts.failed
4378 )));
4379 }
4380 Ok(())
4381}
4382
4383fn ensure_flow_dependencies_have_not_failed(
4384 job: &Job,
4385 jobs: &HashMap<JobId, Job>,
4386 index: Option<&InMemoryFlowDependencyIndex>,
4387 action: &str,
4388) -> Result<()> {
4389 let counts = flow_dependency_counts(job, jobs, index);
4390 if counts.failed > 0 {
4391 return Err(LaneError::JobStateConflict(format!(
4392 "cannot {action} job {}; it has {} failed flow dependencies",
4393 job.id, counts.failed
4394 )));
4395 }
4396 Ok(())
4397}
4398
4399fn require_removable(job: &Job) -> Result<()> {
4400 if job.state == JobState::Active && job.lock_token.is_some() {
4401 Err(LaneError::JobLeaseConflict(format!(
4402 "cannot remove active leased job {}",
4403 job.id
4404 )))
4405 } else {
4406 Ok(())
4407 }
4408}
4409
4410fn job_reference_time(job: &Job) -> DateTime<Utc> {
4411 job.finished_at
4412 .or(job.processed_at)
4413 .unwrap_or(job.scheduled_at)
4414}
4415
4416fn should_retry(job: &Job) -> bool {
4417 job.options.retry_policy.max_retries > 0
4418 && job.attempts_made <= job.options.retry_policy.max_retries
4419}
4420
4421fn retries_exhausted(job: &Job) -> bool {
4422 job.state == JobState::Failed && job.attempts_made > job.options.retry_policy.max_retries
4423}
4424
4425fn add_duration(at: DateTime<Utc>, duration: Duration) -> DateTime<Utc> {
4426 match chrono::Duration::from_std(duration) {
4427 Ok(delta) => at.checked_add_signed(delta).unwrap_or(at),
4428 Err(_) => at,
4429 }
4430}
4431
4432fn subtract_duration(at: DateTime<Utc>, duration: Duration) -> DateTime<Utc> {
4433 match chrono::Duration::from_std(duration) {
4434 Ok(delta) => at.checked_sub_signed(delta).unwrap_or(at),
4435 Err(_) => at,
4436 }
4437}