Skip to main content

bullmq/
queue.rs

1use std::collections::{HashMap, HashSet};
2use std::future::{Future, IntoFuture};
3use std::pin::Pin;
4use std::time::Duration;
5
6use serde::Serialize;
7use tracing::{debug, instrument};
8
9use crate::error::Error;
10use crate::job::{Job, ScriptContext};
11use crate::keys::{resolve_parent_queue_key, validate_queue_name, QueueKeys};
12use crate::options::{DeduplicationOptions, JobOptions, ParentOptions, QueueOptions};
13use crate::redis_connection::RedisConnection;
14use crate::types::{
15    BackoffStrategy, DependenciesCount, JobCounts, JobState, QueueMeta, RemoveOnFinish,
16};
17
18/// The version string stored in queue metadata for compatibility tracking.
19const BULLMQ_VERSION: &str = "bullmq-official:0.1.0";
20
21/// Maximum forward backfill iterations performed by the `getJobs` Lua script
22/// to replace skipped ids whose job hashes are missing in bounded ranges.
23const GET_JOBS_MAX_BACKFILL_ITERATIONS: usize = 5;
24
25/// A callback invoked once per `(state, count)` pair by
26/// [`Queue::record_job_counts_metric`]. This is the integration point for
27/// forwarding job-count gauges to a metrics/telemetry backend.
28pub type JobCountRecorder<'a> = &'a dyn Fn(&str, u64);
29
30/// A Queue is the main entry point for adding jobs to be processed.
31///
32/// It provides methods for adding single and bulk jobs, and managing
33/// queue state (pause, resume, drain, obliterate).
34#[derive(Clone)]
35pub struct Queue {
36    name: String,
37    keys: QueueKeys,
38    conn: RedisConnection,
39    default_job_options: JobOptions,
40}
41
42impl Queue {
43    fn validate_job_size(job: &Job, argv2: &str) -> Result<(), Error> {
44        if let Some(size_limit) = job.opts().size_limit {
45            if argv2.len() > size_limit {
46                return Err(Error::InvalidConfig(format!(
47                    "The size of job {} exceeds the limit {} bytes",
48                    job.name(),
49                    size_limit
50                )));
51            }
52        }
53
54        Ok(())
55    }
56
57    /// Create a new Queue with default options.
58    ///
59    /// Uses a default Redis connection (`redis://127.0.0.1:6379`) and the `bull`
60    /// key prefix. Use [`Queue::with_options`] to customize the connection,
61    /// prefix, or default job options.
62    ///
63    /// ```no_run
64    /// # async fn demo() -> bullmq::Result<()> {
65    /// use bullmq::Queue;
66    /// let queue = Queue::new("emails").await?;
67    /// # let _ = queue; Ok(()) }
68    /// ```
69    pub async fn new(name: &str) -> Result<Self, Error> {
70        Self::with_options(name, QueueOptions::default()).await
71    }
72
73    /// Create a new Queue with explicit options.
74    pub async fn with_options(name: &str, opts: QueueOptions) -> Result<Self, Error> {
75        validate_queue_name(name)?;
76        let conn = RedisConnection::new(&opts.connection).await?;
77        let keys = QueueKeys::new(name, Some(&opts.prefix));
78
79        let queue = Self {
80            name: name.to_string(),
81            keys,
82            conn,
83            default_job_options: opts.default_job_options,
84        };
85
86        queue.update_meta().await?;
87
88        Ok(queue)
89    }
90
91    /// Create a Queue with an existing Redis connection.
92    pub async fn with_connection(
93        name: &str,
94        conn: RedisConnection,
95        opts: QueueOptions,
96    ) -> Result<Self, Error> {
97        validate_queue_name(name)?;
98        let keys = QueueKeys::new(name, Some(&opts.prefix));
99
100        let queue = Self {
101            name: name.to_string(),
102            keys,
103            conn,
104            default_job_options: opts.default_job_options,
105        };
106
107        queue.update_meta().await?;
108
109        Ok(queue)
110    }
111
112    /// The queue name.
113    pub fn name(&self) -> &str {
114        &self.name
115    }
116
117    /// The queue keys helper.
118    pub fn keys(&self) -> &QueueKeys {
119        &self.keys
120    }
121
122    /// The underlying Redis connection.
123    pub fn connection(&self) -> &RedisConnection {
124        &self.conn
125    }
126
127    /// Add a job to the queue.
128    ///
129    /// Returns an [`AddJob`] builder that can be awaited directly, or customized
130    /// fluently before awaiting:
131    ///
132    /// ```no_run
133    /// # async fn demo(queue: bullmq::Queue) -> bullmq::Result<()> {
134    /// use std::time::Duration;
135    /// // simplest form:
136    /// queue.add("cars", serde_json::json!({ "color": "blue" })).await?;
137    /// // customized:
138    /// queue
139    ///     .add("cars", serde_json::json!({ "color": "blue" }))
140    ///     .delay(Duration::from_secs(30))
141    ///     .priority(5)
142    ///     .await?;
143    /// # Ok(()) }
144    /// ```
145    ///
146    /// `data` can be any [`Serialize`] type (your own struct, a
147    /// [`serde_json::Value`], …); it is serialized to JSON internally, so a
148    /// serialization failure surfaces when the builder is awaited.
149    pub fn add<T: Serialize>(&self, name: &str, data: T) -> AddJob<'_> {
150        AddJob::new(self, name, data)
151    }
152
153    /// Internal single-job add used by [`AddJob`].
154    #[instrument(skip(self, data, opts), fields(queue = %self.name))]
155    async fn add_internal(
156        &self,
157        name: &str,
158        data: serde_json::Value,
159        opts: Option<JobOptions>,
160    ) -> Result<Job, Error> {
161        let merged_opts = self.merge_job_options(opts);
162        let mut job = Job::new(name, data, merged_opts);
163        self.add_job(&mut job).await?;
164        job.set_context(self.make_script_context());
165        Ok(job)
166    }
167
168    /// Add multiple jobs to the queue in a single operation.
169    ///
170    /// Jobs are added concurrently over the multiplexed connection for maximum throughput.
171    #[instrument(skip(self, jobs), fields(queue = %self.name, count = jobs.len()))]
172    pub async fn add_bulk(&self, jobs: Vec<BulkJob>) -> Result<Vec<Job>, Error> {
173        if jobs.is_empty() {
174            return Ok(Vec::new());
175        }
176
177        let mut job_objects: Vec<Job> = jobs
178            .into_iter()
179            .map(|bulk| {
180                let BulkJob { name, data, opts } = bulk;
181                let merged = self.merge_job_options(Some(opts));
182                Ok(Job::new(&name, data?, merged))
183            })
184            .collect::<Result<_, Error>>()?;
185
186        let serialized_job_data: Vec<String> = job_objects
187            .iter()
188            .map(|job| {
189                let argv2 = serde_json::to_string(job.data())?;
190                Self::validate_job_size(job, &argv2)?;
191                Ok(argv2)
192            })
193            .collect::<Result<_, Error>>()?;
194
195        // Run all add_job calls concurrently since the connection is multiplexed
196        let futures: Vec<_> = job_objects
197            .iter()
198            .zip(serialized_job_data)
199            .map(|(job, argv2)| {
200                let delay = job.delay();
201                let priority = job.priority();
202                let timestamp = job.timestamp();
203                let custom_job_id = job.opts().job_id.as_deref().unwrap_or("").to_string();
204
205                let script_name = if delay > 0 {
206                    "addDelayedJob"
207                } else if priority > 0 {
208                    "addPrioritizedJob"
209                } else {
210                    "addStandardJob"
211                };
212
213                let script = self
214                    .conn
215                    .scripts()
216                    .get(script_name)
217                    .ok_or_else(|| {
218                        Error::InvalidConfig(format!("script '{}' not found", script_name))
219                    })
220                    .cloned();
221                let keys = self.add_job_keys(script_name);
222                let packed_args = self.pack_add_args(job, &custom_job_id, timestamp);
223                let argv3 = self.pack_job_opts(job);
224                let mut conn = self.conn.conn();
225
226                async move {
227                    let script = script?;
228                    let argv1 = packed_args?;
229                    let argv2_bytes = argv2.into_bytes();
230                    let args: Vec<&[u8]> = vec![&argv1, &argv2_bytes, &argv3];
231                    let result = script.execute(&mut conn, &keys, &args).await?;
232                    Self::parse_added_job_id(result)
233                }
234            })
235            .collect();
236
237        let results = futures::future::join_all(futures).await;
238
239        let ctx = self.make_script_context();
240        for (job, result) in job_objects.iter_mut().zip(results.into_iter()) {
241            let id = result?;
242            job.set_id(id);
243            job.set_context(ctx.clone());
244        }
245
246        Ok(job_objects)
247    }
248
249    /// Internal: add a single job via the appropriate Lua script.
250    async fn add_job(&self, job: &mut Job) -> Result<(), Error> {
251        let delay = job.delay();
252        let priority = job.priority();
253        let timestamp = job.timestamp();
254
255        let custom_job_id = job.opts().job_id.as_deref().unwrap_or("");
256
257        let script_name = if delay > 0 {
258            "addDelayedJob"
259        } else if priority > 0 {
260            "addPrioritizedJob"
261        } else {
262            "addStandardJob"
263        };
264
265        let script = self
266            .conn
267            .scripts()
268            .get(script_name)
269            .ok_or_else(|| Error::InvalidConfig(format!("script '{}' not found", script_name)))?
270            .clone();
271
272        // Build KEYS
273        let keys = self.add_job_keys(script_name);
274
275        // Build ARGV[1]: msgpack array of metadata
276        let argv1 = self.pack_add_args(job, custom_job_id, timestamp)?;
277
278        // Build ARGV[2]: JSON stringified job data
279        let argv2 = serde_json::to_string(job.data())?;
280
281        // Enforce sizeLimit client-side (matches Node.js `validateOptions`):
282        // reject jobs whose serialized data exceeds the configured byte limit.
283        Self::validate_job_size(job, &argv2)?;
284
285        // Build ARGV[3]: msgpack map of options
286        let argv3 = self.pack_job_opts(job);
287
288        let argv2_bytes = argv2.into_bytes();
289        let args: Vec<&[u8]> = vec![&argv1, &argv2_bytes, &argv3];
290
291        let mut conn = self.conn.conn();
292        let result = script.execute(&mut conn, &keys, &args).await?;
293
294        let returned_job_id = Self::parse_added_job_id(result)?;
295        job.set_id(returned_job_id);
296
297        debug!(job_id = %job.id(), name = %job.name(), "job added");
298        Ok(())
299    }
300
301    /// Merge per-job options with queue default job options.
302    /// Per-job options take precedence over defaults.
303    /// Create a ScriptContext for jobs returned by queue methods.
304    fn make_script_context(&self) -> ScriptContext {
305        let (progress_tx, _) = tokio::sync::broadcast::channel(1);
306        ScriptContext {
307            conn: self.conn.clone(),
308            keys: self.keys.clone(),
309            progress_tx,
310            token: String::new(),
311            lock_duration: 0,
312        }
313    }
314
315    fn merge_job_options(&self, opts: Option<JobOptions>) -> Option<JobOptions> {
316        let defaults = &self.default_job_options;
317        let is_default = defaults.attempts.is_none()
318            && defaults.backoff.is_none()
319            && defaults.remove_on_complete.is_none()
320            && defaults.remove_on_fail.is_none()
321            && defaults.delay.is_none()
322            && defaults.priority.is_none()
323            && defaults.lifo.is_none();
324
325        if is_default {
326            return opts;
327        }
328
329        let job_opts = opts.unwrap_or_default();
330        Some(JobOptions {
331            attempts: job_opts.attempts.or(defaults.attempts),
332            backoff: job_opts
333                .backoff
334                .clone()
335                .or_else(|| defaults.backoff.clone()),
336            remove_on_complete: job_opts
337                .remove_on_complete
338                .clone()
339                .or_else(|| defaults.remove_on_complete.clone()),
340            remove_on_fail: job_opts
341                .remove_on_fail
342                .clone()
343                .or_else(|| defaults.remove_on_fail.clone()),
344            delay: job_opts.delay.or(defaults.delay),
345            priority: job_opts.priority.or(defaults.priority),
346            lifo: job_opts.lifo.or(defaults.lifo),
347            job_id: job_opts.job_id,
348            ..job_opts
349        })
350    }
351
352    /// Build KEYS array for addStandardJob/addDelayedJob/addPrioritizedJob.
353    fn add_job_keys(&self, script_name: &str) -> Vec<String> {
354        match script_name {
355            "addStandardJob" => vec![
356                self.keys.wait(),
357                self.keys.paused(),
358                self.keys.meta(),
359                self.keys.id(),
360                self.keys.completed(),
361                self.keys.delayed(),
362                self.keys.active(),
363                self.keys.events(),
364                self.keys.marker(),
365            ],
366            "addDelayedJob" => vec![
367                self.keys.marker(),
368                self.keys.meta(),
369                self.keys.id(),
370                self.keys.delayed(),
371                self.keys.completed(),
372                self.keys.events(),
373            ],
374            "addPrioritizedJob" => vec![
375                self.keys.marker(),
376                self.keys.meta(),
377                self.keys.id(),
378                self.keys.prioritized(),
379                self.keys.delayed(),
380                self.keys.completed(),
381                self.keys.active(),
382                self.keys.events(),
383                self.keys.pc(),
384            ],
385            _ => vec![],
386        }
387    }
388
389    /// Pack ARGV[1]: msgpack array matching the Lua script contract.
390    ///
391    /// Positions: [key_prefix, job_id, name, timestamp, parentKey, parentDepsKey, parent, repeatJobKey, deduplicationKey]
392    fn pack_add_args(&self, job: &Job, job_id: &str, timestamp: u64) -> Result<Vec<u8>, Error> {
393        use rmp::encode::*;
394
395        let mut buf = Vec::with_capacity(128);
396        write_array_len(&mut buf, 9).unwrap();
397
398        // [1] key prefix (with trailing colon)
399        write_str(&mut buf, &self.keys.key_prefix()).unwrap();
400        // [2] job id
401        write_str(&mut buf, job_id).unwrap();
402        // [3] name
403        write_str(&mut buf, job.name()).unwrap();
404        // [4] timestamp
405        write_uint(&mut buf, timestamp).unwrap();
406
407        // [5] parentKey, [6] parent deps key, [7] parent
408        if let Some(ref parent) = job.opts().parent {
409            let parent_queue_key = resolve_parent_queue_key(self.keys.prefix(), &parent.queue)?;
410            let parent_key = format!("{}:{}", parent_queue_key, parent.id);
411            let parent_deps_key = format!("{}:dependencies", parent_key);
412            let opts = job.opts();
413            let mut parent_map_len = 2u32;
414            if opts.fail_parent_on_failure == Some(true) {
415                parent_map_len += 1;
416            }
417            if opts.ignore_dependency_on_failure == Some(true) {
418                parent_map_len += 1;
419            }
420            if opts.remove_dependency_on_failure == Some(true) {
421                parent_map_len += 1;
422            }
423            if opts.continue_parent_on_failure == Some(true) {
424                parent_map_len += 1;
425            }
426
427            write_str(&mut buf, &parent_key).unwrap();
428            write_str(&mut buf, &parent_deps_key).unwrap();
429            write_map_len(&mut buf, parent_map_len).unwrap();
430            write_str(&mut buf, "id").unwrap();
431            write_str(&mut buf, &parent.id).unwrap();
432            write_str(&mut buf, "queueKey").unwrap();
433            write_str(&mut buf, &parent_queue_key).unwrap();
434            if opts.fail_parent_on_failure == Some(true) {
435                write_str(&mut buf, "fpof").unwrap();
436                write_bool(&mut buf, true).unwrap();
437            }
438            if opts.ignore_dependency_on_failure == Some(true) {
439                write_str(&mut buf, "idof").unwrap();
440                write_bool(&mut buf, true).unwrap();
441            }
442            if opts.remove_dependency_on_failure == Some(true) {
443                write_str(&mut buf, "rdof").unwrap();
444                write_bool(&mut buf, true).unwrap();
445            }
446            if opts.continue_parent_on_failure == Some(true) {
447                write_str(&mut buf, "cpof").unwrap();
448                write_bool(&mut buf, true).unwrap();
449            }
450        } else {
451            write_nil(&mut buf).unwrap();
452            write_nil(&mut buf).unwrap();
453            write_nil(&mut buf).unwrap();
454        }
455
456        // [8] repeat job key - nil
457        write_nil(&mut buf).unwrap();
458        // [9] deduplication key
459        if let Some(ref dedup) = job.opts().deduplication {
460            let key = format!("{}:de:{}", self.keys.base(), dedup.id);
461            write_str(&mut buf, &key).unwrap();
462        } else {
463            write_nil(&mut buf).unwrap();
464        }
465
466        Ok(buf)
467    }
468
469    /// Pack ARGV[3]: msgpack map of job options using the raw script keys.
470    fn pack_job_opts(&self, job: &Job) -> Vec<u8> {
471        use rmp::encode::*;
472
473        let opts = job.opts();
474
475        // Collect entries
476        let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
477
478        if let Some(delay) = opts.delay {
479            if delay > 0 {
480                let mut b = Vec::new();
481                write_uint(&mut b, delay).unwrap();
482                entries.push(("delay", b));
483            }
484        }
485        if let Some(priority) = opts.priority {
486            if priority > 0 {
487                let mut b = Vec::new();
488                write_uint(&mut b, priority as u64).unwrap();
489                entries.push(("priority", b));
490            }
491        }
492        if let Some(attempts) = opts.attempts {
493            let mut b = Vec::new();
494            write_uint(&mut b, attempts as u64).unwrap();
495            entries.push(("attempts", b));
496        }
497        if let Some(true) = opts.lifo {
498            let mut b = Vec::new();
499            write_bool(&mut b, true).unwrap();
500            entries.push(("lifo", b));
501        }
502
503        if let Some(ref roc) = opts.remove_on_complete {
504            let b = Self::encode_remove_on_finish(roc);
505            entries.push(("removeOnComplete", b));
506        }
507
508        if let Some(ref rof) = opts.remove_on_fail {
509            let b = Self::encode_remove_on_finish(rof);
510            entries.push(("removeOnFail", b));
511        }
512
513        if let Some(ref backoff) = opts.backoff {
514            let b = Self::encode_backoff(backoff);
515            entries.push(("backoff", b));
516        }
517
518        if let Some(ref dedup) = opts.deduplication {
519            let b = Self::encode_deduplication(dedup);
520            entries.push(("de", b));
521        }
522
523        // Encode as msgpack map
524        let mut buf = Vec::with_capacity(64);
525        write_map_len(&mut buf, entries.len() as u32).unwrap();
526        for (key, val) in &entries {
527            write_str(&mut buf, key).unwrap();
528            buf.extend_from_slice(val);
529        }
530
531        buf
532    }
533
534    pub(crate) fn encode_remove_on_finish(rof: &crate::types::RemoveOnFinish) -> Vec<u8> {
535        use rmp::encode::*;
536        let mut b = Vec::new();
537        match rof {
538            crate::types::RemoveOnFinish::Bool(val) => {
539                write_bool(&mut b, *val).unwrap();
540            }
541            crate::types::RemoveOnFinish::Count(n) => {
542                write_uint(&mut b, *n as u64).unwrap();
543            }
544            crate::types::RemoveOnFinish::Options(keep) => {
545                let mut count = 0u32;
546                if keep.age.is_some() {
547                    count += 1;
548                }
549                if keep.count.is_some() {
550                    count += 1;
551                }
552                if keep.limit.is_some() {
553                    count += 1;
554                }
555                write_map_len(&mut b, count).unwrap();
556                if let Some(age) = keep.age {
557                    write_str(&mut b, "age").unwrap();
558                    write_uint(&mut b, age).unwrap();
559                }
560                if let Some(cnt) = keep.count {
561                    write_str(&mut b, "count").unwrap();
562                    write_uint(&mut b, cnt as u64).unwrap();
563                }
564                if let Some(limit) = keep.limit {
565                    write_str(&mut b, "limit").unwrap();
566                    write_uint(&mut b, limit as u64).unwrap();
567                }
568            }
569        }
570        b
571    }
572
573    pub(crate) fn encode_backoff(backoff: &crate::types::BackoffStrategy) -> Vec<u8> {
574        use rmp::encode::*;
575        let mut b = Vec::new();
576        match backoff {
577            crate::types::BackoffStrategy::Fixed(delay) => {
578                write_map_len(&mut b, 2).unwrap();
579                write_str(&mut b, "type").unwrap();
580                write_str(&mut b, "fixed").unwrap();
581                write_str(&mut b, "delay").unwrap();
582                write_uint(&mut b, *delay).unwrap();
583            }
584            crate::types::BackoffStrategy::Exponential(delay) => {
585                write_map_len(&mut b, 2).unwrap();
586                write_str(&mut b, "type").unwrap();
587                write_str(&mut b, "exponential").unwrap();
588                write_str(&mut b, "delay").unwrap();
589                write_uint(&mut b, *delay).unwrap();
590            }
591            crate::types::BackoffStrategy::Custom(name) => {
592                write_map_len(&mut b, 2).unwrap();
593                write_str(&mut b, "type").unwrap();
594                write_str(&mut b, name).unwrap();
595                write_str(&mut b, "delay").unwrap();
596                write_uint(&mut b, 0).unwrap();
597            }
598        }
599        b
600    }
601
602    pub(crate) fn encode_deduplication(dedup: &crate::options::DeduplicationOptions) -> Vec<u8> {
603        use rmp::encode::*;
604        let mut b = Vec::new();
605
606        let mut count = 1u32; // 'id' is always present
607        if dedup.ttl.is_some() {
608            count += 1;
609        }
610        if dedup.extend.is_some() {
611            count += 1;
612        }
613        if dedup.replace.is_some() {
614            count += 1;
615        }
616        if dedup.keep_last_if_active.is_some() {
617            count += 1;
618        }
619
620        write_map_len(&mut b, count).unwrap();
621        write_str(&mut b, "id").unwrap();
622        write_str(&mut b, &dedup.id).unwrap();
623
624        if let Some(ttl) = dedup.ttl {
625            write_str(&mut b, "ttl").unwrap();
626            write_uint(&mut b, ttl).unwrap();
627        }
628        if let Some(extend) = dedup.extend {
629            write_str(&mut b, "extend").unwrap();
630            write_bool(&mut b, extend).unwrap();
631        }
632        if let Some(replace) = dedup.replace {
633            write_str(&mut b, "replace").unwrap();
634            write_bool(&mut b, replace).unwrap();
635        }
636        if let Some(keep_last) = dedup.keep_last_if_active {
637            write_str(&mut b, "keepLastIfActive").unwrap();
638            write_bool(&mut b, keep_last).unwrap();
639        }
640
641        b
642    }
643
644    fn parse_added_job_id(result: redis::Value) -> Result<String, Error> {
645        match result {
646            redis::Value::BulkString(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()),
647            redis::Value::SimpleString(value) => Ok(value),
648            redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
649            redis::Value::Int(job_id) => Ok(job_id.to_string()),
650            value => Err(Error::InvalidConfig(format!(
651                "unexpected add job script result: {:?}",
652                value
653            ))),
654        }
655    }
656
657    /// Update queue metadata (version).
658    async fn update_meta(&self) -> Result<(), Error> {
659        let mut conn = self.conn.conn();
660        let meta_key = self.keys.meta();
661
662        redis::cmd("HSET")
663            .arg(&meta_key)
664            .arg("library")
665            .arg(BULLMQ_VERSION)
666            .query_async::<()>(&mut conn)
667            .await?;
668
669        Ok(())
670    }
671
672    /// Pause the queue.
673    #[instrument(skip(self), fields(queue = %self.name))]
674    pub async fn pause(&self) -> Result<(), Error> {
675        let script = self
676            .conn
677            .scripts()
678            .get("pause")
679            .ok_or_else(|| Error::InvalidConfig("pause script not found".to_string()))?
680            .clone();
681
682        let keys = vec![
683            self.keys.wait(),
684            self.keys.paused(),
685            self.keys.meta(),
686            self.keys.prioritized(),
687            self.keys.events(),
688            self.keys.delayed(),
689            self.keys.marker(),
690        ];
691
692        let args: Vec<&[u8]> = vec![b"paused"];
693
694        let mut conn = self.conn.conn();
695        script.execute(&mut conn, &keys, &args).await?;
696
697        debug!("queue paused");
698        Ok(())
699    }
700
701    /// Resume the queue.
702    #[instrument(skip(self), fields(queue = %self.name))]
703    pub async fn resume(&self) -> Result<(), Error> {
704        let script = self
705            .conn
706            .scripts()
707            .get("pause")
708            .ok_or_else(|| Error::InvalidConfig("pause script not found".to_string()))?
709            .clone();
710
711        let keys = vec![
712            self.keys.paused(),
713            self.keys.wait(),
714            self.keys.meta(),
715            self.keys.prioritized(),
716            self.keys.events(),
717            self.keys.delayed(),
718            self.keys.marker(),
719        ];
720
721        let args: Vec<&[u8]> = vec![b"resumed"];
722
723        let mut conn = self.conn.conn();
724        script.execute(&mut conn, &keys, &args).await?;
725
726        debug!("queue resumed");
727        Ok(())
728    }
729
730    /// Check if the queue is paused.
731    pub async fn is_paused(&self) -> Result<bool, Error> {
732        let mut conn = self.conn.conn();
733        let paused: Option<String> = redis::cmd("HGET")
734            .arg(self.keys.meta())
735            .arg("paused")
736            .query_async(&mut conn)
737            .await?;
738        Ok(paused.as_deref() == Some("1"))
739    }
740
741    /// Get a job by its ID.
742    pub async fn get_job(&self, job_id: &str) -> Result<Option<Job>, Error> {
743        let job = Job::from_id(&self.conn, &self.keys, job_id).await?;
744        Ok(job.map(|mut j| {
745            j.set_context(self.make_script_context());
746            j
747        }))
748    }
749
750    /// Get the counts of jobs in each state.
751    pub async fn get_job_counts(&self) -> Result<JobCounts, Error> {
752        let mut conn = self.conn.conn();
753        let mut pipe = redis::pipe();
754
755        pipe.cmd("LLEN").arg(self.keys.wait());
756        pipe.cmd("LLEN").arg(self.keys.active());
757        pipe.cmd("ZCARD").arg(self.keys.delayed());
758        pipe.cmd("ZCARD").arg(self.keys.prioritized());
759        pipe.cmd("ZCARD").arg(self.keys.completed());
760        pipe.cmd("ZCARD").arg(self.keys.failed());
761        pipe.cmd("ZCARD").arg(self.keys.waiting_children());
762        pipe.cmd("LLEN").arg(self.keys.paused());
763
764        let result: Vec<u64> = pipe.query_async(&mut conn).await?;
765
766        Ok(JobCounts {
767            waiting: result.first().copied().unwrap_or(0),
768            active: result.get(1).copied().unwrap_or(0),
769            delayed: result.get(2).copied().unwrap_or(0),
770            prioritized: result.get(3).copied().unwrap_or(0),
771            completed: result.get(4).copied().unwrap_or(0),
772            failed: result.get(5).copied().unwrap_or(0),
773            waiting_children: result.get(6).copied().unwrap_or(0),
774            paused: result.get(7).copied().unwrap_or(0),
775        })
776    }
777
778    /// Sanitize a list of job types: dedupe (preserving order) and add `paused`
779    /// whenever `waiting` is requested.
780    ///
781    /// Note: `paused` is **not** a per-job state — there is no way to pause an
782    /// individual job. It is the list where otherwise-`waiting` jobs are parked
783    /// while the *queue* is paused (via [`Queue::pause`]). Because a waiting job
784    /// may sit in either the `wait` or the `paused` list depending on whether the
785    /// queue is paused, querying `waiting` must also include `paused` to return
786    /// all waiting jobs. (This mirrors Node.js `sanitizeJobTypes`.)
787    ///
788    /// An empty input expands to all queryable states.
789    fn sanitize_job_types(types: &[&str]) -> Vec<String> {
790        if types.is_empty() {
791            return [
792                "active",
793                "completed",
794                "delayed",
795                "failed",
796                "paused",
797                "prioritized",
798                "waiting",
799                "waiting-children",
800            ]
801            .iter()
802            .map(|s| s.to_string())
803            .collect();
804        }
805
806        let mut out: Vec<String> = Vec::new();
807        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
808        for t in types {
809            if seen.insert((*t).to_string()) {
810                out.push((*t).to_string());
811            }
812        }
813        if types.contains(&"waiting") && seen.insert("paused".to_string()) {
814            out.push("paused".to_string());
815        }
816        out
817    }
818
819    /// Return job IDs for the given states with pagination.
820    ///
821    /// `types` are state names (e.g. `"waiting"`, `"active"`, `"completed"`).
822    /// `start`/`end` are zero-based inclusive indices (`-1` = last). When `asc`
823    /// is true, jobs are returned in ascending (oldest-first) order.
824    ///
825    /// The requested types are sanitized via [`Queue::sanitize_job_types`]: an
826    /// empty slice expands to all states, `waiting` also queries `paused`, and
827    /// duplicate types are removed. Results from all requested states are
828    /// concatenated and de-duplicated while preserving order.
829    pub async fn get_ranges(
830        &self,
831        types: &[&str],
832        start: i64,
833        end: i64,
834        asc: bool,
835    ) -> Result<Vec<String>, Error> {
836        let script = self
837            .conn
838            .scripts()
839            .get("getRanges")
840            .ok_or_else(|| Error::InvalidConfig("getRanges script not found".to_string()))?
841            .clone();
842
843        // The Lua script uses the queue key prefix (with trailing colon) as KEYS[1].
844        let keys = vec![self.keys.key_prefix()];
845
846        let start_s = start.to_string();
847        let end_s = end.to_string();
848        let asc_s = if asc { "1" } else { "0" };
849        // Apply the same sanitization as `get_jobs`: de-duplicate the requested
850        // types and, when `waiting` is requested, also include `paused` (where
851        // otherwise-waiting jobs are parked while the queue is paused). Then
852        // alias "waiting" -> "wait" for the Lua script.
853        let sanitized = Self::sanitize_job_types(types);
854        let transformed: Vec<String> = sanitized
855            .iter()
856            .map(|t| {
857                if t == "waiting" {
858                    "wait".to_string()
859                } else {
860                    t.clone()
861                }
862            })
863            .collect();
864
865        let mut args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes(), asc_s.as_bytes()];
866        for t in &transformed {
867            args.push(t.as_bytes());
868        }
869
870        let mut conn = self.conn.conn();
871        let result = script.execute(&mut conn, &keys, &args).await?;
872
873        // The script returns an array (one entry per state) of arrays of IDs.
874        // For list-based states (wait/paused/active) read in ascending order we
875        // must reverse the group — this mirrors the Node.js getter, which
876        // reverses `lrange` results when `asc` is set.
877        let extract_id = |v: redis::Value| -> Option<String> {
878            match v {
879                redis::Value::BulkString(bytes) => {
880                    Some(String::from_utf8_lossy(&bytes).to_string())
881                }
882                redis::Value::SimpleString(s) => Some(s),
883                _ => None,
884            }
885        };
886
887        let mut out: Vec<String> = Vec::new();
888        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
889
890        if let redis::Value::Array(groups) = result {
891            for (i, group) in groups.into_iter().enumerate() {
892                let is_list = matches!(
893                    transformed.get(i).map(|s| s.as_str()),
894                    Some("wait") | Some("paused") | Some("active")
895                );
896                let mut ids: Vec<String> = match group {
897                    redis::Value::Array(items) => {
898                        items.into_iter().filter_map(extract_id).collect()
899                    }
900                    other => extract_id(other).into_iter().collect(),
901                };
902                if asc && is_list {
903                    ids.reverse();
904                }
905                for id in ids {
906                    if seen.insert(id.clone()) {
907                        out.push(id);
908                    }
909                }
910            }
911        }
912
913        Ok(out)
914    }
915
916    /// Return the jobs that are in the given states with pagination.
917    ///
918    /// `types` are state names. An empty slice returns jobs from all states.
919    pub async fn get_jobs(
920        &self,
921        types: &[&str],
922        start: i64,
923        end: i64,
924        asc: bool,
925    ) -> Result<Vec<Job>, Error> {
926        let script = self
927            .conn
928            .scripts()
929            .get("getJobs")
930            .ok_or_else(|| Error::InvalidConfig("getJobs script not found".to_string()))?
931            .clone();
932
933        let keys = vec![self.keys.key_prefix()];
934
935        let start_s = start.to_string();
936        let end_s = end.to_string();
937        let asc_s = if asc { "1" } else { "0" };
938        let max_iterations_s = GET_JOBS_MAX_BACKFILL_ITERATIONS.to_string();
939        let sanitized = Self::sanitize_job_types(types);
940        let transformed: Vec<String> = sanitized
941            .iter()
942            .map(|t| {
943                if t == "waiting" {
944                    "wait".to_string()
945                } else {
946                    t.clone()
947                }
948            })
949            .collect();
950
951        let mut args: Vec<&[u8]> = vec![
952            start_s.as_bytes(),
953            end_s.as_bytes(),
954            asc_s.as_bytes(),
955            max_iterations_s.as_bytes(),
956        ];
957        for t in &transformed {
958            args.push(t.as_bytes());
959        }
960
961        let mut conn = self.conn.conn();
962        let result = script.execute(&mut conn, &keys, &args).await?;
963
964        let redis_value_to_string = |value: redis::Value| -> Option<String> {
965            match value {
966                redis::Value::BulkString(bytes) => {
967                    Some(String::from_utf8_lossy(&bytes).to_string())
968                }
969                redis::Value::SimpleString(s) => Some(s),
970                redis::Value::Int(n) => Some(n.to_string()),
971                _ => None,
972            }
973        };
974
975        let redis_hash_to_map = |value: redis::Value| -> Option<HashMap<String, String>> {
976            let redis::Value::Array(items) = value else {
977                return None;
978            };
979
980            let mut fields = HashMap::new();
981            let mut iter = items.into_iter();
982            while let Some(field) = iter.next() {
983                let Some(value) = iter.next() else {
984                    break;
985                };
986                if let (Some(field), Some(value)) =
987                    (redis_value_to_string(field), redis_value_to_string(value))
988                {
989                    fields.insert(field, value);
990                }
991            }
992            Some(fields)
993        };
994
995        let parse_job_entry = |value: redis::Value| -> Option<(String, HashMap<String, String>)> {
996            let redis::Value::Array(entry) = value else {
997                return None;
998            };
999
1000            let mut iter = entry.into_iter();
1001            let job_id = redis_value_to_string(iter.next()?)?;
1002            let fields = redis_hash_to_map(iter.next()?)?;
1003            if fields.is_empty() {
1004                return None;
1005            }
1006
1007            Some((job_id, fields))
1008        };
1009
1010        let mut jobs = Vec::new();
1011        let mut seen: HashSet<String> = HashSet::new();
1012
1013        if let redis::Value::Array(groups) = result {
1014            for group in groups {
1015                let redis::Value::Array(entries) = group else {
1016                    continue;
1017                };
1018
1019                for entry in entries {
1020                    let Some((job_id, fields)) = parse_job_entry(entry) else {
1021                        continue;
1022                    };
1023
1024                    if seen.insert(job_id.clone()) {
1025                        let mut job = Job::from_redis_hash(&job_id, &fields)?;
1026                        job.set_context(self.make_script_context());
1027                        jobs.push(job);
1028                    }
1029                }
1030            }
1031        }
1032
1033        Ok(jobs)
1034    }
1035
1036    /// Return jobs in the `waiting` (and `paused`) state.
1037    pub async fn get_waiting(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1038        self.get_jobs(&["waiting"], start, end, true).await
1039    }
1040
1041    /// Return jobs in the `waiting-children` state (parents with pending children).
1042    pub async fn get_waiting_children(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1043        self.get_jobs(&["waiting-children"], start, end, true).await
1044    }
1045
1046    /// Return jobs in the `active` state.
1047    pub async fn get_active(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1048        self.get_jobs(&["active"], start, end, true).await
1049    }
1050
1051    /// Return jobs in the `delayed` state.
1052    pub async fn get_delayed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1053        self.get_jobs(&["delayed"], start, end, true).await
1054    }
1055
1056    /// Return jobs in the `prioritized` state.
1057    pub async fn get_prioritized(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1058        self.get_jobs(&["prioritized"], start, end, true).await
1059    }
1060
1061    /// Return jobs in the `completed` state.
1062    pub async fn get_completed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1063        self.get_jobs(&["completed"], start, end, false).await
1064    }
1065
1066    /// Return jobs in the `failed` state.
1067    pub async fn get_failed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
1068        self.get_jobs(&["failed"], start, end, false).await
1069    }
1070
1071    /// Raw `getCounts` script call. Transforms `waiting` -> `wait` and returns
1072    /// one count per provided type, in order.
1073    async fn get_counts_raw(&self, types: &[String]) -> Result<Vec<u64>, Error> {
1074        let script = self
1075            .conn
1076            .scripts()
1077            .get("getCounts")
1078            .ok_or_else(|| Error::InvalidConfig("getCounts script not found".to_string()))?
1079            .clone();
1080
1081        let keys = vec![self.keys.key_prefix()];
1082        let transformed: Vec<String> = types
1083            .iter()
1084            .map(|t| {
1085                if t == "waiting" {
1086                    "wait".to_string()
1087                } else {
1088                    t.clone()
1089                }
1090            })
1091            .collect();
1092        let args: Vec<&[u8]> = transformed.iter().map(|s| s.as_bytes()).collect();
1093
1094        let mut conn = self.conn.conn();
1095        let result = script.execute(&mut conn, &keys, &args).await?;
1096
1097        let counts = match result {
1098            redis::Value::Array(arr) => arr
1099                .into_iter()
1100                .map(|v| match v {
1101                    redis::Value::Int(n) => n.max(0) as u64,
1102                    _ => 0,
1103                })
1104                .collect(),
1105            _ => Vec::new(),
1106        };
1107        Ok(counts)
1108    }
1109
1110    /// Return the job counts for each provided type, keyed by type name.
1111    ///
1112    /// An empty `types` slice returns counts for every queryable state.
1113    pub async fn get_job_counts_by_types(
1114        &self,
1115        types: &[&str],
1116    ) -> Result<HashMap<String, u64>, Error> {
1117        let sanitized = Self::sanitize_job_types(types);
1118        let counts = self.get_counts_raw(&sanitized).await?;
1119
1120        let mut map = HashMap::new();
1121        for (i, t) in sanitized.iter().enumerate() {
1122            map.insert(t.clone(), counts.get(i).copied().unwrap_or(0));
1123        }
1124        Ok(map)
1125    }
1126
1127    /// Return the total number of jobs across the provided types.
1128    pub async fn get_job_count_by_types(&self, types: &[&str]) -> Result<u64, Error> {
1129        let map = self.get_job_counts_by_types(types).await?;
1130        Ok(map.values().sum())
1131    }
1132
1133    /// Return job counts by state, invoking `recorder` once per `(state, count)`.
1134    ///
1135    /// Mirrors Node.js `Queue.recordJobCountsMetric`, which records gauge
1136    /// metrics when telemetry is configured. The Rust port has no built-in
1137    /// telemetry subsystem, so the optional `recorder` closure is the
1138    /// integration point: pass `None` to simply retrieve the counts, or supply
1139    /// a closure to forward each state's count to your metrics backend.
1140    pub async fn record_job_counts_metric(
1141        &self,
1142        types: &[&str],
1143        recorder: Option<JobCountRecorder<'_>>,
1144    ) -> Result<HashMap<String, u64>, Error> {
1145        let counts = self.get_job_counts_by_types(types).await?;
1146        if let Some(record) = recorder {
1147            for (state, count) in &counts {
1148                record(state, *count);
1149            }
1150        }
1151        Ok(counts)
1152    }
1153
1154    /// Return the number of jobs waiting to be processed: this sums `waiting`,
1155    /// `paused`, `delayed`, `prioritized` and `waiting-children`.
1156    pub async fn count(&self) -> Result<u64, Error> {
1157        self.get_job_count_by_types(&[
1158            "waiting",
1159            "paused",
1160            "delayed",
1161            "prioritized",
1162            "waiting-children",
1163        ])
1164        .await
1165    }
1166
1167    /// Return the number of jobs per priority for the provided priorities.
1168    ///
1169    /// Priorities are de-duplicated. The returned map is keyed by priority value.
1170    pub async fn get_counts_per_priority(
1171        &self,
1172        priorities: &[u64],
1173    ) -> Result<HashMap<u64, u64>, Error> {
1174        let script = self
1175            .conn
1176            .scripts()
1177            .get("getCountsPerPriority")
1178            .ok_or_else(|| {
1179                Error::InvalidConfig("getCountsPerPriority script not found".to_string())
1180            })?
1181            .clone();
1182
1183        // De-duplicate while preserving order.
1184        let mut unique: Vec<u64> = Vec::new();
1185        let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1186        for p in priorities {
1187            if seen.insert(*p) {
1188                unique.push(*p);
1189            }
1190        }
1191
1192        let keys = vec![
1193            self.keys.wait(),
1194            self.keys.paused(),
1195            self.keys.meta(),
1196            self.keys.prioritized(),
1197        ];
1198        let prio_strs: Vec<String> = unique.iter().map(|p| p.to_string()).collect();
1199        let args: Vec<&[u8]> = prio_strs.iter().map(|s| s.as_bytes()).collect();
1200
1201        let mut conn = self.conn.conn();
1202        let result = script.execute(&mut conn, &keys, &args).await?;
1203
1204        let counts: Vec<u64> = match result {
1205            redis::Value::Array(arr) => arr
1206                .into_iter()
1207                .map(|v| match v {
1208                    redis::Value::Int(n) => n.max(0) as u64,
1209                    _ => 0,
1210                })
1211                .collect(),
1212            _ => Vec::new(),
1213        };
1214
1215        let mut map = HashMap::new();
1216        for (i, p) in unique.iter().enumerate() {
1217            map.insert(*p, counts.get(i).copied().unwrap_or(0));
1218        }
1219        Ok(map)
1220    }
1221
1222    /// Return the number of jobs in the `completed` state.
1223    pub async fn get_completed_count(&self) -> Result<u64, Error> {
1224        self.get_job_count_by_types(&["completed"]).await
1225    }
1226
1227    /// Return the number of jobs in the `failed` state.
1228    pub async fn get_failed_count(&self) -> Result<u64, Error> {
1229        self.get_job_count_by_types(&["failed"]).await
1230    }
1231
1232    /// Return the number of jobs in the `delayed` state.
1233    pub async fn get_delayed_count(&self) -> Result<u64, Error> {
1234        self.get_job_count_by_types(&["delayed"]).await
1235    }
1236
1237    /// Return the number of jobs in the `active` state.
1238    pub async fn get_active_count(&self) -> Result<u64, Error> {
1239        self.get_job_count_by_types(&["active"]).await
1240    }
1241
1242    /// Return the number of jobs in the `prioritized` state.
1243    pub async fn get_prioritized_count(&self) -> Result<u64, Error> {
1244        self.get_job_count_by_types(&["prioritized"]).await
1245    }
1246
1247    /// Return the number of jobs in the `waiting` (and `paused`) state.
1248    pub async fn get_waiting_count(&self) -> Result<u64, Error> {
1249        self.get_job_count_by_types(&["waiting"]).await
1250    }
1251
1252    /// Return the number of jobs in the `waiting-children` state.
1253    pub async fn get_waiting_children_count(&self) -> Result<u64, Error> {
1254        self.get_job_count_by_types(&["waiting-children"]).await
1255    }
1256
1257    /// Return the list of workers currently connected to this queue.
1258    ///
1259    /// Workers register themselves via `CLIENT SETNAME` on their blocking
1260    /// connection; this method runs `CLIENT LIST` and returns the parsed info
1261    /// maps for clients whose name matches this queue. Each map's `name` field
1262    /// is set to the queue name and the original client name is preserved in
1263    /// `rawname` (mirroring Node.js `Queue.getWorkers`).
1264    ///
1265    /// Note: some managed Redis providers (e.g. GCP Memorystore) do not support
1266    /// `CLIENT SETNAME`/`CLIENT LIST`, in which case this returns an empty list.
1267    pub async fn get_workers(&self) -> Result<Vec<HashMap<String, String>>, Error> {
1268        let mut cmd = redis::cmd("CLIENT");
1269        cmd.arg("LIST");
1270        let list: String = match self.conn.cmd(&mut cmd).await {
1271            Ok(list) => list,
1272            Err(err) => {
1273                debug!(error = %err, "CLIENT LIST unavailable; returning empty worker list");
1274                return Ok(Vec::new());
1275            }
1276        };
1277
1278        let unnamed = self.keys.client_name("");
1279        let named_prefix = self.keys.client_name(":w:");
1280        Ok(parse_client_list(
1281            &list,
1282            &self.name,
1283            &unnamed,
1284            &named_prefix,
1285        ))
1286    }
1287
1288    /// Return the number of workers currently connected to this queue.
1289    pub async fn get_workers_count(&self) -> Result<usize, Error> {
1290        Ok(self.get_workers().await?.len())
1291    }
1292
1293    /// Return the queue's public metadata (read from the `meta` hash).
1294    ///
1295    /// Well-known numeric/boolean fields (`concurrency`, `max`, `duration`,
1296    /// `opts.maxLenEvents`, `paused`) are parsed into typed fields; any other
1297    /// entries are preserved in [`QueueMeta::other`]. Mirrors Node.js
1298    /// `Queue.getMeta`.
1299    pub async fn get_meta(&self) -> Result<QueueMeta, Error> {
1300        let mut conn = self.conn.conn();
1301        let config: HashMap<String, String> = redis::cmd("HGETALL")
1302            .arg(self.keys.meta())
1303            .query_async(&mut conn)
1304            .await?;
1305
1306        let mut meta = QueueMeta::default();
1307        for (key, value) in config {
1308            match key.as_str() {
1309                "concurrency" => meta.concurrency = value.parse().ok(),
1310                "max" => meta.max = value.parse().ok(),
1311                "duration" => meta.duration = value.parse().ok(),
1312                "opts.maxLenEvents" => meta.max_len_events = value.parse().ok(),
1313                "paused" => meta.paused = value == "1",
1314                _ => {
1315                    meta.other.insert(key, value);
1316                }
1317            }
1318        }
1319
1320        Ok(meta)
1321    }
1322
1323    /// Return the library version string stored in the `meta` hash.
1324    ///
1325    /// The Rust port records `bullmq-official:<version>` under the `library` field
1326    /// when the queue is created. Returns `None` if the field is unset.
1327    pub async fn get_version(&self) -> Result<Option<String>, Error> {
1328        let mut conn = self.conn.conn();
1329        let value: Option<String> = redis::cmd("HGET")
1330            .arg(self.keys.meta())
1331            .arg("library")
1332            .query_async(&mut conn)
1333            .await?;
1334        Ok(value)
1335    }
1336
1337    /// Return `true` when the number of active jobs has reached the queue's
1338    /// global concurrency limit. Returns `false` when no global concurrency is
1339    /// configured. Mirrors Node.js `Queue.isMaxed`.
1340    pub async fn is_maxed(&self) -> Result<bool, Error> {
1341        let script = self
1342            .conn
1343            .scripts()
1344            .get("isMaxed")
1345            .ok_or_else(|| Error::InvalidConfig("isMaxed script not found".to_string()))?
1346            .clone();
1347
1348        let keys = vec![self.keys.meta(), self.keys.active()];
1349        let args: Vec<&[u8]> = vec![];
1350
1351        let mut conn = self.conn.conn();
1352        let result = script.execute(&mut conn, &keys, &args).await?;
1353        Ok(matches!(result, redis::Value::Int(1)) || matches!(result, redis::Value::Boolean(true)))
1354    }
1355
1356    /// Export the queue's job counts and totals in the Prometheus text
1357    /// exposition format.
1358    ///
1359    /// Emits a `bullmq_job_count` gauge per job state plus
1360    /// `bullmq_job_completed_total` / `bullmq_job_failed_total` counters sourced
1361    /// from the time-series metrics. `global_labels` are appended (in order) as
1362    /// extra labels on every series; pass `&[]` for none. Mirrors Node.js
1363    /// `Queue.exportPrometheusMetrics`.
1364    pub async fn export_prometheus_metrics(
1365        &self,
1366        global_labels: &[(&str, &str)],
1367    ) -> Result<String, Error> {
1368        let counts = self.get_job_counts().await?;
1369        let mut metrics: Vec<String> = Vec::new();
1370
1371        metrics.push("# HELP bullmq_job_count Number of jobs in the queue by state".to_string());
1372        metrics.push("# TYPE bullmq_job_count gauge".to_string());
1373
1374        let escaped_queue = escape_prometheus_label_value(&self.name);
1375        let variables: String = global_labels
1376            .iter()
1377            .map(|(k, v)| format!(", {}=\"{}\"", k, escape_prometheus_label_value(v)))
1378            .collect();
1379
1380        let states: [(&str, u64); 8] = [
1381            ("active", counts.active),
1382            ("completed", counts.completed),
1383            ("delayed", counts.delayed),
1384            ("failed", counts.failed),
1385            ("paused", counts.paused),
1386            ("prioritized", counts.prioritized),
1387            ("waiting", counts.waiting),
1388            ("waiting-children", counts.waiting_children),
1389        ];
1390        for (state, count) in states {
1391            metrics.push(format!(
1392                "bullmq_job_count{{queue=\"{escaped_queue}\", state=\"{state}\"{variables}}} {count}"
1393            ));
1394        }
1395
1396        let completed_metrics = self.get_metrics("completed", 0, -1).await?;
1397        let failed_metrics = self.get_metrics("failed", 0, -1).await?;
1398
1399        metrics
1400            .push("# HELP bullmq_job_completed_total Total number of completed jobs".to_string());
1401        metrics.push("# TYPE bullmq_job_completed_total counter".to_string());
1402        metrics.push(format!(
1403            "bullmq_job_completed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1404            completed_metrics.meta.count
1405        ));
1406
1407        metrics.push("# HELP bullmq_job_failed_total Total number of failed jobs".to_string());
1408        metrics.push("# TYPE bullmq_job_failed_total counter".to_string());
1409        metrics.push(format!(
1410            "bullmq_job_failed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1411            failed_metrics.meta.count
1412        ));
1413
1414        Ok(metrics.join("\n"))
1415    }
1416
1417    /// Return the time-series metrics for the queue.
1418    ///
1419    /// `metric_type` must be `"completed"` or `"failed"`. Metrics are recorded
1420    /// per minute by workers configured with the `metrics` option. `start`/`end`
1421    /// are zero-based indices into the data points where `0` is the newest.
1422    pub async fn get_metrics(
1423        &self,
1424        metric_type: &str,
1425        start: i64,
1426        end: i64,
1427    ) -> Result<crate::types::Metrics, Error> {
1428        // Node.js restricts the metric type to `completed`/`failed` at the type
1429        // level; in Rust we validate at runtime so an invalid name returns an
1430        // error instead of silently reading non-existent keys and returning
1431        // empty metrics.
1432        if metric_type != "completed" && metric_type != "failed" {
1433            return Err(Error::InvalidConfig(format!(
1434                "metric type must be \"completed\" or \"failed\", got \"{}\"",
1435                metric_type
1436            )));
1437        }
1438
1439        let script = self
1440            .conn
1441            .scripts()
1442            .get("getMetrics")
1443            .ok_or_else(|| Error::InvalidConfig("getMetrics script not found".to_string()))?
1444            .clone();
1445
1446        let metrics_key = self.keys.get(&format!("metrics:{}", metric_type));
1447        let data_key = self.keys.get(&format!("metrics:{}:data", metric_type));
1448        let keys = vec![metrics_key, data_key];
1449
1450        let start_s = start.to_string();
1451        let end_s = end.to_string();
1452        let args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes()];
1453
1454        let mut conn = self.conn.conn();
1455        let result = script.execute(&mut conn, &keys, &args).await?;
1456
1457        // The script returns [meta(array of 3), data(array), count(int)].
1458        let parse_u64 = |v: &redis::Value| -> u64 {
1459            match v {
1460                redis::Value::BulkString(b) => String::from_utf8_lossy(b).parse().unwrap_or(0),
1461                redis::Value::SimpleString(s) => s.parse().unwrap_or(0),
1462                redis::Value::Int(n) => (*n).max(0) as u64,
1463                _ => 0,
1464            }
1465        };
1466
1467        let mut metrics = crate::types::Metrics::default();
1468        if let redis::Value::Array(parts) = result {
1469            if let Some(redis::Value::Array(meta)) = parts.first() {
1470                metrics.meta.count = meta.first().map(parse_u64).unwrap_or(0);
1471                metrics.meta.prev_ts = meta.get(1).map(parse_u64).unwrap_or(0);
1472                metrics.meta.prev_count = meta.get(2).map(parse_u64).unwrap_or(0);
1473            }
1474            if let Some(redis::Value::Array(data)) = parts.get(1) {
1475                metrics.data = data.iter().map(parse_u64).collect();
1476            }
1477            if let Some(count) = parts.get(2) {
1478                metrics.count = parse_u64(count);
1479            }
1480        }
1481
1482        Ok(metrics)
1483    }
1484
1485    /// Get return values of all completed children of a parent job.
1486    pub async fn get_children_values(
1487        &self,
1488        job_id: &str,
1489    ) -> Result<HashMap<String, serde_json::Value>, Error> {
1490        let processed_key = format!("{}:processed", self.keys.job_key(job_id));
1491        let mut conn = self.conn.conn();
1492
1493        let result: HashMap<String, String> = redis::cmd("HGETALL")
1494            .arg(&processed_key)
1495            .query_async(&mut conn)
1496            .await?;
1497
1498        let mut parsed = HashMap::new();
1499        for (key, value) in result {
1500            let parsed_value: serde_json::Value =
1501                serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
1502            parsed.insert(key, parsed_value);
1503        }
1504        Ok(parsed)
1505    }
1506
1507    /// Get failure values of children that failed with ignoreDependencyOnFailure.
1508    pub async fn get_failed_children_values(
1509        &self,
1510        job_id: &str,
1511    ) -> Result<HashMap<String, String>, Error> {
1512        let failed_key = format!("{}:failed", self.keys.job_key(job_id));
1513        let mut conn = self.conn.conn();
1514
1515        let result: HashMap<String, String> = redis::cmd("HGETALL")
1516            .arg(&failed_key)
1517            .query_async(&mut conn)
1518            .await?;
1519
1520        Ok(result)
1521    }
1522
1523    /// Get counts of dependencies for a parent job.
1524    pub async fn get_dependencies_count(&self, job_id: &str) -> Result<DependenciesCount, Error> {
1525        let job_key = self.keys.job_key(job_id);
1526        let processed_key = format!("{}:processed", job_key);
1527        let deps_key = format!("{}:dependencies", job_key);
1528        let failed_key = format!("{}:failed", job_key);
1529        let unsuccessful_key = format!("{}:unsuccessful", job_key);
1530
1531        let mut conn = self.conn.conn();
1532        let mut pipe = redis::pipe();
1533        pipe.cmd("HLEN").arg(&processed_key);
1534        pipe.cmd("SCARD").arg(&deps_key);
1535        pipe.cmd("HLEN").arg(&failed_key);
1536        pipe.cmd("ZCARD").arg(&unsuccessful_key);
1537
1538        let (processed, unprocessed, ignored, failed): (u64, u64, u64, u64) =
1539            pipe.query_async(&mut conn).await?;
1540
1541        Ok(DependenciesCount {
1542            processed,
1543            unprocessed,
1544            ignored,
1545            failed,
1546        })
1547    }
1548
1549    /// Get unprocessed dependencies (children still pending).
1550    pub async fn get_unprocessed_dependencies(&self, job_id: &str) -> Result<Vec<String>, Error> {
1551        let deps_key = format!("{}:dependencies", self.keys.job_key(job_id));
1552        let mut conn = self.conn.conn();
1553
1554        let result: Vec<String> = redis::cmd("SMEMBERS")
1555            .arg(&deps_key)
1556            .query_async(&mut conn)
1557            .await?;
1558
1559        Ok(result)
1560    }
1561
1562    /// Remove a child's dependency from its parent.
1563    ///
1564    /// `job_id` - the child job ID
1565    /// `parent_key` - the fully qualified parent key (prefix:queue:parentId)
1566    ///
1567    /// Returns `true` if the dependency was broken, `false` otherwise.
1568    pub async fn remove_child_dependency(
1569        &self,
1570        job_id: &str,
1571        parent_key: &str,
1572    ) -> Result<bool, Error> {
1573        let script = self
1574            .conn
1575            .scripts()
1576            .get("removeChildDependency")
1577            .ok_or_else(|| {
1578                Error::InvalidConfig("removeChildDependency script not found".to_string())
1579            })?
1580            .clone();
1581
1582        let prefix_key = self.keys.key_prefix().to_string();
1583        let job_key = self.keys.job_key(job_id);
1584
1585        let keys = vec![prefix_key];
1586        let args: Vec<&[u8]> = vec![job_key.as_bytes(), parent_key.as_bytes()];
1587
1588        let mut conn = self.conn.conn();
1589        let result = script.execute(&mut conn, &keys, &args).await?;
1590
1591        match result {
1592            redis::Value::Int(0) => Ok(true),
1593            redis::Value::Int(1) => Ok(false),
1594            redis::Value::Int(-1) => Err(Error::InvalidConfig(format!(
1595                "Missing key for job {}. removeChildDependency",
1596                job_id
1597            ))),
1598            redis::Value::Int(-5) => Err(Error::InvalidConfig(format!(
1599                "Missing key for parent job {}. removeChildDependency",
1600                parent_key
1601            ))),
1602            _ => Ok(false),
1603        }
1604    }
1605
1606    /// Get the state of a specific job.
1607    pub async fn get_job_state(&self, job_id: &str) -> Result<JobState, Error> {
1608        let script = self
1609            .conn
1610            .scripts()
1611            .get("getState")
1612            .ok_or_else(|| Error::InvalidConfig("getState script not found".to_string()))?
1613            .clone();
1614
1615        let keys = vec![
1616            self.keys.completed(),
1617            self.keys.failed(),
1618            self.keys.delayed(),
1619            self.keys.active(),
1620            self.keys.wait(),
1621            self.keys.paused(),
1622            self.keys.waiting_children(),
1623            self.keys.prioritized(),
1624        ];
1625
1626        let job_id_bytes = job_id.as_bytes().to_vec();
1627        let args: Vec<&[u8]> = vec![&job_id_bytes];
1628
1629        let mut conn = self.conn.conn();
1630        let result = script.execute(&mut conn, &keys, &args).await?;
1631
1632        match result {
1633            redis::Value::BulkString(bytes) => {
1634                let state_str = String::from_utf8_lossy(&bytes);
1635                Ok(JobState::from_redis_str(&state_str))
1636            }
1637            redis::Value::SimpleString(s) => Ok(JobState::from_redis_str(&s)),
1638            _ => Ok(JobState::Unknown),
1639        }
1640    }
1641
1642    /// Remove a job by its ID.
1643    pub async fn remove(&self, job_id: &str) -> Result<bool, Error> {
1644        self.remove_job(job_id, true).await
1645    }
1646
1647    /// Remove a job without removing its children.
1648    ///
1649    /// Children remain in their queues and lose their parent reference.
1650    pub async fn remove_without_children(&self, job_id: &str) -> Result<bool, Error> {
1651        self.remove_job(job_id, false).await
1652    }
1653
1654    async fn remove_job(&self, job_id: &str, remove_children: bool) -> Result<bool, Error> {
1655        let script = self
1656            .conn
1657            .scripts()
1658            .get("removeJob")
1659            .ok_or_else(|| Error::InvalidConfig("removeJob script not found".to_string()))?
1660            .clone();
1661
1662        let keys = vec![self.keys.job_key(job_id), self.keys.repeat()];
1663        let prefix = self.keys.key_prefix();
1664        let remove_children_flag = if remove_children { b"1" as &[u8] } else { b"0" };
1665        let args: Vec<&[u8]> = vec![job_id.as_bytes(), remove_children_flag, prefix.as_bytes()];
1666
1667        let mut conn = self.conn.conn();
1668        let result = script.execute(&mut conn, &keys, &args).await?;
1669
1670        match result {
1671            // 1 = removed, 0 = job (or a dependency) is locked. Mirroring
1672            // Node.js, a locked job is a normal "not removed" outcome rather
1673            // than an error.
1674            redis::Value::Int(1) => Ok(true),
1675            redis::Value::Int(0) => Ok(false),
1676            redis::Value::Int(code) if code < 0 => {
1677                if code == crate::error::error_code::JOB_BELONGS_TO_JOB_SCHEDULER {
1678                    Err(Error::Script {
1679                        code,
1680                        message: format!(
1681                            "Job {} belongs to a job scheduler and cannot be removed directly. removeJob",
1682                            job_id
1683                        ),
1684                    })
1685                } else {
1686                    Err(Error::from_script_code(code))
1687                }
1688            }
1689            _ => Ok(false),
1690        }
1691    }
1692
1693    /// Remove all unprocessed children of a job.
1694    ///
1695    /// This removes children that are still in the dependencies set (not yet completed/failed).
1696    /// Active children are skipped.
1697    pub async fn remove_unprocessed_children(&self, job_id: &str) -> Result<(), Error> {
1698        let script = self
1699            .conn
1700            .scripts()
1701            .get("removeUnprocessedChildren")
1702            .ok_or_else(|| {
1703                Error::InvalidConfig("removeUnprocessedChildren script not found".to_string())
1704            })?
1705            .clone();
1706
1707        let keys = vec![self.keys.job_key(job_id), self.keys.meta()];
1708        let prefix = self.keys.key_prefix();
1709        let args: Vec<&[u8]> = vec![prefix.as_bytes(), job_id.as_bytes()];
1710
1711        let mut conn = self.conn.conn();
1712        script.execute(&mut conn, &keys, &args).await?;
1713        Ok(())
1714    }
1715
1716    /// Clean jobs from a specific set (completed, failed, etc.).
1717    ///
1718    /// `grace` - Only remove jobs older than this many milliseconds.
1719    /// `limit` - Maximum number of jobs to remove (0 = unlimited).
1720    /// `state` - Which state set to clean ("completed", "failed", "wait", "active", "delayed", "prioritized", "paused").
1721    ///
1722    /// Returns the IDs of removed jobs.
1723    pub async fn clean(&self, grace: u64, limit: u32, state: &str) -> Result<Vec<String>, Error> {
1724        let script = self
1725            .conn
1726            .scripts()
1727            .get("cleanJobsInSet")
1728            .ok_or_else(|| Error::InvalidConfig("cleanJobsInSet script not found".to_string()))?
1729            .clone();
1730
1731        let now = std::time::SystemTime::now()
1732            .duration_since(std::time::UNIX_EPOCH)
1733            .unwrap()
1734            .as_millis() as u64;
1735        let timestamp = now.saturating_sub(grace);
1736
1737        // Normalize "waiting" to "wait"
1738        let normalized = if state == "waiting" { "wait" } else { state };
1739
1740        let set_key = match normalized {
1741            "completed" => self.keys.completed(),
1742            "failed" => self.keys.failed(),
1743            "wait" => self.keys.wait(),
1744            "active" => self.keys.active(),
1745            "delayed" => self.keys.delayed(),
1746            "paused" => self.keys.paused(),
1747            "prioritized" => self.keys.prioritized(),
1748            _ => return Err(Error::InvalidConfig(format!("invalid state: {}", state))),
1749        };
1750
1751        let max_per_call = if limit == 0 {
1752            10000u32
1753        } else {
1754            limit.min(10000)
1755        };
1756        let max_total = if limit == 0 { u32::MAX } else { limit };
1757        let mut all_deleted: Vec<String> = Vec::new();
1758
1759        loop {
1760            let keys = vec![set_key.clone(), self.keys.events(), self.keys.repeat()];
1761
1762            let prefix = self.keys.key_prefix();
1763            let ts_str = timestamp.to_string();
1764            let limit_str = max_per_call.to_string();
1765
1766            let args: Vec<&[u8]> = vec![
1767                prefix.as_bytes(),
1768                ts_str.as_bytes(),
1769                limit_str.as_bytes(),
1770                normalized.as_bytes(),
1771            ];
1772
1773            let mut conn = self.conn.conn();
1774            let result = script.execute(&mut conn, &keys, &args).await?;
1775
1776            let batch: Vec<String> = match result {
1777                redis::Value::Array(arr) => arr
1778                    .into_iter()
1779                    .filter_map(|v| match v {
1780                        redis::Value::BulkString(bytes) => {
1781                            Some(String::from_utf8_lossy(&bytes).to_string())
1782                        }
1783                        redis::Value::SimpleString(s) => Some(s),
1784                        _ => None,
1785                    })
1786                    .collect(),
1787                _ => Vec::new(),
1788            };
1789
1790            let batch_len = batch.len() as u32;
1791            all_deleted.extend(batch);
1792
1793            if batch_len < max_per_call || all_deleted.len() as u32 >= max_total {
1794                break;
1795            }
1796        }
1797
1798        Ok(all_deleted)
1799    }
1800
1801    /// Drain the queue (remove all waiting and delayed jobs).
1802    pub async fn drain(&self, delayed: bool) -> Result<(), Error> {
1803        let script = self
1804            .conn
1805            .scripts()
1806            .get("drain")
1807            .ok_or_else(|| Error::InvalidConfig("drain script not found".to_string()))?
1808            .clone();
1809
1810        let keys = vec![
1811            self.keys.wait(),
1812            self.keys.paused(),
1813            self.keys.delayed(),
1814            self.keys.prioritized(),
1815            self.keys.repeat(),
1816        ];
1817
1818        let delayed_str = if delayed { "1" } else { "0" };
1819        let prefix = self.keys.key_prefix();
1820        let args: Vec<&[u8]> = vec![prefix.as_bytes(), delayed_str.as_bytes()];
1821
1822        let mut conn = self.conn.conn();
1823        script.execute(&mut conn, &keys, &args).await?;
1824
1825        debug!(delayed, "queue drained");
1826        Ok(())
1827    }
1828
1829    /// Retry all failed (or completed) jobs, moving them back to wait.
1830    ///
1831    /// - `state`: "failed" or "completed" (default: "failed")
1832    /// - `count`: max jobs to move per batch (default: 1000)
1833    /// - `timestamp`: only retry jobs finished before this timestamp in ms (default: now)
1834    pub async fn retry_jobs(
1835        &self,
1836        state: &str,
1837        count: u32,
1838        timestamp: Option<u64>,
1839    ) -> Result<(), Error> {
1840        let script = self
1841            .conn
1842            .scripts()
1843            .get("moveJobsToWait")
1844            .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1845            .clone();
1846
1847        let ts = timestamp.unwrap_or_else(|| {
1848            std::time::SystemTime::now()
1849                .duration_since(std::time::UNIX_EPOCH)
1850                .unwrap()
1851                .as_millis() as u64
1852        });
1853
1854        let keys = vec![
1855            self.keys.key_prefix(),
1856            self.keys.events(),
1857            self.keys.get(state),
1858            self.keys.wait(),
1859            self.keys.paused(),
1860            self.keys.meta(),
1861            self.keys.active(),
1862            self.keys.marker(),
1863        ];
1864
1865        let count_str = count.to_string();
1866        let ts_str = ts.to_string();
1867
1868        let mut conn = self.conn.conn();
1869        loop {
1870            let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), state.as_bytes()];
1871            let result = script.execute(&mut conn, &keys, &args).await?;
1872
1873            match result {
1874                redis::Value::Int(1) => continue,
1875                _ => break,
1876            }
1877        }
1878
1879        debug!(state, "retry_jobs completed");
1880        Ok(())
1881    }
1882
1883    /// Promote all delayed jobs to waiting.
1884    ///
1885    /// - `count`: max jobs to promote per batch (default: 1000)
1886    pub async fn promote_jobs(&self, count: u32) -> Result<(), Error> {
1887        let script = self
1888            .conn
1889            .scripts()
1890            .get("moveJobsToWait")
1891            .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1892            .clone();
1893
1894        let keys = vec![
1895            self.keys.key_prefix(),
1896            self.keys.events(),
1897            self.keys.delayed(),
1898            self.keys.wait(),
1899            self.keys.paused(),
1900            self.keys.meta(),
1901            self.keys.active(),
1902            self.keys.marker(),
1903        ];
1904
1905        let count_str = count.to_string();
1906        // Use MAX_VALUE equivalent for timestamp so all delayed jobs match
1907        let ts_str = "9007199254740991".to_string(); // Number.MAX_SAFE_INTEGER
1908
1909        let mut conn = self.conn.conn();
1910        loop {
1911            let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), b"delayed"];
1912            let result = script.execute(&mut conn, &keys, &args).await?;
1913
1914            match result {
1915                redis::Value::Int(1) => continue,
1916                _ => break,
1917            }
1918        }
1919
1920        debug!("promote_jobs completed");
1921        Ok(())
1922    }
1923
1924    /// Override the rate limit to be active for the next jobs.
1925    ///
1926    /// Sets the rate limiter key to MAX value with the given TTL,
1927    /// preventing any new jobs from being processed until it expires.
1928    pub async fn rate_limit(&self, expire_time_ms: u64) -> Result<(), Error> {
1929        let limiter_key = self.keys.limiter();
1930        let mut conn = self.conn.conn();
1931
1932        redis::cmd("SET")
1933            .arg(&limiter_key)
1934            .arg("9007199254740991") // Number.MAX_SAFE_INTEGER
1935            .arg("PX")
1936            .arg(expire_time_ms)
1937            .query_async::<()>(&mut conn)
1938            .await?;
1939
1940        Ok(())
1941    }
1942
1943    /// Remove the rate limit key, allowing processing to resume immediately.
1944    pub async fn remove_rate_limit_key(&self) -> Result<bool, Error> {
1945        let limiter_key = self.keys.limiter();
1946        let mut conn = self.conn.conn();
1947
1948        let result: u32 = redis::cmd("DEL")
1949            .arg(&limiter_key)
1950            .query_async(&mut conn)
1951            .await?;
1952
1953        Ok(result > 0)
1954    }
1955
1956    /// Set global concurrency limit (stored in queue meta hash).
1957    /// Limits the total number of active jobs across all workers for this queue.
1958    pub async fn set_global_concurrency(&self, concurrency: u64) -> Result<(), Error> {
1959        let meta_key = self.keys.meta();
1960        let mut conn = self.conn.conn();
1961
1962        redis::cmd("HSET")
1963            .arg(&meta_key)
1964            .arg("concurrency")
1965            .arg(concurrency)
1966            .query_async::<()>(&mut conn)
1967            .await?;
1968
1969        Ok(())
1970    }
1971
1972    /// Remove global concurrency limit from queue meta.
1973    pub async fn remove_global_concurrency(&self) -> Result<(), Error> {
1974        let meta_key = self.keys.meta();
1975        let mut conn = self.conn.conn();
1976
1977        redis::cmd("HDEL")
1978            .arg(&meta_key)
1979            .arg("concurrency")
1980            .query_async::<()>(&mut conn)
1981            .await?;
1982
1983        Ok(())
1984    }
1985
1986    /// Set global rate limit (stored in queue meta hash).
1987    pub async fn set_global_rate_limit(&self, max: u64, duration: u64) -> Result<(), Error> {
1988        let meta_key = self.keys.meta();
1989        let mut conn = self.conn.conn();
1990
1991        redis::cmd("HSET")
1992            .arg(&meta_key)
1993            .arg("max")
1994            .arg(max)
1995            .arg("duration")
1996            .arg(duration)
1997            .query_async::<()>(&mut conn)
1998            .await?;
1999
2000        Ok(())
2001    }
2002
2003    /// Remove global rate limit values from queue meta.
2004    pub async fn remove_global_rate_limit(&self) -> Result<(), Error> {
2005        let meta_key = self.keys.meta();
2006        let mut conn = self.conn.conn();
2007
2008        redis::cmd("HDEL")
2009            .arg(&meta_key)
2010            .arg("max")
2011            .arg("duration")
2012            .query_async::<()>(&mut conn)
2013            .await?;
2014
2015        Ok(())
2016    }
2017
2018    /// Return the time-to-live (in ms) for the rate-limited key.
2019    ///
2020    /// `max_jobs` is the maximum number of jobs considered in the rate-limit
2021    /// state. When `None`, the remaining TTL is returned without checking
2022    /// whether the max is exceeded. Returns `0` when not rate limited and
2023    /// `-2`/`-1` mirror Redis `PTTL` semantics (no key / no expiry).
2024    pub async fn get_rate_limit_ttl(&self, max_jobs: Option<u64>) -> Result<i64, Error> {
2025        let script = self
2026            .conn
2027            .scripts()
2028            .get("getRateLimitTtl")
2029            .ok_or_else(|| Error::InvalidConfig("getRateLimitTtl script not found".to_string()))?
2030            .clone();
2031
2032        let keys = vec![self.keys.limiter(), self.keys.meta()];
2033        let max_jobs_str = max_jobs
2034            .map(|m| m.to_string())
2035            .unwrap_or_else(|| "0".to_string());
2036        let args: Vec<&[u8]> = vec![max_jobs_str.as_bytes()];
2037
2038        let mut conn = self.conn.conn();
2039        let result = script.execute(&mut conn, &keys, &args).await?;
2040
2041        match result {
2042            redis::Value::Int(n) => Ok(n),
2043            _ => Ok(0),
2044        }
2045    }
2046
2047    /// Return the global concurrency value, or `None` when not set.
2048    pub async fn get_global_concurrency(&self) -> Result<Option<u64>, Error> {
2049        let meta_key = self.keys.meta();
2050        let mut conn = self.conn.conn();
2051
2052        let value: Option<String> = redis::cmd("HGET")
2053            .arg(&meta_key)
2054            .arg("concurrency")
2055            .query_async(&mut conn)
2056            .await?;
2057
2058        Ok(value.and_then(|v| v.parse::<u64>().ok()))
2059    }
2060
2061    /// Return the global rate limit as `(max, duration)`, or `None` when not set.
2062    pub async fn get_global_rate_limit(&self) -> Result<Option<(u64, u64)>, Error> {
2063        let meta_key = self.keys.meta();
2064        let mut conn = self.conn.conn();
2065
2066        let values: Vec<Option<String>> = redis::cmd("HMGET")
2067            .arg(&meta_key)
2068            .arg("max")
2069            .arg("duration")
2070            .query_async(&mut conn)
2071            .await?;
2072
2073        let max = values
2074            .first()
2075            .and_then(|v| v.as_ref())
2076            .and_then(|v| v.parse::<u64>().ok());
2077        let duration = values
2078            .get(1)
2079            .and_then(|v| v.as_ref())
2080            .and_then(|v| v.parse::<u64>().ok());
2081
2082        match (max, duration) {
2083            (Some(m), Some(d)) => Ok(Some((m, d))),
2084            _ => Ok(None),
2085        }
2086    }
2087
2088    /// Remove a deduplication key if the stored job ID matches the given one.
2089    ///
2090    /// Uses the `removeDeduplicationKey` Lua script for atomic check-and-delete.
2091    pub async fn remove_deduplication_key(
2092        &self,
2093        deduplication_id: &str,
2094        job_id: &str,
2095    ) -> Result<bool, Error> {
2096        let script = self
2097            .conn
2098            .scripts()
2099            .get("removeDeduplicationKey")
2100            .ok_or_else(|| {
2101                Error::InvalidConfig("removeDeduplicationKey script not found".to_string())
2102            })?
2103            .clone();
2104
2105        let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2106        let keys = vec![dedup_key];
2107        let args: Vec<&[u8]> = vec![job_id.as_bytes()];
2108
2109        let mut conn = self.conn.conn();
2110        let result = script.execute(&mut conn, &keys, &args).await?;
2111
2112        match result {
2113            redis::Value::Int(1) => Ok(true),
2114            _ => Ok(false),
2115        }
2116    }
2117
2118    /// Get the job ID stored for a given deduplication ID.
2119    ///
2120    /// Returns `None` if no deduplication key exists.
2121    pub async fn get_deduplication_job_id(
2122        &self,
2123        deduplication_id: &str,
2124    ) -> Result<Option<String>, Error> {
2125        let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2126        let mut conn = self.conn.conn();
2127        let result: redis::Value = redis::cmd("GET")
2128            .arg(&dedup_key)
2129            .query_async(&mut conn)
2130            .await
2131            .map_err(Error::Redis)?;
2132
2133        match result {
2134            redis::Value::BulkString(bytes) => {
2135                Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
2136            }
2137            redis::Value::SimpleString(s) => Ok(Some(s)),
2138            redis::Value::Nil => Ok(None),
2139            _ => Ok(None),
2140        }
2141    }
2142
2143    /// Get the job ID that started a debounced state.
2144    ///
2145    /// **Deprecated:** use [`Queue::get_deduplication_job_id`] instead. Provided
2146    /// for parity with the legacy Node.js `Queue.getDebounceJobId`.
2147    pub async fn get_debounce_job_id(&self, id: &str) -> Result<Option<String>, Error> {
2148        self.get_deduplication_job_id(id).await
2149    }
2150
2151    /// Remove a debounce key unconditionally, returning the number of keys
2152    /// deleted (`0` or `1`).
2153    ///
2154    /// **Deprecated:** use [`Queue::remove_deduplication_key`] instead. Provided
2155    /// for parity with the legacy Node.js `Queue.removeDebounceKey`. Unlike the
2156    /// deduplication variant, this performs a plain `DEL` without checking the
2157    /// stored job ID.
2158    pub async fn remove_debounce_key(&self, id: &str) -> Result<u64, Error> {
2159        let dedup_key = format!("{}:de:{}", self.keys.base(), id);
2160        let mut conn = self.conn.conn();
2161        let deleted: u64 = redis::cmd("DEL")
2162            .arg(&dedup_key)
2163            .query_async(&mut conn)
2164            .await?;
2165        Ok(deleted)
2166    }
2167
2168    /// Get logs for a specific job.
2169    ///
2170    /// Returns the log entries and total count.
2171    pub async fn get_job_logs(
2172        &self,
2173        job_id: &str,
2174        start: isize,
2175        end: isize,
2176        asc: bool,
2177    ) -> Result<(Vec<String>, usize), Error> {
2178        let logs_key = format!("{}{}:logs", self.keys.key_prefix(), job_id);
2179        let mut conn = self.conn.conn();
2180
2181        let (logs, count): (Vec<String>, usize) = if asc {
2182            redis::pipe()
2183                .cmd("LRANGE")
2184                .arg(&logs_key)
2185                .arg(start)
2186                .arg(end)
2187                .cmd("LLEN")
2188                .arg(&logs_key)
2189                .query_async(&mut conn)
2190                .await?
2191        } else {
2192            let actual_start = -(end + 1);
2193            let actual_end = -(start + 1);
2194            let (mut logs, count): (Vec<String>, usize) = redis::pipe()
2195                .cmd("LRANGE")
2196                .arg(&logs_key)
2197                .arg(actual_start)
2198                .arg(actual_end)
2199                .cmd("LLEN")
2200                .arg(&logs_key)
2201                .query_async(&mut conn)
2202                .await?;
2203            logs.reverse();
2204            (logs, count)
2205        };
2206
2207        Ok((logs, count))
2208    }
2209
2210    /// Trim the event stream to approximately `max_length` entries.
2211    pub async fn trim_events(&self, max_length: usize) -> Result<usize, Error> {
2212        let mut conn = self.conn.conn();
2213        let trimmed: usize = redis::cmd("XTRIM")
2214            .arg(self.keys.events())
2215            .arg("MAXLEN")
2216            .arg("~")
2217            .arg(max_length)
2218            .query_async(&mut conn)
2219            .await?;
2220        Ok(trimmed)
2221    }
2222
2223    /// Update a job's progress by id, without loading the job first.
2224    ///
2225    /// Mirrors Node.js `Queue.updateJobProgress`: runs the `updateProgress`
2226    /// script which sets the job's `progress` field and emits a `progress`
2227    /// event on the queue's event stream.
2228    pub async fn update_job_progress(
2229        &self,
2230        job_id: &str,
2231        progress: crate::types::JobProgress,
2232    ) -> Result<(), Error> {
2233        let script = self
2234            .conn
2235            .scripts()
2236            .get("updateProgress")
2237            .ok_or_else(|| Error::InvalidConfig("updateProgress script not found".to_string()))?
2238            .clone();
2239
2240        let job_key = self.keys.job_key(job_id);
2241        let events_key = self.keys.events();
2242        let meta_key = self.keys.meta();
2243        let progress_json = serialize_progress_for_script(&progress)?;
2244
2245        let keys = vec![job_key, events_key, meta_key];
2246        let args: Vec<&[u8]> = vec![job_id.as_bytes(), progress_json.as_bytes()];
2247
2248        let mut conn = self.conn.conn();
2249        let result: redis::Value = script.execute(&mut conn, &keys, &args).await?;
2250
2251        match result {
2252            redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2253            _ => Ok(()),
2254        }
2255    }
2256
2257    /// Obliterate the queue (remove all keys).
2258    ///
2259    /// When `force` is true, automatically pauses the queue first.
2260    pub async fn obliterate(&self, force: bool, count: usize) -> Result<(), Error> {
2261        // The script requires the queue to be paused
2262        if force {
2263            let _ = self.pause().await;
2264        }
2265
2266        let script = self
2267            .conn
2268            .scripts()
2269            .get("obliterate")
2270            .ok_or_else(|| Error::InvalidConfig("obliterate script not found".to_string()))?
2271            .clone();
2272
2273        let keys = vec![self.keys.meta(), self.keys.key_prefix()];
2274        let count_str = count.to_string();
2275        let force_str = if force { "1" } else { "0" };
2276
2277        let mut conn = self.conn.conn();
2278        loop {
2279            let args: Vec<&[u8]> = vec![count_str.as_bytes(), force_str.as_bytes()];
2280            let result = script.execute(&mut conn, &keys, &args).await?;
2281
2282            match result {
2283                redis::Value::Nil => break,
2284                redis::Value::Int(0) => break,
2285                redis::Value::Int(1) => continue, // more to delete
2286                redis::Value::Int(code) if code < 0 => {
2287                    return Err(Error::from_script_code(code));
2288                }
2289                _ => break,
2290            }
2291        }
2292
2293        debug!(force, "queue obliterated");
2294        Ok(())
2295    }
2296
2297    // ── Job Scheduler Methods ────────────────────────────────────────────
2298
2299    /// Create or update a job scheduler.
2300    ///
2301    /// Creates a scheduled repeating job that will run on a cron pattern or at
2302    /// fixed intervals. The scheduler is persisted in Redis and will create
2303    /// the next delayed job automatically after each execution.
2304    ///
2305    /// # Arguments
2306    /// - `job_scheduler_id` — Unique ID for this scheduler.
2307    /// - `repeat_opts` — Schedule configuration (cron pattern or every-ms).
2308    /// - `job_name` — Name for the created jobs (defaults to scheduler ID).
2309    /// - `job_data` — JSON data for the created jobs.
2310    /// - `job_opts` — Options for the created jobs (attempts, backoff, etc.).
2311    pub async fn upsert_job_scheduler(
2312        &self,
2313        job_scheduler_id: &str,
2314        repeat_opts: crate::job_scheduler::RepeatOptions,
2315        job_name: Option<&str>,
2316        job_data: Option<serde_json::Value>,
2317        job_opts: Option<JobOptions>,
2318    ) -> Result<Option<Job>, Error> {
2319        use crate::job_scheduler::{next_cron_millis, pack_delayed_job_opts, pack_scheduler_opts};
2320
2321        // Validation
2322        if repeat_opts.pattern.is_some() && repeat_opts.every.is_some() {
2323            return Err(Error::InvalidConfig(
2324                "Both .pattern and .every options are defined; only one may be used".to_string(),
2325            ));
2326        }
2327        if repeat_opts.pattern.is_none() && repeat_opts.every.is_none() {
2328            return Err(Error::InvalidConfig(
2329                "Either .pattern or .every option must be defined".to_string(),
2330            ));
2331        }
2332        if repeat_opts.immediately == Some(true) && repeat_opts.start_date.is_some() {
2333            return Err(Error::InvalidConfig(
2334                "Both .immediately and .startDate options are defined; only one may be used"
2335                    .to_string(),
2336            ));
2337        }
2338
2339        let now = std::time::SystemTime::now()
2340            .duration_since(std::time::UNIX_EPOCH)
2341            .unwrap()
2342            .as_millis() as u64;
2343
2344        // Validate end date
2345        if let Some(end_date) = repeat_opts.end_date {
2346            if end_date < now {
2347                return Err(Error::InvalidConfig(
2348                    "End date must be greater than current timestamp".to_string(),
2349                ));
2350            }
2351        }
2352
2353        // Compute iteration count
2354        let iteration_count = repeat_opts.count.unwrap_or(0) + 1;
2355        if let Some(limit) = repeat_opts.limit {
2356            if iteration_count > limit {
2357                return Ok(None);
2358            }
2359        }
2360
2361        // Compute nextMillis for cron patterns
2362        let next_millis: Option<u64> = if let Some(ref pattern) = repeat_opts.pattern {
2363            if repeat_opts.immediately == Some(true) {
2364                Some(now)
2365            } else {
2366                next_cron_millis(
2367                    pattern,
2368                    now,
2369                    repeat_opts.tz.as_deref(),
2370                    repeat_opts.start_date,
2371                )?
2372            }
2373        } else {
2374            // For `every`, nextMillis is not computed here — the Lua script handles it
2375            None
2376        };
2377
2378        // We need either nextMillis or every to proceed
2379        if next_millis.is_none() && repeat_opts.every.is_none() {
2380            return Ok(None);
2381        }
2382
2383        let effective_name = job_name.unwrap_or(job_scheduler_id);
2384        let effective_data = job_data.unwrap_or(serde_json::json!({}));
2385        let effective_opts = job_opts.unwrap_or_default();
2386
2387        let offset = if repeat_opts.every.is_some() {
2388            repeat_opts.offset
2389        } else {
2390            None
2391        };
2392
2393        // Pack arguments for Lua script
2394        let scheduler_opts_packed = pack_scheduler_opts(effective_name, &repeat_opts);
2395        let template_data_json =
2396            serde_json::to_string(&effective_data).unwrap_or_else(|_| "{}".to_string());
2397        let template_opts_packed = self.pack_job_opts_from_options(&effective_opts);
2398        let delayed_job_opts_packed = pack_delayed_job_opts(
2399            &effective_opts,
2400            job_scheduler_id,
2401            next_millis.unwrap_or(0),
2402            iteration_count,
2403            offset,
2404            &repeat_opts,
2405        );
2406
2407        let script = self
2408            .conn
2409            .scripts()
2410            .get("addJobScheduler")
2411            .ok_or_else(|| Error::InvalidConfig("addJobScheduler script not found".to_string()))?
2412            .clone();
2413
2414        // KEYS[1-11]
2415        let keys = vec![
2416            self.keys.repeat(),      // KEYS[1]
2417            self.keys.delayed(),     // KEYS[2]
2418            self.keys.wait(),        // KEYS[3]
2419            self.keys.paused(),      // KEYS[4]
2420            self.keys.meta(),        // KEYS[5]
2421            self.keys.prioritized(), // KEYS[6]
2422            self.keys.marker(),      // KEYS[7]
2423            self.keys.id(),          // KEYS[8]
2424            self.keys.events(),      // KEYS[9]
2425            self.keys.pc(),          // KEYS[10]
2426            self.keys.active(),      // KEYS[11]
2427        ];
2428
2429        // ARGV
2430        let next_millis_str = next_millis.unwrap_or(0).to_string();
2431        let timestamp_str = now.to_string();
2432        let prefix = self.keys.key_prefix();
2433        let producer_key = Vec::new(); // empty for non-flow jobs
2434
2435        let args: Vec<&[u8]> = vec![
2436            next_millis_str.as_bytes(),    // ARGV[1]
2437            &scheduler_opts_packed,        // ARGV[2]
2438            job_scheduler_id.as_bytes(),   // ARGV[3]
2439            template_data_json.as_bytes(), // ARGV[4]
2440            &template_opts_packed,         // ARGV[5]
2441            &delayed_job_opts_packed,      // ARGV[6]
2442            timestamp_str.as_bytes(),      // ARGV[7]
2443            prefix.as_bytes(),             // ARGV[8]
2444            &producer_key,                 // ARGV[9]
2445        ];
2446
2447        let mut conn = self.conn.conn();
2448        let result = script.execute(&mut conn, &keys, &args).await?;
2449
2450        // Parse result: the script returns [jobId, delay] on success
2451        match result {
2452            redis::Value::Array(ref arr) if arr.len() >= 2 => {
2453                let job_id = match &arr[0] {
2454                    redis::Value::BulkString(bytes) => String::from_utf8_lossy(bytes).to_string(),
2455                    redis::Value::SimpleString(s) => s.clone(),
2456                    redis::Value::Int(n) => n.to_string(),
2457                    _ => return Ok(None),
2458                };
2459
2460                let mut job = Job::new(effective_name, effective_data, Some(effective_opts));
2461                job.set_id(job_id);
2462                job.set_context(self.make_script_context());
2463                Ok(Some(job))
2464            }
2465            redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2466            _ => Ok(None),
2467        }
2468    }
2469
2470    /// Get a job scheduler by its ID.
2471    pub async fn get_job_scheduler(
2472        &self,
2473        job_scheduler_id: &str,
2474    ) -> Result<Option<crate::job_scheduler::JobSchedulerJson>, Error> {
2475        let script = self
2476            .conn
2477            .scripts()
2478            .get("getJobScheduler")
2479            .ok_or_else(|| Error::InvalidConfig("getJobScheduler script not found".to_string()))?
2480            .clone();
2481
2482        let keys = vec![self.keys.repeat()];
2483        let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes()];
2484
2485        let mut conn = self.conn.conn();
2486        let result = script.execute(&mut conn, &keys, &args).await?;
2487
2488        // Result is [hash_fields_array, score_string]
2489        match result {
2490            redis::Value::Array(ref arr) if arr.len() >= 2 => {
2491                let fields = Self::parse_hash_array(&arr[0]);
2492                if fields.is_empty() {
2493                    return Ok(None);
2494                }
2495                let next_millis = match &arr[1] {
2496                    redis::Value::BulkString(bytes) => {
2497                        String::from_utf8_lossy(bytes).parse::<u64>().ok()
2498                    }
2499                    redis::Value::SimpleString(s) => s.parse::<u64>().ok(),
2500                    redis::Value::Int(n) => Some(*n as u64),
2501                    _ => None,
2502                };
2503                Ok(Some(crate::job_scheduler::parse_scheduler_hash(
2504                    job_scheduler_id,
2505                    &fields,
2506                    next_millis,
2507                )))
2508            }
2509            redis::Value::Nil => Ok(None),
2510            redis::Value::Array(ref arr) if arr.is_empty() => Ok(None),
2511            _ => Ok(None),
2512        }
2513    }
2514
2515    /// Get a paginated list of job schedulers.
2516    ///
2517    /// Returns schedulers ordered by next execution time.
2518    pub async fn get_job_schedulers(
2519        &self,
2520        start: isize,
2521        end: isize,
2522        asc: bool,
2523    ) -> Result<Vec<crate::job_scheduler::JobSchedulerJson>, Error> {
2524        let mut conn = self.conn.conn();
2525        let repeat_key = self.keys.repeat();
2526
2527        // Get members with scores
2528        let results: Vec<(String, f64)> = if asc {
2529            redis::cmd("ZRANGE")
2530                .arg(&repeat_key)
2531                .arg(start)
2532                .arg(end)
2533                .arg("WITHSCORES")
2534                .query_async(&mut conn)
2535                .await?
2536        } else {
2537            redis::cmd("ZREVRANGE")
2538                .arg(&repeat_key)
2539                .arg(start)
2540                .arg(end)
2541                .arg("WITHSCORES")
2542                .query_async(&mut conn)
2543                .await?
2544        };
2545
2546        let mut schedulers = Vec::with_capacity(results.len());
2547        for (scheduler_id, score) in &results {
2548            let scheduler_hash_key = format!("{}repeat:{}", self.keys.key_prefix(), scheduler_id);
2549            let fields: std::collections::HashMap<String, String> = redis::cmd("HGETALL")
2550                .arg(&scheduler_hash_key)
2551                .query_async(&mut conn)
2552                .await?;
2553
2554            if !fields.is_empty() {
2555                schedulers.push(crate::job_scheduler::parse_scheduler_hash(
2556                    scheduler_id,
2557                    &fields,
2558                    Some(*score as u64),
2559                ));
2560            }
2561        }
2562
2563        Ok(schedulers)
2564    }
2565
2566    /// Get the total number of job schedulers.
2567    pub async fn get_job_schedulers_count(&self) -> Result<u64, Error> {
2568        let mut conn = self.conn.conn();
2569        let count: u64 = redis::cmd("ZCARD")
2570            .arg(self.keys.repeat())
2571            .query_async(&mut conn)
2572            .await?;
2573        Ok(count)
2574    }
2575
2576    /// Remove a job scheduler by its ID.
2577    ///
2578    /// Also removes the next scheduled delayed job if one exists.
2579    /// Returns `true` if the scheduler was found and removed.
2580    pub async fn remove_job_scheduler(&self, job_scheduler_id: &str) -> Result<bool, Error> {
2581        let script = self
2582            .conn
2583            .scripts()
2584            .get("removeJobScheduler")
2585            .ok_or_else(|| Error::InvalidConfig("removeJobScheduler script not found".to_string()))?
2586            .clone();
2587
2588        let keys = vec![self.keys.repeat(), self.keys.delayed(), self.keys.events()];
2589
2590        let prefix = self.keys.key_prefix();
2591        let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes(), prefix.as_bytes()];
2592
2593        let mut conn = self.conn.conn();
2594        let result = script.execute(&mut conn, &keys, &args).await?;
2595
2596        match result {
2597            redis::Value::Int(0) => Ok(true),  // 0 = success (removed)
2598            redis::Value::Int(1) => Ok(false), // 1 = not found
2599            _ => Ok(false),
2600        }
2601    }
2602
2603    /// Parse a Redis hash array (alternating key/value) into a HashMap.
2604    fn parse_hash_array(value: &redis::Value) -> std::collections::HashMap<String, String> {
2605        let mut map = std::collections::HashMap::new();
2606        if let redis::Value::Array(arr) = value {
2607            let mut iter = arr.iter();
2608            while let (Some(key_val), Some(val_val)) = (iter.next(), iter.next()) {
2609                let key = match key_val {
2610                    redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2611                    redis::Value::SimpleString(s) => s.clone(),
2612                    _ => continue,
2613                };
2614                let val = match val_val {
2615                    redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2616                    redis::Value::SimpleString(s) => s.clone(),
2617                    redis::Value::Int(n) => n.to_string(),
2618                    _ => String::new(),
2619                };
2620                map.insert(key, val);
2621            }
2622        }
2623        map
2624    }
2625
2626    /// Pack job options from a JobOptions struct into msgpack (for template opts).
2627    fn pack_job_opts_from_options(&self, opts: &JobOptions) -> Vec<u8> {
2628        use rmp::encode::*;
2629
2630        let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
2631
2632        if let Some(attempts) = opts.attempts {
2633            let mut b = Vec::new();
2634            write_uint(&mut b, attempts as u64).unwrap();
2635            entries.push(("attempts", b));
2636        }
2637        if let Some(ref backoff) = opts.backoff {
2638            let b = Self::encode_backoff(backoff);
2639            entries.push(("backoff", b));
2640        }
2641        if let Some(ref roc) = opts.remove_on_complete {
2642            let b = Self::encode_remove_on_finish(roc);
2643            entries.push(("removeOnComplete", b));
2644        }
2645        if let Some(ref rof) = opts.remove_on_fail {
2646            let b = Self::encode_remove_on_finish(rof);
2647            entries.push(("removeOnFail", b));
2648        }
2649        if let Some(priority) = opts.priority {
2650            if priority > 0 {
2651                let mut b = Vec::new();
2652                write_uint(&mut b, priority as u64).unwrap();
2653                entries.push(("priority", b));
2654            }
2655        }
2656
2657        let mut buf = Vec::with_capacity(64);
2658        write_map_len(&mut buf, entries.len() as u32).unwrap();
2659        for (key, val) in &entries {
2660            write_str(&mut buf, key).unwrap();
2661            buf.extend_from_slice(val);
2662        }
2663        buf
2664    }
2665
2666    /// Close the queue connection.
2667    pub async fn close(&self) {
2668        self.conn.close().await;
2669    }
2670}
2671
2672/// Fluent builder returned by [`Queue::add`].
2673///
2674/// `AddJob` implements [`IntoFuture`], so awaiting it adds the job with the
2675/// accumulated options. Setters are infallible and chainable; any invalid
2676/// configuration (or a data serialization failure) is reported when the builder
2677/// is awaited.
2678#[must_use = "an AddJob does nothing until awaited"]
2679pub struct AddJob<'a> {
2680    queue: &'a Queue,
2681    name: String,
2682    data: Result<serde_json::Value, Error>,
2683    opts: JobOptions,
2684}
2685
2686impl<'a> AddJob<'a> {
2687    fn new<T: Serialize>(queue: &'a Queue, name: &str, data: T) -> Self {
2688        Self {
2689            queue,
2690            name: name.to_string(),
2691            data: serde_json::to_value(data).map_err(Error::from),
2692            opts: JobOptions::default(),
2693        }
2694    }
2695
2696    /// Use `opts` as the base options, replacing anything set so far.
2697    ///
2698    /// Useful for reusing a prepared [`JobOptions`]; subsequent setters still
2699    /// override individual fields.
2700    pub fn options(mut self, opts: JobOptions) -> Self {
2701        self.opts = opts;
2702        self
2703    }
2704
2705    /// Delay before the job becomes available for processing.
2706    pub fn delay(mut self, delay: Duration) -> Self {
2707        self.opts.delay = Some(duration_as_millis(delay));
2708        self
2709    }
2710
2711    /// Job priority. Lower values are processed first; unset (or `0`) means the
2712    /// job is not prioritized and follows normal FIFO/LIFO ordering.
2713    pub fn priority(mut self, priority: u32) -> Self {
2714        self.opts.priority = Some(priority);
2715        self
2716    }
2717
2718    /// Total number of attempts before the job permanently fails.
2719    pub fn attempts(mut self, attempts: u32) -> Self {
2720        self.opts.attempts = Some(attempts);
2721        self
2722    }
2723
2724    /// Backoff strategy applied between retries.
2725    pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2726        self.opts.backoff = Some(backoff);
2727        self
2728    }
2729
2730    /// Add the job to the back of the queue (last-in-first-out ordering).
2731    pub fn lifo(mut self) -> Self {
2732        self.opts.lifo = Some(true);
2733        self
2734    }
2735
2736    /// Use a custom, unique job id instead of an auto-generated one.
2737    pub fn job_id(mut self, id: impl Into<String>) -> Self {
2738        self.opts.job_id = Some(id.into());
2739        self
2740    }
2741
2742    /// Completed-job retention policy.
2743    pub fn remove_on_complete(mut self, policy: RemoveOnFinish) -> Self {
2744        self.opts.remove_on_complete = Some(policy);
2745        self
2746    }
2747
2748    /// Failed-job retention policy.
2749    pub fn remove_on_fail(mut self, policy: RemoveOnFinish) -> Self {
2750        self.opts.remove_on_fail = Some(policy);
2751        self
2752    }
2753
2754    /// Maximum number of log entries to retain for the job.
2755    pub fn keep_logs(mut self, count: u32) -> Self {
2756        self.opts.keep_logs = Some(count);
2757        self
2758    }
2759
2760    /// Attach this job to a parent job (flow / dependency chains).
2761    pub fn parent(mut self, parent: ParentOptions) -> Self {
2762        self.opts.parent = Some(parent);
2763        self
2764    }
2765
2766    /// Deduplicate the job using the given options.
2767    pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2768        self.opts.deduplication = Some(deduplication);
2769        self
2770    }
2771
2772    /// Reject the job if its serialized payload exceeds `bytes` UTF-8 bytes.
2773    pub fn size_limit(mut self, bytes: usize) -> Self {
2774        self.opts.size_limit = Some(bytes);
2775        self
2776    }
2777}
2778
2779impl<'a> IntoFuture for AddJob<'a> {
2780    type Output = Result<Job, Error>;
2781    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
2782
2783    fn into_future(self) -> Self::IntoFuture {
2784        Box::pin(async move {
2785            let data = self.data?;
2786            self.queue
2787                .add_internal(&self.name, data, Some(self.opts))
2788                .await
2789        })
2790    }
2791}
2792
2793/// A single job definition for [`Queue::add_bulk`].
2794///
2795/// Construct with [`BulkJob::new`] (or [`BulkJob::with_options`]) and, if
2796/// needed, attach options fluently or via [`BulkJob::options`]. `data` may be
2797/// any [`Serialize`] type; it is serialized internally and any failure surfaces
2798/// when the batch is added.
2799///
2800/// ```no_run
2801/// # async fn demo(queue: bullmq::Queue) -> bullmq::Result<()> {
2802/// use std::time::Duration;
2803/// use bullmq::BulkJob;
2804///
2805/// queue
2806///     .add_bulk(vec![
2807///         BulkJob::new("email", serde_json::json!({ "to": "a@example.com" })),
2808///         BulkJob::new("email", serde_json::json!({ "to": "b@example.com" }))
2809///             .delay(Duration::from_secs(60))
2810///             .priority(5),
2811///     ])
2812///     .await?;
2813/// # Ok(()) }
2814/// ```
2815#[must_use = "a BulkJob must be passed to Queue::add_bulk to have any effect"]
2816pub struct BulkJob {
2817    name: String,
2818    data: Result<serde_json::Value, Error>,
2819    opts: JobOptions,
2820}
2821
2822impl BulkJob {
2823    /// Create a bulk job entry with default options.
2824    pub fn new<T: Serialize>(name: impl Into<String>, data: T) -> Self {
2825        Self {
2826            name: name.into(),
2827            data: serde_json::to_value(data).map_err(Error::from),
2828            opts: JobOptions::default(),
2829        }
2830    }
2831
2832    /// Create a bulk job entry with explicit options.
2833    pub fn with_options<T: Serialize>(name: impl Into<String>, data: T, opts: JobOptions) -> Self {
2834        Self {
2835            name: name.into(),
2836            data: serde_json::to_value(data).map_err(Error::from),
2837            opts,
2838        }
2839    }
2840
2841    /// Use `opts` as the options, replacing anything set so far.
2842    pub fn options(mut self, opts: JobOptions) -> Self {
2843        self.opts = opts;
2844        self
2845    }
2846
2847    /// Delay before the job becomes available for processing.
2848    pub fn delay(mut self, delay: Duration) -> Self {
2849        self.opts = std::mem::take(&mut self.opts).delay(delay);
2850        self
2851    }
2852
2853    /// Job priority. Lower values are processed first; unset (or `0`) means the
2854    /// job is not prioritized.
2855    pub fn priority(mut self, priority: u32) -> Self {
2856        self.opts = std::mem::take(&mut self.opts).priority(priority);
2857        self
2858    }
2859
2860    /// Total number of attempts before the job permanently fails.
2861    pub fn attempts(mut self, attempts: u32) -> Self {
2862        self.opts = std::mem::take(&mut self.opts).attempts(attempts);
2863        self
2864    }
2865
2866    /// Backoff strategy applied between retries.
2867    pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2868        self.opts = std::mem::take(&mut self.opts).backoff(backoff);
2869        self
2870    }
2871
2872    /// Add the job to the back of the queue (last-in-first-out ordering).
2873    pub fn lifo(mut self) -> Self {
2874        self.opts = std::mem::take(&mut self.opts).lifo();
2875        self
2876    }
2877
2878    /// Use a custom, unique job id instead of an auto-generated one.
2879    pub fn job_id(mut self, id: impl Into<String>) -> Self {
2880        self.opts = std::mem::take(&mut self.opts).job_id(id);
2881        self
2882    }
2883
2884    /// Attach this job to a parent job (flow / dependency chains).
2885    pub fn parent(mut self, parent: ParentOptions) -> Self {
2886        self.opts = std::mem::take(&mut self.opts).parent(parent);
2887        self
2888    }
2889
2890    /// Deduplicate the job using the given options.
2891    pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2892        self.opts = std::mem::take(&mut self.opts).deduplication(deduplication);
2893        self
2894    }
2895
2896    /// Reject the job if its serialized payload exceeds `bytes` UTF-8 bytes.
2897    pub fn size_limit(mut self, bytes: usize) -> Self {
2898        self.opts = std::mem::take(&mut self.opts).size_limit(bytes);
2899        self
2900    }
2901}
2902
2903/// Convert a [`Duration`] to whole milliseconds, saturating at [`u64::MAX`].
2904fn duration_as_millis(duration: Duration) -> u64 {
2905    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2906}
2907
2908/// Escape a Prometheus label value (`\`, `"`, and newlines).
2909///
2910/// Mirrors Node.js `escapePrometheusLabelValue`.
2911fn escape_prometheus_label_value(value: &str) -> String {
2912    value
2913        .replace('\\', "\\\\")
2914        .replace('"', "\\\"")
2915        .replace('\n', "\\n")
2916}
2917
2918/// Parse the output of Redis `CLIENT LIST` and return the info maps for clients
2919/// belonging to this queue.
2920/// Mirrors Node.js `QueueGetters.parseClientList`: each line is a space-
2921/// separated set of `key=value` pairs. A client matches when its `name` equals
2922/// `unnamed` (an unnamed worker) or starts with `named_prefix` (a named worker,
2923/// `{clientName}:w:`). For matches, `name` is replaced with the queue name and
2924/// the raw client name is kept under `rawname`.
2925fn parse_client_list(
2926    list: &str,
2927    queue_name: &str,
2928    unnamed: &str,
2929    named_prefix: &str,
2930) -> Vec<HashMap<String, String>> {
2931    let mut clients = Vec::new();
2932
2933    for line in list.split(['\r', '\n']).filter(|l| !l.is_empty()) {
2934        let mut client: HashMap<String, String> = HashMap::new();
2935        for key_value in line.split(' ') {
2936            if let Some(idx) = key_value.find('=') {
2937                let key = &key_value[..idx];
2938                let value = &key_value[idx + 1..];
2939                client.insert(key.to_string(), value.to_string());
2940            }
2941        }
2942
2943        let name = client.get("name").cloned().unwrap_or_default();
2944        if !name.is_empty() && (name == unnamed || name.starts_with(named_prefix)) {
2945            client.insert("name".to_string(), queue_name.to_string());
2946            client.insert("rawname".to_string(), name);
2947            clients.push(client);
2948        }
2949    }
2950
2951    clients
2952}
2953
2954fn serialize_progress_for_script(progress: &crate::types::JobProgress) -> Result<String, Error> {
2955    match progress {
2956        // Match Node.js behavior: JSON.stringify(NaN/±Infinity) -> "null".
2957        crate::types::JobProgress::Number(n) if !n.is_finite() => Ok("null".to_string()),
2958        _ => Ok(serde_json::to_string(progress)?),
2959    }
2960}
2961
2962#[cfg(test)]
2963mod client_list_tests {
2964    use super::parse_client_list;
2965
2966    #[test]
2967    fn matches_unnamed_and_named_workers() {
2968        let unnamed = "bull:dGVzdA==";
2969        let named_prefix = "bull:dGVzdA==:w:";
2970        let list = format!(
2971            "id=1 addr=127.0.0.1:1 name={unnamed} age=5\n\
2972             id=2 addr=127.0.0.1:2 name={named_prefix}alpha age=3\n\
2973             id=3 addr=127.0.0.1:3 name=bull:b3RoZXI= age=1\n\
2974             id=4 addr=127.0.0.1:4 name= age=1\n"
2975        );
2976
2977        let clients = parse_client_list(&list, "test", unnamed, named_prefix);
2978        assert_eq!(clients.len(), 2);
2979        assert_eq!(clients[0].get("name"), Some(&"test".to_string()));
2980        assert_eq!(clients[0].get("rawname"), Some(&unnamed.to_string()));
2981        assert_eq!(
2982            clients[1].get("rawname"),
2983            Some(&format!("{named_prefix}alpha"))
2984        );
2985        assert_eq!(clients[1].get("name"), Some(&"test".to_string()));
2986    }
2987
2988    #[test]
2989    fn returns_empty_when_no_match() {
2990        let list = "id=1 addr=127.0.0.1:1 name=other age=5\n";
2991        let clients = parse_client_list(list, "test", "bull:dGVzdA==", "bull:dGVzdA==:w:");
2992        assert!(clients.is_empty());
2993    }
2994}
2995
2996#[cfg(test)]
2997mod prometheus_tests {
2998    use super::escape_prometheus_label_value;
2999
3000    #[test]
3001    fn escapes_special_characters() {
3002        assert_eq!(escape_prometheus_label_value("plain"), "plain");
3003        assert_eq!(escape_prometheus_label_value("a\\b"), "a\\\\b");
3004        assert_eq!(escape_prometheus_label_value("a\"b"), "a\\\"b");
3005        assert_eq!(escape_prometheus_label_value("a\nb"), "a\\nb");
3006    }
3007}
3008
3009#[cfg(test)]
3010mod progress_serialization_tests {
3011    use super::serialize_progress_for_script;
3012    use crate::types::JobProgress;
3013
3014    #[test]
3015    fn serializes_non_finite_numbers_as_null() {
3016        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3017            let serialized = serialize_progress_for_script(&JobProgress::Number(value)).unwrap();
3018            assert_eq!(serialized, "null");
3019        }
3020    }
3021
3022    #[test]
3023    fn serializes_regular_values_as_json() {
3024        let serialized = serialize_progress_for_script(&JobProgress::Number(42.0)).unwrap();
3025        assert_eq!(serialized, "42.0");
3026    }
3027}