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![self.keys.wait(), self.keys.prioritized()];
1193        let prio_strs: Vec<String> = unique.iter().map(|p| p.to_string()).collect();
1194        let args: Vec<&[u8]> = prio_strs.iter().map(|s| s.as_bytes()).collect();
1195
1196        let mut conn = self.conn.conn();
1197        let result = script.execute(&mut conn, &keys, &args).await?;
1198
1199        let counts: Vec<u64> = match result {
1200            redis::Value::Array(arr) => arr
1201                .into_iter()
1202                .map(|v| match v {
1203                    redis::Value::Int(n) => n.max(0) as u64,
1204                    _ => 0,
1205                })
1206                .collect(),
1207            _ => Vec::new(),
1208        };
1209
1210        let mut map = HashMap::new();
1211        for (i, p) in unique.iter().enumerate() {
1212            map.insert(*p, counts.get(i).copied().unwrap_or(0));
1213        }
1214        Ok(map)
1215    }
1216
1217    /// Return the number of jobs in the `completed` state.
1218    pub async fn get_completed_count(&self) -> Result<u64, Error> {
1219        self.get_job_count_by_types(&["completed"]).await
1220    }
1221
1222    /// Return the number of jobs in the `failed` state.
1223    pub async fn get_failed_count(&self) -> Result<u64, Error> {
1224        self.get_job_count_by_types(&["failed"]).await
1225    }
1226
1227    /// Return the number of jobs in the `delayed` state.
1228    pub async fn get_delayed_count(&self) -> Result<u64, Error> {
1229        self.get_job_count_by_types(&["delayed"]).await
1230    }
1231
1232    /// Return the number of jobs in the `active` state.
1233    pub async fn get_active_count(&self) -> Result<u64, Error> {
1234        self.get_job_count_by_types(&["active"]).await
1235    }
1236
1237    /// Return the number of jobs in the `prioritized` state.
1238    pub async fn get_prioritized_count(&self) -> Result<u64, Error> {
1239        self.get_job_count_by_types(&["prioritized"]).await
1240    }
1241
1242    /// Return the number of jobs in the `waiting` (and `paused`) state.
1243    pub async fn get_waiting_count(&self) -> Result<u64, Error> {
1244        self.get_job_count_by_types(&["waiting"]).await
1245    }
1246
1247    /// Return the number of jobs in the `waiting-children` state.
1248    pub async fn get_waiting_children_count(&self) -> Result<u64, Error> {
1249        self.get_job_count_by_types(&["waiting-children"]).await
1250    }
1251
1252    /// Return the list of workers currently connected to this queue.
1253    ///
1254    /// Workers register themselves via `CLIENT SETNAME` on their blocking
1255    /// connection; this method runs `CLIENT LIST` and returns the parsed info
1256    /// maps for clients whose name matches this queue. Each map's `name` field
1257    /// is set to the queue name and the original client name is preserved in
1258    /// `rawname` (mirroring Node.js `Queue.getWorkers`).
1259    ///
1260    /// Note: some managed Redis providers (e.g. GCP Memorystore) do not support
1261    /// `CLIENT SETNAME`/`CLIENT LIST`, in which case this returns an empty list.
1262    pub async fn get_workers(&self) -> Result<Vec<HashMap<String, String>>, Error> {
1263        let mut cmd = redis::cmd("CLIENT");
1264        cmd.arg("LIST");
1265        let list: String = match self.conn.cmd(&mut cmd).await {
1266            Ok(list) => list,
1267            Err(err) => {
1268                debug!(error = %err, "CLIENT LIST unavailable; returning empty worker list");
1269                return Ok(Vec::new());
1270            }
1271        };
1272
1273        let unnamed = self.keys.client_name("");
1274        let named_prefix = self.keys.client_name(":w:");
1275        Ok(parse_client_list(
1276            &list,
1277            &self.name,
1278            &unnamed,
1279            &named_prefix,
1280        ))
1281    }
1282
1283    /// Return the number of workers currently connected to this queue.
1284    pub async fn get_workers_count(&self) -> Result<usize, Error> {
1285        Ok(self.get_workers().await?.len())
1286    }
1287
1288    /// Return the queue's public metadata (read from the `meta` hash).
1289    ///
1290    /// Well-known numeric/boolean fields (`concurrency`, `max`, `duration`,
1291    /// `opts.maxLenEvents`, `paused`) are parsed into typed fields; any other
1292    /// entries are preserved in [`QueueMeta::other`]. Mirrors Node.js
1293    /// `Queue.getMeta`.
1294    pub async fn get_meta(&self) -> Result<QueueMeta, Error> {
1295        let mut conn = self.conn.conn();
1296        let config: HashMap<String, String> = redis::cmd("HGETALL")
1297            .arg(self.keys.meta())
1298            .query_async(&mut conn)
1299            .await?;
1300
1301        let mut meta = QueueMeta::default();
1302        for (key, value) in config {
1303            match key.as_str() {
1304                "concurrency" => meta.concurrency = value.parse().ok(),
1305                "max" => meta.max = value.parse().ok(),
1306                "duration" => meta.duration = value.parse().ok(),
1307                "opts.maxLenEvents" => meta.max_len_events = value.parse().ok(),
1308                "paused" => meta.paused = value == "1",
1309                _ => {
1310                    meta.other.insert(key, value);
1311                }
1312            }
1313        }
1314
1315        Ok(meta)
1316    }
1317
1318    /// Return the library version string stored in the `meta` hash.
1319    ///
1320    /// The Rust port records `bullmq-official:<version>` under the `library` field
1321    /// when the queue is created. Returns `None` if the field is unset.
1322    pub async fn get_version(&self) -> Result<Option<String>, Error> {
1323        let mut conn = self.conn.conn();
1324        let value: Option<String> = redis::cmd("HGET")
1325            .arg(self.keys.meta())
1326            .arg("library")
1327            .query_async(&mut conn)
1328            .await?;
1329        Ok(value)
1330    }
1331
1332    /// Return `true` when the number of active jobs has reached the queue's
1333    /// global concurrency limit. Returns `false` when no global concurrency is
1334    /// configured. Mirrors Node.js `Queue.isMaxed`.
1335    pub async fn is_maxed(&self) -> Result<bool, Error> {
1336        let script = self
1337            .conn
1338            .scripts()
1339            .get("isMaxed")
1340            .ok_or_else(|| Error::InvalidConfig("isMaxed script not found".to_string()))?
1341            .clone();
1342
1343        let keys = vec![self.keys.meta(), self.keys.active()];
1344        let args: Vec<&[u8]> = vec![];
1345
1346        let mut conn = self.conn.conn();
1347        let result = script.execute(&mut conn, &keys, &args).await?;
1348        Ok(matches!(result, redis::Value::Int(1)) || matches!(result, redis::Value::Boolean(true)))
1349    }
1350
1351    /// Export the queue's job counts and totals in the Prometheus text
1352    /// exposition format.
1353    ///
1354    /// Emits a `bullmq_job_count` gauge per job state plus
1355    /// `bullmq_job_completed_total` / `bullmq_job_failed_total` counters sourced
1356    /// from the time-series metrics. `global_labels` are appended (in order) as
1357    /// extra labels on every series; pass `&[]` for none. Mirrors Node.js
1358    /// `Queue.exportPrometheusMetrics`.
1359    pub async fn export_prometheus_metrics(
1360        &self,
1361        global_labels: &[(&str, &str)],
1362    ) -> Result<String, Error> {
1363        let counts = self.get_job_counts().await?;
1364        let mut metrics: Vec<String> = Vec::new();
1365
1366        metrics.push("# HELP bullmq_job_count Number of jobs in the queue by state".to_string());
1367        metrics.push("# TYPE bullmq_job_count gauge".to_string());
1368
1369        let escaped_queue = escape_prometheus_label_value(&self.name);
1370        let variables: String = global_labels
1371            .iter()
1372            .map(|(k, v)| format!(", {}=\"{}\"", k, escape_prometheus_label_value(v)))
1373            .collect();
1374
1375        let states: [(&str, u64); 8] = [
1376            ("active", counts.active),
1377            ("completed", counts.completed),
1378            ("delayed", counts.delayed),
1379            ("failed", counts.failed),
1380            ("paused", counts.paused),
1381            ("prioritized", counts.prioritized),
1382            ("waiting", counts.waiting),
1383            ("waiting-children", counts.waiting_children),
1384        ];
1385        for (state, count) in states {
1386            metrics.push(format!(
1387                "bullmq_job_count{{queue=\"{escaped_queue}\", state=\"{state}\"{variables}}} {count}"
1388            ));
1389        }
1390
1391        let completed_metrics = self.get_metrics("completed", 0, -1).await?;
1392        let failed_metrics = self.get_metrics("failed", 0, -1).await?;
1393
1394        metrics
1395            .push("# HELP bullmq_job_completed_total Total number of completed jobs".to_string());
1396        metrics.push("# TYPE bullmq_job_completed_total counter".to_string());
1397        metrics.push(format!(
1398            "bullmq_job_completed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1399            completed_metrics.meta.count
1400        ));
1401
1402        metrics.push("# HELP bullmq_job_failed_total Total number of failed jobs".to_string());
1403        metrics.push("# TYPE bullmq_job_failed_total counter".to_string());
1404        metrics.push(format!(
1405            "bullmq_job_failed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1406            failed_metrics.meta.count
1407        ));
1408
1409        Ok(metrics.join("\n"))
1410    }
1411
1412    /// Return the time-series metrics for the queue.
1413    ///
1414    /// `metric_type` must be `"completed"` or `"failed"`. Metrics are recorded
1415    /// per minute by workers configured with the `metrics` option. `start`/`end`
1416    /// are zero-based indices into the data points where `0` is the newest.
1417    pub async fn get_metrics(
1418        &self,
1419        metric_type: &str,
1420        start: i64,
1421        end: i64,
1422    ) -> Result<crate::types::Metrics, Error> {
1423        // Node.js restricts the metric type to `completed`/`failed` at the type
1424        // level; in Rust we validate at runtime so an invalid name returns an
1425        // error instead of silently reading non-existent keys and returning
1426        // empty metrics.
1427        if metric_type != "completed" && metric_type != "failed" {
1428            return Err(Error::InvalidConfig(format!(
1429                "metric type must be \"completed\" or \"failed\", got \"{}\"",
1430                metric_type
1431            )));
1432        }
1433
1434        let script = self
1435            .conn
1436            .scripts()
1437            .get("getMetrics")
1438            .ok_or_else(|| Error::InvalidConfig("getMetrics script not found".to_string()))?
1439            .clone();
1440
1441        let metrics_key = self.keys.get(&format!("metrics:{}", metric_type));
1442        let data_key = self.keys.get(&format!("metrics:{}:data", metric_type));
1443        let keys = vec![metrics_key, data_key];
1444
1445        let start_s = start.to_string();
1446        let end_s = end.to_string();
1447        let args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes()];
1448
1449        let mut conn = self.conn.conn();
1450        let result = script.execute(&mut conn, &keys, &args).await?;
1451
1452        // The script returns [meta(array of 3), data(array), count(int)].
1453        let parse_u64 = |v: &redis::Value| -> u64 {
1454            match v {
1455                redis::Value::BulkString(b) => String::from_utf8_lossy(b).parse().unwrap_or(0),
1456                redis::Value::SimpleString(s) => s.parse().unwrap_or(0),
1457                redis::Value::Int(n) => (*n).max(0) as u64,
1458                _ => 0,
1459            }
1460        };
1461
1462        let mut metrics = crate::types::Metrics::default();
1463        if let redis::Value::Array(parts) = result {
1464            if let Some(redis::Value::Array(meta)) = parts.first() {
1465                metrics.meta.count = meta.first().map(parse_u64).unwrap_or(0);
1466                metrics.meta.prev_ts = meta.get(1).map(parse_u64).unwrap_or(0);
1467                metrics.meta.prev_count = meta.get(2).map(parse_u64).unwrap_or(0);
1468            }
1469            if let Some(redis::Value::Array(data)) = parts.get(1) {
1470                metrics.data = data.iter().map(parse_u64).collect();
1471            }
1472            if let Some(count) = parts.get(2) {
1473                metrics.count = parse_u64(count);
1474            }
1475        }
1476
1477        Ok(metrics)
1478    }
1479
1480    /// Get return values of all completed children of a parent job.
1481    pub async fn get_children_values(
1482        &self,
1483        job_id: &str,
1484    ) -> Result<HashMap<String, serde_json::Value>, Error> {
1485        let processed_key = format!("{}:processed", self.keys.job_key(job_id));
1486        let mut conn = self.conn.conn();
1487
1488        let result: HashMap<String, String> = redis::cmd("HGETALL")
1489            .arg(&processed_key)
1490            .query_async(&mut conn)
1491            .await?;
1492
1493        let mut parsed = HashMap::new();
1494        for (key, value) in result {
1495            let parsed_value: serde_json::Value =
1496                serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
1497            parsed.insert(key, parsed_value);
1498        }
1499        Ok(parsed)
1500    }
1501
1502    /// Get failure values of children that failed with ignoreDependencyOnFailure.
1503    pub async fn get_failed_children_values(
1504        &self,
1505        job_id: &str,
1506    ) -> Result<HashMap<String, String>, Error> {
1507        let failed_key = format!("{}:failed", self.keys.job_key(job_id));
1508        let mut conn = self.conn.conn();
1509
1510        let result: HashMap<String, String> = redis::cmd("HGETALL")
1511            .arg(&failed_key)
1512            .query_async(&mut conn)
1513            .await?;
1514
1515        Ok(result)
1516    }
1517
1518    /// Get counts of dependencies for a parent job.
1519    pub async fn get_dependencies_count(&self, job_id: &str) -> Result<DependenciesCount, Error> {
1520        let job_key = self.keys.job_key(job_id);
1521        let processed_key = format!("{}:processed", job_key);
1522        let deps_key = format!("{}:dependencies", job_key);
1523        let failed_key = format!("{}:failed", job_key);
1524        let unsuccessful_key = format!("{}:unsuccessful", job_key);
1525
1526        let mut conn = self.conn.conn();
1527        let mut pipe = redis::pipe();
1528        pipe.cmd("HLEN").arg(&processed_key);
1529        pipe.cmd("SCARD").arg(&deps_key);
1530        pipe.cmd("HLEN").arg(&failed_key);
1531        pipe.cmd("ZCARD").arg(&unsuccessful_key);
1532
1533        let (processed, unprocessed, ignored, failed): (u64, u64, u64, u64) =
1534            pipe.query_async(&mut conn).await?;
1535
1536        Ok(DependenciesCount {
1537            processed,
1538            unprocessed,
1539            ignored,
1540            failed,
1541        })
1542    }
1543
1544    /// Get unprocessed dependencies (children still pending).
1545    pub async fn get_unprocessed_dependencies(&self, job_id: &str) -> Result<Vec<String>, Error> {
1546        let deps_key = format!("{}:dependencies", self.keys.job_key(job_id));
1547        let mut conn = self.conn.conn();
1548
1549        let result: Vec<String> = redis::cmd("SMEMBERS")
1550            .arg(&deps_key)
1551            .query_async(&mut conn)
1552            .await?;
1553
1554        Ok(result)
1555    }
1556
1557    /// Remove a child's dependency from its parent.
1558    ///
1559    /// `job_id` - the child job ID
1560    /// `parent_key` - the fully qualified parent key (prefix:queue:parentId)
1561    ///
1562    /// Returns `true` if the dependency was broken, `false` otherwise.
1563    pub async fn remove_child_dependency(
1564        &self,
1565        job_id: &str,
1566        parent_key: &str,
1567    ) -> Result<bool, Error> {
1568        let script = self
1569            .conn
1570            .scripts()
1571            .get("removeChildDependency")
1572            .ok_or_else(|| {
1573                Error::InvalidConfig("removeChildDependency script not found".to_string())
1574            })?
1575            .clone();
1576
1577        let prefix_key = self.keys.key_prefix().to_string();
1578        let job_key = self.keys.job_key(job_id);
1579
1580        let keys = vec![prefix_key];
1581        let args: Vec<&[u8]> = vec![job_key.as_bytes(), parent_key.as_bytes()];
1582
1583        let mut conn = self.conn.conn();
1584        let result = script.execute(&mut conn, &keys, &args).await?;
1585
1586        match result {
1587            redis::Value::Int(0) => Ok(true),
1588            redis::Value::Int(1) => Ok(false),
1589            redis::Value::Int(-1) => Err(Error::InvalidConfig(format!(
1590                "Missing key for job {}. removeChildDependency",
1591                job_id
1592            ))),
1593            redis::Value::Int(-5) => Err(Error::InvalidConfig(format!(
1594                "Missing key for parent job {}. removeChildDependency",
1595                parent_key
1596            ))),
1597            _ => Ok(false),
1598        }
1599    }
1600
1601    /// Get the state of a specific job.
1602    pub async fn get_job_state(&self, job_id: &str) -> Result<JobState, Error> {
1603        let script = self
1604            .conn
1605            .scripts()
1606            .get("getState")
1607            .ok_or_else(|| Error::InvalidConfig("getState script not found".to_string()))?
1608            .clone();
1609
1610        let keys = vec![
1611            self.keys.completed(),
1612            self.keys.failed(),
1613            self.keys.delayed(),
1614            self.keys.active(),
1615            self.keys.wait(),
1616            self.keys.paused(),
1617            self.keys.waiting_children(),
1618            self.keys.prioritized(),
1619        ];
1620
1621        let job_id_bytes = job_id.as_bytes().to_vec();
1622        let args: Vec<&[u8]> = vec![&job_id_bytes];
1623
1624        let mut conn = self.conn.conn();
1625        let result = script.execute(&mut conn, &keys, &args).await?;
1626
1627        match result {
1628            redis::Value::BulkString(bytes) => {
1629                let state_str = String::from_utf8_lossy(&bytes);
1630                Ok(JobState::from_redis_str(&state_str))
1631            }
1632            redis::Value::SimpleString(s) => Ok(JobState::from_redis_str(&s)),
1633            _ => Ok(JobState::Unknown),
1634        }
1635    }
1636
1637    /// Remove a job by its ID, together with all of its dependencies.
1638    ///
1639    /// This mirrors the semantics of the Node.js `Queue#remove`, where the
1640    /// numeric result `1`/`0` is represented here as a `bool`:
1641    ///
1642    /// - `Ok(true)` — the job was removed. This is also returned when the job
1643    ///   no longer exists (for example, it never existed or was already
1644    ///   auto-removed when it finished), because there is nothing left to
1645    ///   remove.
1646    /// - `Ok(false)` — the job could not be removed because it, or one of its
1647    ///   dependencies, is locked (typically because it is being processed by a
1648    ///   worker). This is a normal outcome, not an error.
1649    /// - `Err(_)` — the removal could not be attempted, for example because the
1650    ///   job belongs to a job scheduler (which must be removed via
1651    ///   [`Queue::remove_job_scheduler`]), or because of an underlying
1652    ///   script/Redis failure.
1653    pub async fn remove(&self, job_id: &str) -> Result<bool, Error> {
1654        self.remove_job(job_id, true).await
1655    }
1656
1657    /// Remove a job without removing its children.
1658    ///
1659    /// Children remain in their queues and lose their parent reference. The
1660    /// return value follows the same semantics as [`Queue::remove`].
1661    pub async fn remove_without_children(&self, job_id: &str) -> Result<bool, Error> {
1662        self.remove_job(job_id, false).await
1663    }
1664
1665    async fn remove_job(&self, job_id: &str, remove_children: bool) -> Result<bool, Error> {
1666        let script = self
1667            .conn
1668            .scripts()
1669            .get("removeJob")
1670            .ok_or_else(|| Error::InvalidConfig("removeJob script not found".to_string()))?
1671            .clone();
1672
1673        let keys = vec![self.keys.job_key(job_id), self.keys.repeat()];
1674        let prefix = self.keys.key_prefix();
1675        let remove_children_flag = if remove_children { b"1" as &[u8] } else { b"0" };
1676        let args: Vec<&[u8]> = vec![job_id.as_bytes(), remove_children_flag, prefix.as_bytes()];
1677
1678        let mut conn = self.conn.conn();
1679        let result = script.execute(&mut conn, &keys, &args).await?;
1680
1681        match result {
1682            // 1 = removed, 0 = job (or a dependency) is locked. Mirroring
1683            // Node.js, a locked job is a normal "not removed" outcome rather
1684            // than an error.
1685            redis::Value::Int(1) => Ok(true),
1686            redis::Value::Int(0) => Ok(false),
1687            redis::Value::Int(code) if code < 0 => {
1688                if code == crate::error::error_code::JOB_BELONGS_TO_JOB_SCHEDULER {
1689                    Err(Error::Script {
1690                        code,
1691                        message: format!(
1692                            "Job {} belongs to a job scheduler and cannot be removed directly. removeJob",
1693                            job_id
1694                        ),
1695                    })
1696                } else {
1697                    Err(Error::from_script_code(code))
1698                }
1699            }
1700            _ => Ok(false),
1701        }
1702    }
1703
1704    /// Remove all unprocessed children of a job.
1705    ///
1706    /// This removes children that are still in the dependencies set (not yet completed/failed).
1707    /// Active children are skipped.
1708    pub async fn remove_unprocessed_children(&self, job_id: &str) -> Result<(), Error> {
1709        let script = self
1710            .conn
1711            .scripts()
1712            .get("removeUnprocessedChildren")
1713            .ok_or_else(|| {
1714                Error::InvalidConfig("removeUnprocessedChildren script not found".to_string())
1715            })?
1716            .clone();
1717
1718        let keys = vec![self.keys.job_key(job_id), self.keys.meta()];
1719        let prefix = self.keys.key_prefix();
1720        let args: Vec<&[u8]> = vec![prefix.as_bytes(), job_id.as_bytes()];
1721
1722        let mut conn = self.conn.conn();
1723        script.execute(&mut conn, &keys, &args).await?;
1724        Ok(())
1725    }
1726
1727    /// Clean jobs from a specific set (completed, failed, etc.).
1728    ///
1729    /// `grace` - Only remove jobs older than this many milliseconds.
1730    /// `limit` - Maximum number of jobs to remove (0 = unlimited).
1731    /// `state` - Which state set to clean ("completed", "failed", "wait", "active", "delayed", "prioritized", "paused").
1732    ///
1733    /// Returns the IDs of removed jobs.
1734    pub async fn clean(&self, grace: u64, limit: u32, state: &str) -> Result<Vec<String>, Error> {
1735        let script = self
1736            .conn
1737            .scripts()
1738            .get("cleanJobsInSet")
1739            .ok_or_else(|| Error::InvalidConfig("cleanJobsInSet script not found".to_string()))?
1740            .clone();
1741
1742        let now = std::time::SystemTime::now()
1743            .duration_since(std::time::UNIX_EPOCH)
1744            .unwrap()
1745            .as_millis() as u64;
1746        let timestamp = now.saturating_sub(grace);
1747
1748        // Normalize "waiting" to "wait"
1749        let normalized = if state == "waiting" { "wait" } else { state };
1750
1751        let set_key = match normalized {
1752            "completed" => self.keys.completed(),
1753            "failed" => self.keys.failed(),
1754            "wait" => self.keys.wait(),
1755            "active" => self.keys.active(),
1756            "delayed" => self.keys.delayed(),
1757            "paused" => self.keys.paused(),
1758            "prioritized" => self.keys.prioritized(),
1759            _ => return Err(Error::InvalidConfig(format!("invalid state: {}", state))),
1760        };
1761
1762        let max_per_call = if limit == 0 {
1763            10000u32
1764        } else {
1765            limit.min(10000)
1766        };
1767        let max_total = if limit == 0 { u32::MAX } else { limit };
1768        let mut all_deleted: Vec<String> = Vec::new();
1769
1770        loop {
1771            let keys = vec![set_key.clone(), self.keys.events(), self.keys.repeat()];
1772
1773            let prefix = self.keys.key_prefix();
1774            let ts_str = timestamp.to_string();
1775            let limit_str = max_per_call.to_string();
1776
1777            let args: Vec<&[u8]> = vec![
1778                prefix.as_bytes(),
1779                ts_str.as_bytes(),
1780                limit_str.as_bytes(),
1781                normalized.as_bytes(),
1782            ];
1783
1784            let mut conn = self.conn.conn();
1785            let result = script.execute(&mut conn, &keys, &args).await?;
1786
1787            let batch: Vec<String> = match result {
1788                redis::Value::Array(arr) => arr
1789                    .into_iter()
1790                    .filter_map(|v| match v {
1791                        redis::Value::BulkString(bytes) => {
1792                            Some(String::from_utf8_lossy(&bytes).to_string())
1793                        }
1794                        redis::Value::SimpleString(s) => Some(s),
1795                        _ => None,
1796                    })
1797                    .collect(),
1798                _ => Vec::new(),
1799            };
1800
1801            let batch_len = batch.len() as u32;
1802            all_deleted.extend(batch);
1803
1804            if batch_len < max_per_call || all_deleted.len() as u32 >= max_total {
1805                break;
1806            }
1807        }
1808
1809        Ok(all_deleted)
1810    }
1811
1812    /// Drain the queue (remove all waiting and delayed jobs).
1813    pub async fn drain(&self, delayed: bool) -> Result<(), Error> {
1814        let script = self
1815            .conn
1816            .scripts()
1817            .get("drain")
1818            .ok_or_else(|| Error::InvalidConfig("drain script not found".to_string()))?
1819            .clone();
1820
1821        let keys = vec![
1822            self.keys.wait(),
1823            self.keys.paused(),
1824            self.keys.delayed(),
1825            self.keys.prioritized(),
1826            self.keys.repeat(),
1827        ];
1828
1829        let delayed_str = if delayed { "1" } else { "0" };
1830        let prefix = self.keys.key_prefix();
1831        let args: Vec<&[u8]> = vec![prefix.as_bytes(), delayed_str.as_bytes()];
1832
1833        let mut conn = self.conn.conn();
1834        script.execute(&mut conn, &keys, &args).await?;
1835
1836        debug!(delayed, "queue drained");
1837        Ok(())
1838    }
1839
1840    /// Retry all failed (or completed) jobs, moving them back to wait.
1841    ///
1842    /// - `state`: "failed" or "completed" (default: "failed")
1843    /// - `count`: max jobs to move per batch (default: 1000)
1844    /// - `timestamp`: only retry jobs finished before this timestamp in ms (default: now)
1845    pub async fn retry_jobs(
1846        &self,
1847        state: &str,
1848        count: u32,
1849        timestamp: Option<u64>,
1850    ) -> Result<(), Error> {
1851        let script = self
1852            .conn
1853            .scripts()
1854            .get("moveJobsToWait")
1855            .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1856            .clone();
1857
1858        let ts = timestamp.unwrap_or_else(|| {
1859            std::time::SystemTime::now()
1860                .duration_since(std::time::UNIX_EPOCH)
1861                .unwrap()
1862                .as_millis() as u64
1863        });
1864
1865        let keys = vec![
1866            self.keys.key_prefix(),
1867            self.keys.events(),
1868            self.keys.get(state),
1869            self.keys.wait(),
1870            self.keys.paused(),
1871            self.keys.meta(),
1872            self.keys.active(),
1873            self.keys.marker(),
1874        ];
1875
1876        let count_str = count.to_string();
1877        let ts_str = ts.to_string();
1878
1879        let mut conn = self.conn.conn();
1880        loop {
1881            let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), state.as_bytes()];
1882            let result = script.execute(&mut conn, &keys, &args).await?;
1883
1884            match result {
1885                redis::Value::Int(1) => continue,
1886                _ => break,
1887            }
1888        }
1889
1890        debug!(state, "retry_jobs completed");
1891        Ok(())
1892    }
1893
1894    /// Promote all delayed jobs to waiting.
1895    ///
1896    /// - `count`: max jobs to promote per batch (default: 1000)
1897    pub async fn promote_jobs(&self, count: u32) -> Result<(), Error> {
1898        let script = self
1899            .conn
1900            .scripts()
1901            .get("moveJobsToWait")
1902            .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1903            .clone();
1904
1905        let keys = vec![
1906            self.keys.key_prefix(),
1907            self.keys.events(),
1908            self.keys.delayed(),
1909            self.keys.wait(),
1910            self.keys.paused(),
1911            self.keys.meta(),
1912            self.keys.active(),
1913            self.keys.marker(),
1914        ];
1915
1916        let count_str = count.to_string();
1917        // Use MAX_VALUE equivalent for timestamp so all delayed jobs match
1918        let ts_str = "9007199254740991".to_string(); // Number.MAX_SAFE_INTEGER
1919
1920        let mut conn = self.conn.conn();
1921        loop {
1922            let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), b"delayed"];
1923            let result = script.execute(&mut conn, &keys, &args).await?;
1924
1925            match result {
1926                redis::Value::Int(1) => continue,
1927                _ => break,
1928            }
1929        }
1930
1931        debug!("promote_jobs completed");
1932        Ok(())
1933    }
1934
1935    /// Override the rate limit to be active for the next jobs.
1936    ///
1937    /// Sets the rate limiter key to MAX value with the given TTL,
1938    /// preventing any new jobs from being processed until it expires.
1939    pub async fn rate_limit(&self, expire_time_ms: u64) -> Result<(), Error> {
1940        let limiter_key = self.keys.limiter();
1941        let mut conn = self.conn.conn();
1942
1943        redis::cmd("SET")
1944            .arg(&limiter_key)
1945            .arg("9007199254740991") // Number.MAX_SAFE_INTEGER
1946            .arg("PX")
1947            .arg(expire_time_ms)
1948            .query_async::<()>(&mut conn)
1949            .await?;
1950
1951        Ok(())
1952    }
1953
1954    /// Remove the rate limit key, allowing processing to resume immediately.
1955    pub async fn remove_rate_limit_key(&self) -> Result<bool, Error> {
1956        let limiter_key = self.keys.limiter();
1957        let mut conn = self.conn.conn();
1958
1959        let result: u32 = redis::cmd("DEL")
1960            .arg(&limiter_key)
1961            .query_async(&mut conn)
1962            .await?;
1963
1964        Ok(result > 0)
1965    }
1966
1967    /// Set global concurrency limit (stored in queue meta hash).
1968    /// Limits the total number of active jobs across all workers for this queue.
1969    pub async fn set_global_concurrency(&self, concurrency: u64) -> Result<(), Error> {
1970        let meta_key = self.keys.meta();
1971        let mut conn = self.conn.conn();
1972
1973        redis::cmd("HSET")
1974            .arg(&meta_key)
1975            .arg("concurrency")
1976            .arg(concurrency)
1977            .query_async::<()>(&mut conn)
1978            .await?;
1979
1980        Ok(())
1981    }
1982
1983    /// Remove global concurrency limit from queue meta.
1984    pub async fn remove_global_concurrency(&self) -> Result<(), Error> {
1985        let meta_key = self.keys.meta();
1986        let mut conn = self.conn.conn();
1987
1988        redis::cmd("HDEL")
1989            .arg(&meta_key)
1990            .arg("concurrency")
1991            .query_async::<()>(&mut conn)
1992            .await?;
1993
1994        Ok(())
1995    }
1996
1997    /// Set global rate limit (stored in queue meta hash).
1998    pub async fn set_global_rate_limit(&self, max: u64, duration: u64) -> Result<(), Error> {
1999        let meta_key = self.keys.meta();
2000        let mut conn = self.conn.conn();
2001
2002        redis::cmd("HSET")
2003            .arg(&meta_key)
2004            .arg("max")
2005            .arg(max)
2006            .arg("duration")
2007            .arg(duration)
2008            .query_async::<()>(&mut conn)
2009            .await?;
2010
2011        Ok(())
2012    }
2013
2014    /// Remove global rate limit values from queue meta.
2015    pub async fn remove_global_rate_limit(&self) -> Result<(), Error> {
2016        let meta_key = self.keys.meta();
2017        let mut conn = self.conn.conn();
2018
2019        redis::cmd("HDEL")
2020            .arg(&meta_key)
2021            .arg("max")
2022            .arg("duration")
2023            .query_async::<()>(&mut conn)
2024            .await?;
2025
2026        Ok(())
2027    }
2028
2029    /// Return the time-to-live (in ms) for the rate-limited key.
2030    ///
2031    /// `max_jobs` is the maximum number of jobs considered in the rate-limit
2032    /// state. When `None`, the remaining TTL is returned without checking
2033    /// whether the max is exceeded. Returns `0` when not rate limited and
2034    /// `-2`/`-1` mirror Redis `PTTL` semantics (no key / no expiry).
2035    pub async fn get_rate_limit_ttl(&self, max_jobs: Option<u64>) -> Result<i64, Error> {
2036        let script = self
2037            .conn
2038            .scripts()
2039            .get("getRateLimitTtl")
2040            .ok_or_else(|| Error::InvalidConfig("getRateLimitTtl script not found".to_string()))?
2041            .clone();
2042
2043        let keys = vec![self.keys.limiter(), self.keys.meta()];
2044        let max_jobs_str = max_jobs
2045            .map(|m| m.to_string())
2046            .unwrap_or_else(|| "0".to_string());
2047        let args: Vec<&[u8]> = vec![max_jobs_str.as_bytes()];
2048
2049        let mut conn = self.conn.conn();
2050        let result = script.execute(&mut conn, &keys, &args).await?;
2051
2052        match result {
2053            redis::Value::Int(n) => Ok(n),
2054            _ => Ok(0),
2055        }
2056    }
2057
2058    /// Return the global concurrency value, or `None` when not set.
2059    pub async fn get_global_concurrency(&self) -> Result<Option<u64>, Error> {
2060        let meta_key = self.keys.meta();
2061        let mut conn = self.conn.conn();
2062
2063        let value: Option<String> = redis::cmd("HGET")
2064            .arg(&meta_key)
2065            .arg("concurrency")
2066            .query_async(&mut conn)
2067            .await?;
2068
2069        Ok(value.and_then(|v| v.parse::<u64>().ok()))
2070    }
2071
2072    /// Return the global rate limit as `(max, duration)`, or `None` when not set.
2073    pub async fn get_global_rate_limit(&self) -> Result<Option<(u64, u64)>, Error> {
2074        let meta_key = self.keys.meta();
2075        let mut conn = self.conn.conn();
2076
2077        let values: Vec<Option<String>> = redis::cmd("HMGET")
2078            .arg(&meta_key)
2079            .arg("max")
2080            .arg("duration")
2081            .query_async(&mut conn)
2082            .await?;
2083
2084        let max = values
2085            .first()
2086            .and_then(|v| v.as_ref())
2087            .and_then(|v| v.parse::<u64>().ok());
2088        let duration = values
2089            .get(1)
2090            .and_then(|v| v.as_ref())
2091            .and_then(|v| v.parse::<u64>().ok());
2092
2093        match (max, duration) {
2094            (Some(m), Some(d)) => Ok(Some((m, d))),
2095            _ => Ok(None),
2096        }
2097    }
2098
2099    /// Remove a deduplication key if the stored job ID matches the given one.
2100    ///
2101    /// Uses the `removeDeduplicationKey` Lua script for atomic check-and-delete.
2102    pub async fn remove_deduplication_key(
2103        &self,
2104        deduplication_id: &str,
2105        job_id: &str,
2106    ) -> Result<bool, Error> {
2107        let script = self
2108            .conn
2109            .scripts()
2110            .get("removeDeduplicationKey")
2111            .ok_or_else(|| {
2112                Error::InvalidConfig("removeDeduplicationKey script not found".to_string())
2113            })?
2114            .clone();
2115
2116        let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2117        let keys = vec![dedup_key];
2118        let args: Vec<&[u8]> = vec![job_id.as_bytes()];
2119
2120        let mut conn = self.conn.conn();
2121        let result = script.execute(&mut conn, &keys, &args).await?;
2122
2123        match result {
2124            redis::Value::Int(1) => Ok(true),
2125            _ => Ok(false),
2126        }
2127    }
2128
2129    /// Get the job ID stored for a given deduplication ID.
2130    ///
2131    /// Returns `None` if no deduplication key exists.
2132    pub async fn get_deduplication_job_id(
2133        &self,
2134        deduplication_id: &str,
2135    ) -> Result<Option<String>, Error> {
2136        let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2137        let mut conn = self.conn.conn();
2138        let result: redis::Value = redis::cmd("GET")
2139            .arg(&dedup_key)
2140            .query_async(&mut conn)
2141            .await
2142            .map_err(Error::Redis)?;
2143
2144        match result {
2145            redis::Value::BulkString(bytes) => {
2146                Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
2147            }
2148            redis::Value::SimpleString(s) => Ok(Some(s)),
2149            redis::Value::Nil => Ok(None),
2150            _ => Ok(None),
2151        }
2152    }
2153
2154    /// Get the job ID that started a debounced state.
2155    ///
2156    /// **Deprecated:** use [`Queue::get_deduplication_job_id`] instead. Provided
2157    /// for parity with the legacy Node.js `Queue.getDebounceJobId`.
2158    pub async fn get_debounce_job_id(&self, id: &str) -> Result<Option<String>, Error> {
2159        self.get_deduplication_job_id(id).await
2160    }
2161
2162    /// Remove a debounce key unconditionally, returning the number of keys
2163    /// deleted (`0` or `1`).
2164    ///
2165    /// **Deprecated:** use [`Queue::remove_deduplication_key`] instead. Provided
2166    /// for parity with the legacy Node.js `Queue.removeDebounceKey`. Unlike the
2167    /// deduplication variant, this performs a plain `DEL` without checking the
2168    /// stored job ID.
2169    pub async fn remove_debounce_key(&self, id: &str) -> Result<u64, Error> {
2170        let dedup_key = format!("{}:de:{}", self.keys.base(), id);
2171        let mut conn = self.conn.conn();
2172        let deleted: u64 = redis::cmd("DEL")
2173            .arg(&dedup_key)
2174            .query_async(&mut conn)
2175            .await?;
2176        Ok(deleted)
2177    }
2178
2179    /// Get logs for a specific job.
2180    ///
2181    /// Returns the log entries and total count.
2182    pub async fn get_job_logs(
2183        &self,
2184        job_id: &str,
2185        start: isize,
2186        end: isize,
2187        asc: bool,
2188    ) -> Result<(Vec<String>, usize), Error> {
2189        let logs_key = format!("{}{}:logs", self.keys.key_prefix(), job_id);
2190        let mut conn = self.conn.conn();
2191
2192        let (logs, count): (Vec<String>, usize) = if asc {
2193            redis::pipe()
2194                .cmd("LRANGE")
2195                .arg(&logs_key)
2196                .arg(start)
2197                .arg(end)
2198                .cmd("LLEN")
2199                .arg(&logs_key)
2200                .query_async(&mut conn)
2201                .await?
2202        } else {
2203            let actual_start = -(end + 1);
2204            let actual_end = -(start + 1);
2205            let (mut logs, count): (Vec<String>, usize) = redis::pipe()
2206                .cmd("LRANGE")
2207                .arg(&logs_key)
2208                .arg(actual_start)
2209                .arg(actual_end)
2210                .cmd("LLEN")
2211                .arg(&logs_key)
2212                .query_async(&mut conn)
2213                .await?;
2214            logs.reverse();
2215            (logs, count)
2216        };
2217
2218        Ok((logs, count))
2219    }
2220
2221    /// Trim the event stream to approximately `max_length` entries.
2222    pub async fn trim_events(&self, max_length: usize) -> Result<usize, Error> {
2223        let mut conn = self.conn.conn();
2224        let trimmed: usize = redis::cmd("XTRIM")
2225            .arg(self.keys.events())
2226            .arg("MAXLEN")
2227            .arg("~")
2228            .arg(max_length)
2229            .query_async(&mut conn)
2230            .await?;
2231        Ok(trimmed)
2232    }
2233
2234    /// Update a job's progress by id, without loading the job first.
2235    ///
2236    /// Mirrors Node.js `Queue.updateJobProgress`: runs the `updateProgress`
2237    /// script which sets the job's `progress` field and emits a `progress`
2238    /// event on the queue's event stream.
2239    pub async fn update_job_progress(
2240        &self,
2241        job_id: &str,
2242        progress: crate::types::JobProgress,
2243    ) -> Result<(), Error> {
2244        let script = self
2245            .conn
2246            .scripts()
2247            .get("updateProgress")
2248            .ok_or_else(|| Error::InvalidConfig("updateProgress script not found".to_string()))?
2249            .clone();
2250
2251        let job_key = self.keys.job_key(job_id);
2252        let events_key = self.keys.events();
2253        let meta_key = self.keys.meta();
2254        let progress_json = serialize_progress_for_script(&progress)?;
2255
2256        let keys = vec![job_key, events_key, meta_key];
2257        let args: Vec<&[u8]> = vec![job_id.as_bytes(), progress_json.as_bytes()];
2258
2259        let mut conn = self.conn.conn();
2260        let result: redis::Value = script.execute(&mut conn, &keys, &args).await?;
2261
2262        match result {
2263            redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2264            _ => Ok(()),
2265        }
2266    }
2267
2268    /// Obliterate the queue (remove all keys).
2269    ///
2270    /// When `force` is true, automatically pauses the queue first.
2271    pub async fn obliterate(&self, force: bool, count: usize) -> Result<(), Error> {
2272        // The script requires the queue to be paused
2273        if force {
2274            let _ = self.pause().await;
2275        }
2276
2277        let script = self
2278            .conn
2279            .scripts()
2280            .get("obliterate")
2281            .ok_or_else(|| Error::InvalidConfig("obliterate script not found".to_string()))?
2282            .clone();
2283
2284        let keys = vec![self.keys.meta(), self.keys.key_prefix()];
2285        let count_str = count.to_string();
2286        let force_str = if force { "1" } else { "0" };
2287
2288        let mut conn = self.conn.conn();
2289        loop {
2290            let args: Vec<&[u8]> = vec![count_str.as_bytes(), force_str.as_bytes()];
2291            let result = script.execute(&mut conn, &keys, &args).await?;
2292
2293            match result {
2294                redis::Value::Nil => break,
2295                redis::Value::Int(0) => break,
2296                redis::Value::Int(1) => continue, // more to delete
2297                redis::Value::Int(code) if code < 0 => {
2298                    return Err(Error::from_script_code(code));
2299                }
2300                _ => break,
2301            }
2302        }
2303
2304        debug!(force, "queue obliterated");
2305        Ok(())
2306    }
2307
2308    // ── Job Scheduler Methods ────────────────────────────────────────────
2309
2310    /// Create or update a job scheduler.
2311    ///
2312    /// Creates a scheduled repeating job that will run on a cron pattern or at
2313    /// fixed intervals. The scheduler is persisted in Redis and will create
2314    /// the next delayed job automatically after each execution.
2315    ///
2316    /// # Arguments
2317    /// - `job_scheduler_id` — Unique ID for this scheduler.
2318    /// - `repeat_opts` — Schedule configuration (cron pattern or every-ms).
2319    /// - `job_name` — Name for the created jobs (defaults to scheduler ID).
2320    /// - `job_data` — JSON data for the created jobs.
2321    /// - `job_opts` — Options for the created jobs (attempts, backoff, etc.).
2322    pub async fn upsert_job_scheduler(
2323        &self,
2324        job_scheduler_id: &str,
2325        repeat_opts: crate::job_scheduler::RepeatOptions,
2326        job_name: Option<&str>,
2327        job_data: Option<serde_json::Value>,
2328        job_opts: Option<JobOptions>,
2329    ) -> Result<Option<Job>, Error> {
2330        use crate::job_scheduler::{next_cron_millis, pack_delayed_job_opts, pack_scheduler_opts};
2331
2332        // Validation
2333        if repeat_opts.pattern.is_some() && repeat_opts.every.is_some() {
2334            return Err(Error::InvalidConfig(
2335                "Both .pattern and .every options are defined; only one may be used".to_string(),
2336            ));
2337        }
2338        if repeat_opts.pattern.is_none() && repeat_opts.every.is_none() {
2339            return Err(Error::InvalidConfig(
2340                "Either .pattern or .every option must be defined".to_string(),
2341            ));
2342        }
2343        if repeat_opts.immediately == Some(true) && repeat_opts.start_date.is_some() {
2344            return Err(Error::InvalidConfig(
2345                "Both .immediately and .startDate options are defined; only one may be used"
2346                    .to_string(),
2347            ));
2348        }
2349
2350        let now = std::time::SystemTime::now()
2351            .duration_since(std::time::UNIX_EPOCH)
2352            .unwrap()
2353            .as_millis() as u64;
2354
2355        // Validate end date
2356        if let Some(end_date) = repeat_opts.end_date {
2357            if end_date < now {
2358                return Err(Error::InvalidConfig(
2359                    "End date must be greater than current timestamp".to_string(),
2360                ));
2361            }
2362        }
2363
2364        // Compute iteration count
2365        let iteration_count = repeat_opts.count.unwrap_or(0) + 1;
2366        if let Some(limit) = repeat_opts.limit {
2367            if iteration_count > limit {
2368                return Ok(None);
2369            }
2370        }
2371
2372        // Compute nextMillis for cron patterns
2373        let next_millis: Option<u64> = if let Some(ref pattern) = repeat_opts.pattern {
2374            if repeat_opts.immediately == Some(true) {
2375                Some(now)
2376            } else {
2377                next_cron_millis(
2378                    pattern,
2379                    now,
2380                    repeat_opts.tz.as_deref(),
2381                    repeat_opts.start_date,
2382                )?
2383            }
2384        } else {
2385            // For `every`, nextMillis is not computed here — the Lua script handles it
2386            None
2387        };
2388
2389        // We need either nextMillis or every to proceed
2390        if next_millis.is_none() && repeat_opts.every.is_none() {
2391            return Ok(None);
2392        }
2393
2394        let effective_name = job_name.unwrap_or(job_scheduler_id);
2395        let effective_data = job_data.unwrap_or(serde_json::json!({}));
2396        let effective_opts = job_opts.unwrap_or_default();
2397
2398        let offset = if repeat_opts.every.is_some() {
2399            repeat_opts.offset
2400        } else {
2401            None
2402        };
2403
2404        // Pack arguments for Lua script
2405        let scheduler_opts_packed = pack_scheduler_opts(effective_name, &repeat_opts);
2406        let template_data_json =
2407            serde_json::to_string(&effective_data).unwrap_or_else(|_| "{}".to_string());
2408        let template_opts_packed = self.pack_job_opts_from_options(&effective_opts);
2409        let delayed_job_opts_packed = pack_delayed_job_opts(
2410            &effective_opts,
2411            job_scheduler_id,
2412            next_millis.unwrap_or(0),
2413            iteration_count,
2414            offset,
2415            &repeat_opts,
2416        );
2417
2418        let script = self
2419            .conn
2420            .scripts()
2421            .get("addJobScheduler")
2422            .ok_or_else(|| Error::InvalidConfig("addJobScheduler script not found".to_string()))?
2423            .clone();
2424
2425        // KEYS[1-11]
2426        let keys = vec![
2427            self.keys.repeat(),      // KEYS[1]
2428            self.keys.delayed(),     // KEYS[2]
2429            self.keys.wait(),        // KEYS[3]
2430            self.keys.paused(),      // KEYS[4]
2431            self.keys.meta(),        // KEYS[5]
2432            self.keys.prioritized(), // KEYS[6]
2433            self.keys.marker(),      // KEYS[7]
2434            self.keys.id(),          // KEYS[8]
2435            self.keys.events(),      // KEYS[9]
2436            self.keys.pc(),          // KEYS[10]
2437            self.keys.active(),      // KEYS[11]
2438        ];
2439
2440        // ARGV
2441        let next_millis_str = next_millis.unwrap_or(0).to_string();
2442        let timestamp_str = now.to_string();
2443        let prefix = self.keys.key_prefix();
2444        let producer_key = Vec::new(); // empty for non-flow jobs
2445
2446        let args: Vec<&[u8]> = vec![
2447            next_millis_str.as_bytes(),    // ARGV[1]
2448            &scheduler_opts_packed,        // ARGV[2]
2449            job_scheduler_id.as_bytes(),   // ARGV[3]
2450            template_data_json.as_bytes(), // ARGV[4]
2451            &template_opts_packed,         // ARGV[5]
2452            &delayed_job_opts_packed,      // ARGV[6]
2453            timestamp_str.as_bytes(),      // ARGV[7]
2454            prefix.as_bytes(),             // ARGV[8]
2455            &producer_key,                 // ARGV[9]
2456        ];
2457
2458        let mut conn = self.conn.conn();
2459        let result = script.execute(&mut conn, &keys, &args).await?;
2460
2461        // Parse result: the script returns [jobId, delay] on success
2462        match result {
2463            redis::Value::Array(ref arr) if arr.len() >= 2 => {
2464                let job_id = match &arr[0] {
2465                    redis::Value::BulkString(bytes) => String::from_utf8_lossy(bytes).to_string(),
2466                    redis::Value::SimpleString(s) => s.clone(),
2467                    redis::Value::Int(n) => n.to_string(),
2468                    _ => return Ok(None),
2469                };
2470
2471                let mut job = Job::new(effective_name, effective_data, Some(effective_opts));
2472                job.set_id(job_id);
2473                job.set_context(self.make_script_context());
2474                Ok(Some(job))
2475            }
2476            redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2477            _ => Ok(None),
2478        }
2479    }
2480
2481    /// Get a job scheduler by its ID.
2482    pub async fn get_job_scheduler(
2483        &self,
2484        job_scheduler_id: &str,
2485    ) -> Result<Option<crate::job_scheduler::JobSchedulerJson>, Error> {
2486        let script = self
2487            .conn
2488            .scripts()
2489            .get("getJobScheduler")
2490            .ok_or_else(|| Error::InvalidConfig("getJobScheduler script not found".to_string()))?
2491            .clone();
2492
2493        let keys = vec![self.keys.repeat()];
2494        let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes()];
2495
2496        let mut conn = self.conn.conn();
2497        let result = script.execute(&mut conn, &keys, &args).await?;
2498
2499        // Result is [hash_fields_array, score_string]
2500        match result {
2501            redis::Value::Array(ref arr) if arr.len() >= 2 => {
2502                let fields = Self::parse_hash_array(&arr[0]);
2503                if fields.is_empty() {
2504                    return Ok(None);
2505                }
2506                let next_millis = match &arr[1] {
2507                    redis::Value::BulkString(bytes) => {
2508                        String::from_utf8_lossy(bytes).parse::<u64>().ok()
2509                    }
2510                    redis::Value::SimpleString(s) => s.parse::<u64>().ok(),
2511                    redis::Value::Int(n) => Some(*n as u64),
2512                    _ => None,
2513                };
2514                Ok(Some(crate::job_scheduler::parse_scheduler_hash(
2515                    job_scheduler_id,
2516                    &fields,
2517                    next_millis,
2518                )))
2519            }
2520            redis::Value::Nil => Ok(None),
2521            redis::Value::Array(ref arr) if arr.is_empty() => Ok(None),
2522            _ => Ok(None),
2523        }
2524    }
2525
2526    /// Get a paginated list of job schedulers.
2527    ///
2528    /// Returns schedulers ordered by next execution time.
2529    pub async fn get_job_schedulers(
2530        &self,
2531        start: isize,
2532        end: isize,
2533        asc: bool,
2534    ) -> Result<Vec<crate::job_scheduler::JobSchedulerJson>, Error> {
2535        let mut conn = self.conn.conn();
2536        let repeat_key = self.keys.repeat();
2537
2538        // Get members with scores
2539        let results: Vec<(String, f64)> = if asc {
2540            redis::cmd("ZRANGE")
2541                .arg(&repeat_key)
2542                .arg(start)
2543                .arg(end)
2544                .arg("WITHSCORES")
2545                .query_async(&mut conn)
2546                .await?
2547        } else {
2548            redis::cmd("ZREVRANGE")
2549                .arg(&repeat_key)
2550                .arg(start)
2551                .arg(end)
2552                .arg("WITHSCORES")
2553                .query_async(&mut conn)
2554                .await?
2555        };
2556
2557        let mut schedulers = Vec::with_capacity(results.len());
2558        for (scheduler_id, score) in &results {
2559            let scheduler_hash_key = format!("{}repeat:{}", self.keys.key_prefix(), scheduler_id);
2560            let fields: std::collections::HashMap<String, String> = redis::cmd("HGETALL")
2561                .arg(&scheduler_hash_key)
2562                .query_async(&mut conn)
2563                .await?;
2564
2565            if !fields.is_empty() {
2566                schedulers.push(crate::job_scheduler::parse_scheduler_hash(
2567                    scheduler_id,
2568                    &fields,
2569                    Some(*score as u64),
2570                ));
2571            }
2572        }
2573
2574        Ok(schedulers)
2575    }
2576
2577    /// Get the total number of job schedulers.
2578    pub async fn get_job_schedulers_count(&self) -> Result<u64, Error> {
2579        let mut conn = self.conn.conn();
2580        let count: u64 = redis::cmd("ZCARD")
2581            .arg(self.keys.repeat())
2582            .query_async(&mut conn)
2583            .await?;
2584        Ok(count)
2585    }
2586
2587    /// Remove a job scheduler by its ID.
2588    ///
2589    /// Also removes the next scheduled delayed job if one exists.
2590    /// Returns `true` if the scheduler was found and removed.
2591    pub async fn remove_job_scheduler(&self, job_scheduler_id: &str) -> Result<bool, Error> {
2592        let script = self
2593            .conn
2594            .scripts()
2595            .get("removeJobScheduler")
2596            .ok_or_else(|| Error::InvalidConfig("removeJobScheduler script not found".to_string()))?
2597            .clone();
2598
2599        let keys = vec![self.keys.repeat(), self.keys.delayed(), self.keys.events()];
2600
2601        let prefix = self.keys.key_prefix();
2602        let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes(), prefix.as_bytes()];
2603
2604        let mut conn = self.conn.conn();
2605        let result = script.execute(&mut conn, &keys, &args).await?;
2606
2607        match result {
2608            redis::Value::Int(0) => Ok(true),  // 0 = success (removed)
2609            redis::Value::Int(1) => Ok(false), // 1 = not found
2610            _ => Ok(false),
2611        }
2612    }
2613
2614    /// Parse a Redis hash array (alternating key/value) into a HashMap.
2615    fn parse_hash_array(value: &redis::Value) -> std::collections::HashMap<String, String> {
2616        let mut map = std::collections::HashMap::new();
2617        if let redis::Value::Array(arr) = value {
2618            let mut iter = arr.iter();
2619            while let (Some(key_val), Some(val_val)) = (iter.next(), iter.next()) {
2620                let key = match key_val {
2621                    redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2622                    redis::Value::SimpleString(s) => s.clone(),
2623                    _ => continue,
2624                };
2625                let val = match val_val {
2626                    redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2627                    redis::Value::SimpleString(s) => s.clone(),
2628                    redis::Value::Int(n) => n.to_string(),
2629                    _ => String::new(),
2630                };
2631                map.insert(key, val);
2632            }
2633        }
2634        map
2635    }
2636
2637    /// Pack job options from a JobOptions struct into msgpack (for template opts).
2638    fn pack_job_opts_from_options(&self, opts: &JobOptions) -> Vec<u8> {
2639        use rmp::encode::*;
2640
2641        let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
2642
2643        if let Some(attempts) = opts.attempts {
2644            let mut b = Vec::new();
2645            write_uint(&mut b, attempts as u64).unwrap();
2646            entries.push(("attempts", b));
2647        }
2648        if let Some(ref backoff) = opts.backoff {
2649            let b = Self::encode_backoff(backoff);
2650            entries.push(("backoff", b));
2651        }
2652        if let Some(ref roc) = opts.remove_on_complete {
2653            let b = Self::encode_remove_on_finish(roc);
2654            entries.push(("removeOnComplete", b));
2655        }
2656        if let Some(ref rof) = opts.remove_on_fail {
2657            let b = Self::encode_remove_on_finish(rof);
2658            entries.push(("removeOnFail", b));
2659        }
2660        if let Some(priority) = opts.priority {
2661            if priority > 0 {
2662                let mut b = Vec::new();
2663                write_uint(&mut b, priority as u64).unwrap();
2664                entries.push(("priority", b));
2665            }
2666        }
2667
2668        let mut buf = Vec::with_capacity(64);
2669        write_map_len(&mut buf, entries.len() as u32).unwrap();
2670        for (key, val) in &entries {
2671            write_str(&mut buf, key).unwrap();
2672            buf.extend_from_slice(val);
2673        }
2674        buf
2675    }
2676
2677    /// Close the queue connection.
2678    pub async fn close(&self) {
2679        self.conn.close().await;
2680    }
2681}
2682
2683/// Fluent builder returned by [`Queue::add`].
2684///
2685/// `AddJob` implements [`IntoFuture`], so awaiting it adds the job with the
2686/// accumulated options. Setters are infallible and chainable; any invalid
2687/// configuration (or a data serialization failure) is reported when the builder
2688/// is awaited.
2689#[must_use = "an AddJob does nothing until awaited"]
2690pub struct AddJob<'a> {
2691    queue: &'a Queue,
2692    name: String,
2693    data: Result<serde_json::Value, Error>,
2694    opts: JobOptions,
2695}
2696
2697impl<'a> AddJob<'a> {
2698    fn new<T: Serialize>(queue: &'a Queue, name: &str, data: T) -> Self {
2699        Self {
2700            queue,
2701            name: name.to_string(),
2702            data: serde_json::to_value(data).map_err(Error::from),
2703            opts: JobOptions::default(),
2704        }
2705    }
2706
2707    /// Use `opts` as the base options, replacing anything set so far.
2708    ///
2709    /// Useful for reusing a prepared [`JobOptions`]; subsequent setters still
2710    /// override individual fields.
2711    pub fn options(mut self, opts: JobOptions) -> Self {
2712        self.opts = opts;
2713        self
2714    }
2715
2716    /// Delay before the job becomes available for processing.
2717    pub fn delay(mut self, delay: Duration) -> Self {
2718        self.opts.delay = Some(duration_as_millis(delay));
2719        self
2720    }
2721
2722    /// Job priority. Lower values are processed first; unset (or `0`) means the
2723    /// job is not prioritized and follows normal FIFO/LIFO ordering.
2724    pub fn priority(mut self, priority: u32) -> Self {
2725        self.opts.priority = Some(priority);
2726        self
2727    }
2728
2729    /// Total number of attempts before the job permanently fails.
2730    pub fn attempts(mut self, attempts: u32) -> Self {
2731        self.opts.attempts = Some(attempts);
2732        self
2733    }
2734
2735    /// Backoff strategy applied between retries.
2736    pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2737        self.opts.backoff = Some(backoff);
2738        self
2739    }
2740
2741    /// Add the job to the back of the queue (last-in-first-out ordering).
2742    pub fn lifo(mut self) -> Self {
2743        self.opts.lifo = Some(true);
2744        self
2745    }
2746
2747    /// Use a custom, unique job id instead of an auto-generated one.
2748    pub fn job_id(mut self, id: impl Into<String>) -> Self {
2749        self.opts.job_id = Some(id.into());
2750        self
2751    }
2752
2753    /// Completed-job retention policy.
2754    pub fn remove_on_complete(mut self, policy: RemoveOnFinish) -> Self {
2755        self.opts.remove_on_complete = Some(policy);
2756        self
2757    }
2758
2759    /// Failed-job retention policy.
2760    pub fn remove_on_fail(mut self, policy: RemoveOnFinish) -> Self {
2761        self.opts.remove_on_fail = Some(policy);
2762        self
2763    }
2764
2765    /// Maximum number of log entries to retain for the job.
2766    pub fn keep_logs(mut self, count: u32) -> Self {
2767        self.opts.keep_logs = Some(count);
2768        self
2769    }
2770
2771    /// Attach this job to a parent job (flow / dependency chains).
2772    pub fn parent(mut self, parent: ParentOptions) -> Self {
2773        self.opts.parent = Some(parent);
2774        self
2775    }
2776
2777    /// Deduplicate the job using the given options.
2778    pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2779        self.opts.deduplication = Some(deduplication);
2780        self
2781    }
2782
2783    /// Reject the job if its serialized payload exceeds `bytes` UTF-8 bytes.
2784    pub fn size_limit(mut self, bytes: usize) -> Self {
2785        self.opts.size_limit = Some(bytes);
2786        self
2787    }
2788}
2789
2790impl<'a> IntoFuture for AddJob<'a> {
2791    type Output = Result<Job, Error>;
2792    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
2793
2794    fn into_future(self) -> Self::IntoFuture {
2795        Box::pin(async move {
2796            let data = self.data?;
2797            self.queue
2798                .add_internal(&self.name, data, Some(self.opts))
2799                .await
2800        })
2801    }
2802}
2803
2804/// A single job definition for [`Queue::add_bulk`].
2805///
2806/// Construct with [`BulkJob::new`] (or [`BulkJob::with_options`]) and, if
2807/// needed, attach options fluently or via [`BulkJob::options`]. `data` may be
2808/// any [`Serialize`] type; it is serialized internally and any failure surfaces
2809/// when the batch is added.
2810///
2811/// ```no_run
2812/// # async fn demo(queue: bullmq::Queue) -> bullmq::Result<()> {
2813/// use std::time::Duration;
2814/// use bullmq::BulkJob;
2815///
2816/// queue
2817///     .add_bulk(vec![
2818///         BulkJob::new("email", serde_json::json!({ "to": "a@example.com" })),
2819///         BulkJob::new("email", serde_json::json!({ "to": "b@example.com" }))
2820///             .delay(Duration::from_secs(60))
2821///             .priority(5),
2822///     ])
2823///     .await?;
2824/// # Ok(()) }
2825/// ```
2826#[must_use = "a BulkJob must be passed to Queue::add_bulk to have any effect"]
2827pub struct BulkJob {
2828    name: String,
2829    data: Result<serde_json::Value, Error>,
2830    opts: JobOptions,
2831}
2832
2833impl BulkJob {
2834    /// Create a bulk job entry with default options.
2835    pub fn new<T: Serialize>(name: impl Into<String>, data: T) -> Self {
2836        Self {
2837            name: name.into(),
2838            data: serde_json::to_value(data).map_err(Error::from),
2839            opts: JobOptions::default(),
2840        }
2841    }
2842
2843    /// Create a bulk job entry with explicit options.
2844    pub fn with_options<T: Serialize>(name: impl Into<String>, data: T, opts: JobOptions) -> Self {
2845        Self {
2846            name: name.into(),
2847            data: serde_json::to_value(data).map_err(Error::from),
2848            opts,
2849        }
2850    }
2851
2852    /// Use `opts` as the options, replacing anything set so far.
2853    pub fn options(mut self, opts: JobOptions) -> Self {
2854        self.opts = opts;
2855        self
2856    }
2857
2858    /// Delay before the job becomes available for processing.
2859    pub fn delay(mut self, delay: Duration) -> Self {
2860        self.opts = std::mem::take(&mut self.opts).delay(delay);
2861        self
2862    }
2863
2864    /// Job priority. Lower values are processed first; unset (or `0`) means the
2865    /// job is not prioritized.
2866    pub fn priority(mut self, priority: u32) -> Self {
2867        self.opts = std::mem::take(&mut self.opts).priority(priority);
2868        self
2869    }
2870
2871    /// Total number of attempts before the job permanently fails.
2872    pub fn attempts(mut self, attempts: u32) -> Self {
2873        self.opts = std::mem::take(&mut self.opts).attempts(attempts);
2874        self
2875    }
2876
2877    /// Backoff strategy applied between retries.
2878    pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2879        self.opts = std::mem::take(&mut self.opts).backoff(backoff);
2880        self
2881    }
2882
2883    /// Add the job to the back of the queue (last-in-first-out ordering).
2884    pub fn lifo(mut self) -> Self {
2885        self.opts = std::mem::take(&mut self.opts).lifo();
2886        self
2887    }
2888
2889    /// Use a custom, unique job id instead of an auto-generated one.
2890    pub fn job_id(mut self, id: impl Into<String>) -> Self {
2891        self.opts = std::mem::take(&mut self.opts).job_id(id);
2892        self
2893    }
2894
2895    /// Attach this job to a parent job (flow / dependency chains).
2896    pub fn parent(mut self, parent: ParentOptions) -> Self {
2897        self.opts = std::mem::take(&mut self.opts).parent(parent);
2898        self
2899    }
2900
2901    /// Deduplicate the job using the given options.
2902    pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2903        self.opts = std::mem::take(&mut self.opts).deduplication(deduplication);
2904        self
2905    }
2906
2907    /// Reject the job if its serialized payload exceeds `bytes` UTF-8 bytes.
2908    pub fn size_limit(mut self, bytes: usize) -> Self {
2909        self.opts = std::mem::take(&mut self.opts).size_limit(bytes);
2910        self
2911    }
2912}
2913
2914/// Convert a [`Duration`] to whole milliseconds, saturating at [`u64::MAX`].
2915fn duration_as_millis(duration: Duration) -> u64 {
2916    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2917}
2918
2919/// Escape a Prometheus label value (`\`, `"`, and newlines).
2920///
2921/// Mirrors Node.js `escapePrometheusLabelValue`.
2922fn escape_prometheus_label_value(value: &str) -> String {
2923    value
2924        .replace('\\', "\\\\")
2925        .replace('"', "\\\"")
2926        .replace('\n', "\\n")
2927}
2928
2929/// Parse the output of Redis `CLIENT LIST` and return the info maps for clients
2930/// belonging to this queue.
2931/// Mirrors Node.js `QueueGetters.parseClientList`: each line is a space-
2932/// separated set of `key=value` pairs. A client matches when its `name` equals
2933/// `unnamed` (an unnamed worker) or starts with `named_prefix` (a named worker,
2934/// `{clientName}:w:`). For matches, `name` is replaced with the queue name and
2935/// the raw client name is kept under `rawname`.
2936fn parse_client_list(
2937    list: &str,
2938    queue_name: &str,
2939    unnamed: &str,
2940    named_prefix: &str,
2941) -> Vec<HashMap<String, String>> {
2942    let mut clients = Vec::new();
2943
2944    for line in list.split(['\r', '\n']).filter(|l| !l.is_empty()) {
2945        let mut client: HashMap<String, String> = HashMap::new();
2946        for key_value in line.split(' ') {
2947            if let Some(idx) = key_value.find('=') {
2948                let key = &key_value[..idx];
2949                let value = &key_value[idx + 1..];
2950                client.insert(key.to_string(), value.to_string());
2951            }
2952        }
2953
2954        let name = client.get("name").cloned().unwrap_or_default();
2955        if !name.is_empty() && (name == unnamed || name.starts_with(named_prefix)) {
2956            client.insert("name".to_string(), queue_name.to_string());
2957            client.insert("rawname".to_string(), name);
2958            clients.push(client);
2959        }
2960    }
2961
2962    clients
2963}
2964
2965fn serialize_progress_for_script(progress: &crate::types::JobProgress) -> Result<String, Error> {
2966    match progress {
2967        // Match Node.js behavior: JSON.stringify(NaN/±Infinity) -> "null".
2968        crate::types::JobProgress::Number(n) if !n.is_finite() => Ok("null".to_string()),
2969        _ => Ok(serde_json::to_string(progress)?),
2970    }
2971}
2972
2973#[cfg(test)]
2974mod client_list_tests {
2975    use super::parse_client_list;
2976
2977    #[test]
2978    fn matches_unnamed_and_named_workers() {
2979        let unnamed = "bull:dGVzdA==";
2980        let named_prefix = "bull:dGVzdA==:w:";
2981        let list = format!(
2982            "id=1 addr=127.0.0.1:1 name={unnamed} age=5\n\
2983             id=2 addr=127.0.0.1:2 name={named_prefix}alpha age=3\n\
2984             id=3 addr=127.0.0.1:3 name=bull:b3RoZXI= age=1\n\
2985             id=4 addr=127.0.0.1:4 name= age=1\n"
2986        );
2987
2988        let clients = parse_client_list(&list, "test", unnamed, named_prefix);
2989        assert_eq!(clients.len(), 2);
2990        assert_eq!(clients[0].get("name"), Some(&"test".to_string()));
2991        assert_eq!(clients[0].get("rawname"), Some(&unnamed.to_string()));
2992        assert_eq!(
2993            clients[1].get("rawname"),
2994            Some(&format!("{named_prefix}alpha"))
2995        );
2996        assert_eq!(clients[1].get("name"), Some(&"test".to_string()));
2997    }
2998
2999    #[test]
3000    fn returns_empty_when_no_match() {
3001        let list = "id=1 addr=127.0.0.1:1 name=other age=5\n";
3002        let clients = parse_client_list(list, "test", "bull:dGVzdA==", "bull:dGVzdA==:w:");
3003        assert!(clients.is_empty());
3004    }
3005}
3006
3007#[cfg(test)]
3008mod prometheus_tests {
3009    use super::escape_prometheus_label_value;
3010
3011    #[test]
3012    fn escapes_special_characters() {
3013        assert_eq!(escape_prometheus_label_value("plain"), "plain");
3014        assert_eq!(escape_prometheus_label_value("a\\b"), "a\\\\b");
3015        assert_eq!(escape_prometheus_label_value("a\"b"), "a\\\"b");
3016        assert_eq!(escape_prometheus_label_value("a\nb"), "a\\nb");
3017    }
3018}
3019
3020#[cfg(test)]
3021mod progress_serialization_tests {
3022    use super::serialize_progress_for_script;
3023    use crate::types::JobProgress;
3024
3025    #[test]
3026    fn serializes_non_finite_numbers_as_null() {
3027        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3028            let serialized = serialize_progress_for_script(&JobProgress::Number(value)).unwrap();
3029            assert_eq!(serialized, "null");
3030        }
3031    }
3032
3033    #[test]
3034    fn serializes_regular_values_as_json() {
3035        let serialized = serialize_progress_for_script(&JobProgress::Number(42.0)).unwrap();
3036        assert_eq!(serialized, "42.0");
3037    }
3038}