Skip to main content

a3s_lane/job/
memory.rs

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/// In-memory implementation of the generic job queue backend.
52///
53/// This backend is process-local. It is intentionally useful for development,
54/// tests, embedded runtimes, and as a reference implementation for Redis,
55/// Postgres, or NATS-backed implementations.
56#[derive(Debug, Clone)]
57pub struct InMemoryJobQueue {
58    queue: QueueName,
59    inner: Arc<Mutex<InMemoryJobQueueState>>,
60}
61
62impl InMemoryJobQueue {
63    /// Create an empty queue.
64    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    /// Restore an in-memory queue from a durable snapshot.
72    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    /// Queue name.
143    pub fn queue_name(&self) -> &str {
144        &self.queue
145    }
146
147    /// Capture the current queue state for durable storage.
148    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    /// Add a job using the current wall-clock time.
292    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    /// Add a job at an explicit timestamp. Primarily useful for deterministic tests.
302    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    /// Add multiple jobs using the current wall-clock time.
355    pub async fn add_many(&self, jobs: Vec<JobSpec>) -> Result<Vec<Job>> {
356        self.add_many_at(jobs, Utc::now()).await
357    }
358
359    /// Add multiple jobs at an explicit timestamp.
360    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    /// Add a parent-child flow. The parent remains blocked until every child is
431    /// completed, removed, or released by a terminal failure policy.
432    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    /// Add a parent-child flow at an explicit timestamp.
437    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    /// Add children to an active flow parent using the current wall-clock time.
799    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    /// Add children to an active flow parent and move the parent to waiting-children.
810    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    /// Return a parent flow's current child dependency snapshot.
1017    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    /// Return dependency counts for a flow parent.
1050    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    /// Return selected BullMQ-style dependency counts for a flow parent.
1067    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    /// Return BullMQ-style full dependency buckets for a flow parent.
1086    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    /// Return one cursor page from a parent flow dependency bucket.
1103    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    /// Return cursor pages from several parent flow dependency buckets.
1122    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    /// Return completed child result values for a flow parent.
1141    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    /// Return ignored child failure reasons for a flow parent.
1158    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    /// Remove children that are still unprocessed and not active.
1175    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    /// Remove a single child from its parent dependency list without deleting the child job.
1202    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    /// Return the current state for a job id.
1236    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    /// Return finished status and retained terminal payload for a job.
1242    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    /// Remove a job from the queue.
1248    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    /// Remove the active deduplication owner key, allowing a new owner to be added.
1265    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    /// Return the current active job id for a deduplication id.
1289    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    /// List current non-terminal repeat series owners.
1305    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    /// Return one repeat series / job scheduler by key.
1317    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    /// Return the number of current repeat series / job schedulers.
1326    pub async fn count_repeats(&self) -> Result<usize> {
1327        Ok(self.list_repeats().await?.len())
1328    }
1329
1330    /// Return repeat series / job schedulers ordered by next scheduled time.
1331    pub async fn list_repeats_page(&self, options: JobRepeatListOptions) -> Result<JobRepeatPage> {
1332        Ok(page_repeat_entries(self.list_repeats().await?, options))
1333    }
1334
1335    /// Create or replace the current non-active occurrence for a repeat series.
1336    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) = &current_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    /// Remove the current non-terminal occurrence for a repeat series.
1409    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    /// Promote a single delayed job to waiting.
1433    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    /// Reschedule a delayed job relative to `now`.
1466    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    /// Manually retry a failed job by moving it back to the waiting state.
1501    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    /// Renew the active worker lease for a job.
1585    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    /// Move an active leased job back to delayed state.
1604    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    /// Release an active leased job back to waiting state.
1643    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    /// Update an existing job priority.
1684    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    /// Update an existing job priority and waiting reinsert order.
1689    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    /// List jobs with deterministic pagination.
1731    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    /// Count jobs for requested lifecycle states.
1763    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    /// Count waiting jobs for each requested priority.
1776    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    /// Remove old jobs in a specific state and return their snapshots.
1796    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    /// Drain waiting jobs and optionally non-repeat delayed jobs.
1843    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    /// Remove every job and queue-owned metadata entry.
1874    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    /// Update the stored payload for an existing job.
1895    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    /// Update progress for an existing job.
1906    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    /// Save retained failure diagnostics for a job.
1921    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    /// Append a log line. `keep == 0` retains all log lines.
1938    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    /// Return retained log entries for a job.
1962    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    /// Clear retained log entries for a job. `keep == 0` clears all logs.
1981    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}