Skip to main content

a3s_lane/job/
backend.rs

1use super::types::{
2    page_repeat_entries, Job, JobEvent, JobFinishedResult, JobFlow, JobFlowChildValues,
3    JobFlowDependencies, JobFlowDependencyCountOptions, JobFlowDependencyCounts,
4    JobFlowDependencyPage, JobFlowDependencyPageOptions, JobFlowDependencyPages,
5    JobFlowDependencyPagesOptions, JobFlowDependencySelectedCounts, JobFlowDependencyValues,
6    JobFlowIgnoredFailures, JobId, JobLeaseRenewal, JobListOptions, JobListPage, JobLogPage,
7    JobOptions, JobPriority, JobPriorityCount, JobQueueStats, JobRepeatEntry, JobRepeatListOptions,
8    JobRepeatPage, JobSpec, JobState, JobStateCount, JobWorkerId,
9};
10use crate::error::Result;
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde_json::Value;
14use std::time::Duration;
15
16/// Backend contract for a durable distributed job queue.
17#[async_trait]
18pub trait JobQueueBackend: Send + Sync {
19    async fn add_job(&self, name: String, payload: Value, options: JobOptions) -> Result<Job>;
20
21    /// Add multiple jobs, preserving input order and `add_job` idempotency semantics.
22    async fn add_jobs(&self, jobs: Vec<JobSpec>, now: DateTime<Utc>) -> Result<Vec<Job>>;
23
24    async fn add_flow(
25        &self,
26        parent: JobSpec,
27        children: Vec<JobSpec>,
28        now: DateTime<Utc>,
29    ) -> Result<JobFlow>;
30
31    /// Add children to an active parent and move that parent to `waiting_children`.
32    ///
33    /// This is the same-queue dynamic fan-out counterpart to BullMQ's
34    /// `moveToWaitingChildren()` flow path: the parent must be active and
35    /// token-owned, child jobs are added atomically, and the parent is parked
36    /// until its new dependencies resolve.
37    async fn add_flow_children(
38        &self,
39        parent_id: &str,
40        lock_token: &str,
41        children: Vec<JobSpec>,
42        now: DateTime<Utc>,
43    ) -> Result<Vec<Job>>;
44
45    async fn get_flow_dependencies(&self, parent_id: &str) -> Result<Option<JobFlowDependencies>>;
46
47    async fn get_flow_dependency_counts(
48        &self,
49        parent_id: &str,
50    ) -> Result<Option<JobFlowDependencyCounts>>;
51
52    /// Return selected BullMQ-style dependency counts for a flow parent.
53    async fn get_flow_dependency_selected_counts(
54        &self,
55        parent_id: &str,
56        options: JobFlowDependencyCountOptions,
57    ) -> Result<Option<JobFlowDependencySelectedCounts>>;
58
59    /// Return BullMQ-style full dependency buckets for a flow parent.
60    async fn get_flow_dependency_values(
61        &self,
62        parent_id: &str,
63    ) -> Result<Option<JobFlowDependencyValues>>;
64
65    /// Return one cursor page from a parent flow dependency bucket.
66    async fn get_flow_dependency_page(
67        &self,
68        parent_id: &str,
69        options: JobFlowDependencyPageOptions,
70    ) -> Result<Option<JobFlowDependencyPage>>;
71
72    /// Return cursor pages from several parent flow dependency buckets.
73    async fn get_flow_dependency_pages(
74        &self,
75        parent_id: &str,
76        options: JobFlowDependencyPagesOptions,
77    ) -> Result<Option<JobFlowDependencyPages>>;
78
79    /// Return completed child result values for a flow parent.
80    ///
81    /// This mirrors BullMQ's `getChildrenValues()` fan-in getter: only completed
82    /// children with retained return values are included.
83    async fn get_flow_children_values(&self, parent_id: &str)
84        -> Result<Option<JobFlowChildValues>>;
85
86    /// Return ignored child failure reasons for a flow parent.
87    ///
88    /// This mirrors BullMQ's `getIgnoredChildrenFailures()` getter: only failed
89    /// children configured with `ignoreDependencyOnFailure` are included.
90    async fn get_flow_ignored_children_failures(
91        &self,
92        parent_id: &str,
93    ) -> Result<Option<JobFlowIgnoredFailures>>;
94
95    async fn remove_unprocessed_children(
96        &self,
97        parent_id: &str,
98        now: DateTime<Utc>,
99    ) -> Result<Option<Vec<Job>>>;
100
101    async fn remove_child_dependency(&self, child_id: &str, now: DateTime<Utc>) -> Result<bool>;
102
103    async fn claim_next(
104        &self,
105        worker_id: JobWorkerId,
106        lease_for: Duration,
107        now: DateTime<Utc>,
108    ) -> Result<Option<Job>>;
109
110    /// Claim the next job, optionally waiting for backend-native work signals.
111    ///
112    /// Backends that do not have a blocking primitive fall back to one immediate
113    /// `claim_next()` attempt. Redis overrides this with its marker zset wait
114    /// path, where the blocking wake-up is only a signal and the actual claim
115    /// still runs through the normal atomic ownership script.
116    async fn claim_next_blocking(
117        &self,
118        worker_id: JobWorkerId,
119        lease_for: Duration,
120        _block_for: Duration,
121    ) -> Result<Option<Job>> {
122        self.claim_next(worker_id, lease_for, Utc::now()).await
123    }
124
125    async fn complete_job(
126        &self,
127        job_id: &str,
128        lock_token: &str,
129        value: Value,
130        now: DateTime<Utc>,
131    ) -> Result<Job>;
132
133    async fn fail_job(
134        &self,
135        job_id: &str,
136        lock_token: &str,
137        error: String,
138        now: DateTime<Utc>,
139    ) -> Result<Job>;
140
141    /// Fail an active job without applying its automatic retry policy.
142    ///
143    /// This mirrors BullMQ's runtime `discard()` behavior for the current failure
144    /// path: the job still must be active and token-owned, but the failure is
145    /// terminal even when retries remain.
146    async fn fail_job_discarding_retry(
147        &self,
148        job_id: &str,
149        lock_token: &str,
150        error: String,
151        now: DateTime<Utc>,
152    ) -> Result<Job>;
153
154    async fn renew_lease(
155        &self,
156        job_id: &str,
157        lock_token: &str,
158        lease_for: Duration,
159        now: DateTime<Utc>,
160    ) -> Result<Job>;
161
162    /// Renew multiple active job leases.
163    ///
164    /// This mirrors BullMQ's `extendLocks` script shape: valid token-owned
165    /// active jobs are renewed, and failures are returned as job ids instead of
166    /// aborting the whole batch. Backend transport/script failures still return
167    /// `Err`.
168    async fn renew_leases(
169        &self,
170        renewals: &[JobLeaseRenewal],
171        lease_for: Duration,
172        now: DateTime<Utc>,
173    ) -> Result<Vec<JobId>> {
174        let mut failed = Vec::new();
175        for renewal in renewals {
176            if self
177                .renew_lease(&renewal.job_id, &renewal.lock_token, lease_for, now)
178                .await
179                .is_err()
180            {
181                failed.push(renewal.job_id.clone());
182            }
183        }
184        Ok(failed)
185    }
186
187    async fn delay_active_job(
188        &self,
189        job_id: &str,
190        lock_token: &str,
191        delay: Duration,
192        now: DateTime<Utc>,
193    ) -> Result<Job>;
194
195    async fn release_active_job(
196        &self,
197        job_id: &str,
198        lock_token: &str,
199        now: DateTime<Utc>,
200    ) -> Result<Job>;
201
202    async fn promote_job(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job>;
203
204    async fn reschedule_job(
205        &self,
206        job_id: &str,
207        delay: Duration,
208        now: DateTime<Utc>,
209    ) -> Result<Job>;
210
211    /// Reprocess a retained failed or completed job by moving it back to waiting.
212    async fn retry_job(&self, job_id: &str, now: DateTime<Utc>) -> Result<Job>;
213
214    /// Update an existing job's stored priority.
215    ///
216    /// Waiting jobs are reinserted into the ready index. Retained terminal jobs
217    /// keep their terminal state and only update the stored snapshot, matching
218    /// BullMQ's `changePriority` script behavior.
219    async fn update_priority(&self, job_id: &str, priority: JobPriority) -> Result<Job>;
220
221    /// Update priority and choose how the job is reinserted within the same-priority group.
222    ///
223    /// This mirrors BullMQ's `changePriority({ priority, lifo })` shape: waiting
224    /// jobs get a fresh waiting index position, and `lifo = true` puts the job in
225    /// the LIFO side of that priority range.
226    async fn update_priority_with_lifo(
227        &self,
228        job_id: &str,
229        priority: JobPriority,
230        _lifo: bool,
231    ) -> Result<Job> {
232        self.update_priority(job_id, priority).await
233    }
234
235    async fn remove_job(&self, job_id: &str) -> Result<Option<Job>>;
236
237    async fn remove_repeat(&self, repeat_key: &str) -> Result<Option<Job>>;
238
239    async fn remove_deduplication_key(&self, deduplication_id: &str) -> Result<bool>;
240
241    async fn get_deduplication_job_id(&self, deduplication_id: &str) -> Result<Option<JobId>>;
242
243    async fn list_repeats(&self) -> Result<Vec<JobRepeatEntry>>;
244
245    /// Create or replace the current non-active occurrence for a repeat series.
246    ///
247    /// This follows BullMQ's `upsertJobScheduler(..., override: true)` shape at
248    /// the current Lane repeat-owner layer: the repeat key is taken from
249    /// `spec.options.repeat.key` or falls back to Lane's `queue:name` key,
250    /// non-active current owners are replaced, and active leased owners are
251    /// rejected.
252    async fn upsert_repeat(&self, spec: JobSpec, now: DateTime<Utc>) -> Result<Job>;
253
254    /// Return one repeat series / job scheduler by key.
255    async fn get_repeat(&self, repeat_key: &str) -> Result<Option<JobRepeatEntry>> {
256        Ok(self
257            .list_repeats()
258            .await?
259            .into_iter()
260            .find(|entry| entry.key == repeat_key))
261    }
262
263    /// Return the number of current repeat series / job schedulers.
264    async fn count_repeats(&self) -> Result<usize> {
265        Ok(self.list_repeats().await?.len())
266    }
267
268    /// Return repeat series / job schedulers with BullMQ-style pagination.
269    ///
270    /// Results are ordered by next scheduled time; descending order is the
271    /// default, matching BullMQ's `getJobSchedulers()`.
272    async fn list_repeats_page(&self, options: JobRepeatListOptions) -> Result<JobRepeatPage> {
273        Ok(page_repeat_entries(self.list_repeats().await?, options))
274    }
275
276    async fn clean_jobs(
277        &self,
278        state: JobState,
279        grace: Duration,
280        limit: usize,
281        now: DateTime<Utc>,
282    ) -> Result<Vec<Job>>;
283
284    async fn drain_jobs(&self, include_delayed: bool) -> Result<Vec<Job>>;
285
286    /// Remove all queue data.
287    ///
288    /// This follows BullMQ's `obliterate()` shape: the queue is paused first,
289    /// active jobs are rejected unless `force` is true, and a successful
290    /// obliteration removes the pause marker along with all queue data.
291    async fn obliterate(&self, force: bool) -> Result<usize>;
292
293    async fn list_jobs(&self, options: JobListOptions) -> Result<JobListPage>;
294
295    /// Return counts for the requested states.
296    ///
297    /// Empty input returns all lifecycle states. Duplicate states are counted once,
298    /// preserving the first requested order.
299    async fn get_job_counts(&self, states: &[JobState]) -> Result<Vec<JobStateCount>>;
300
301    /// Return the aggregate count for the requested states.
302    ///
303    /// This mirrors BullMQ's `getJobCountByTypes()` shape: it reuses per-state
304    /// counts, so empty input means all states and duplicate states are counted
305    /// once.
306    async fn get_job_count(&self, states: &[JobState]) -> Result<usize> {
307        let counts = self.get_job_counts(states).await?;
308        Ok(counts.into_iter().map(|count| count.count).sum())
309    }
310
311    /// Return jobs that are waiting to be processed.
312    ///
313    /// This follows BullMQ's queue `count()` meaning: waiting, delayed, and
314    /// waiting-children jobs are included; active and terminal jobs are not.
315    async fn count_pending_jobs(&self) -> Result<usize> {
316        self.get_job_count(JobState::PENDING.as_slice()).await
317    }
318
319    /// Return waiting-job counts for the requested priorities.
320    ///
321    /// Duplicate priorities are counted once, preserving the first requested order.
322    async fn get_counts_per_priority(
323        &self,
324        priorities: &[JobPriority],
325    ) -> Result<Vec<JobPriorityCount>>;
326
327    async fn update_data(&self, job_id: &str, payload: Value) -> Result<Job>;
328
329    /// Update an existing job's stored progress and emit a progress event.
330    ///
331    /// This mirrors BullMQ's `updateProgress` script behavior: any retained job
332    /// can be updated, including terminal jobs, and missing job records return
333    /// `JobNotFound`.
334    async fn update_progress(&self, job_id: &str, progress: Value) -> Result<Job>;
335
336    /// Save retained failure diagnostics for a job.
337    ///
338    /// This mirrors BullMQ's `saveStacktrace` script shape: any retained job can
339    /// be updated, the stacktrace replaces the previous retained stacktrace
340    /// array, and missing job records return `JobNotFound`.
341    async fn save_stacktrace(
342        &self,
343        job_id: &str,
344        stacktrace: Vec<String>,
345        failed_reason: String,
346    ) -> Result<Job>;
347
348    async fn add_log(
349        &self,
350        job_id: &str,
351        line: String,
352        keep: usize,
353        now: DateTime<Utc>,
354    ) -> Result<Job>;
355
356    async fn get_job_logs(
357        &self,
358        job_id: &str,
359        start: isize,
360        end: isize,
361        ascending: bool,
362    ) -> Result<JobLogPage>;
363
364    /// Clear retained job logs, optionally keeping the newest entries.
365    ///
366    /// This mirrors BullMQ's `clearLogs()` storage behavior: `keep == 0`
367    /// removes the log list, while positive values keep the newest `keep`
368    /// entries.
369    async fn clear_job_logs(&self, job_id: &str, keep: usize) -> Result<JobLogPage>;
370
371    /// Read retained queue events in stream-id order.
372    ///
373    /// `start` and `end` follow Redis stream range semantics for the supported
374    /// forms: `-`, `+`, or concrete `<milliseconds>-<sequence>` ids. `limit ==
375    /// 0` returns no events.
376    async fn read_events(&self, _start: &str, _end: &str, _limit: usize) -> Result<Vec<JobEvent>> {
377        Ok(Vec::new())
378    }
379
380    /// Trim retained queue events to approximately `max_len` entries.
381    async fn trim_events(&self, _max_len: usize) -> Result<usize> {
382        Ok(0)
383    }
384
385    async fn promote_due_jobs(&self, now: DateTime<Utc>) -> Result<usize>;
386
387    async fn recover_stalled_jobs(&self, now: DateTime<Utc>) -> Result<usize>;
388
389    async fn pause(&self) -> Result<()>;
390
391    async fn resume(&self) -> Result<()>;
392
393    /// Return whether this queue is currently paused.
394    async fn is_paused(&self) -> Result<bool>;
395
396    async fn get_job(&self, job_id: &str) -> Result<Option<Job>>;
397
398    async fn get_job_state(&self, job_id: &str) -> Result<Option<JobState>>;
399
400    /// Return whether a retained job has finished and include its terminal payload.
401    ///
402    /// This mirrors BullMQ's `isFinished(..., returnValue=true)` Redis shape at
403    /// the Lane type level: missing retained records return `None`, non-terminal
404    /// jobs return `NotFinished`, and terminal jobs return the retained success
405    /// value or failure reason.
406    async fn get_job_finished_result(&self, job_id: &str) -> Result<Option<JobFinishedResult>>;
407
408    async fn stats(&self) -> Result<JobQueueStats>;
409}