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
18const BULLMQ_VERSION: &str = "bullmq-official:0.1.0";
20
21const GET_JOBS_MAX_BACKFILL_ITERATIONS: usize = 5;
24
25pub type JobCountRecorder<'a> = &'a dyn Fn(&str, u64);
29
30#[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 pub async fn new(name: &str) -> Result<Self, Error> {
70 Self::with_options(name, QueueOptions::default()).await
71 }
72
73 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 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 pub fn name(&self) -> &str {
114 &self.name
115 }
116
117 pub fn keys(&self) -> &QueueKeys {
119 &self.keys
120 }
121
122 pub fn connection(&self) -> &RedisConnection {
124 &self.conn
125 }
126
127 pub fn add<T: Serialize>(&self, name: &str, data: T) -> AddJob<'_> {
150 AddJob::new(self, name, data)
151 }
152
153 #[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 #[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 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 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 let keys = self.add_job_keys(script_name);
274
275 let argv1 = self.pack_add_args(job, custom_job_id, timestamp)?;
277
278 let argv2 = serde_json::to_string(job.data())?;
280
281 Self::validate_job_size(job, &argv2)?;
284
285 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 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 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 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 write_str(&mut buf, &self.keys.key_prefix()).unwrap();
400 write_str(&mut buf, job_id).unwrap();
402 write_str(&mut buf, job.name()).unwrap();
404 write_uint(&mut buf, timestamp).unwrap();
406
407 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 write_nil(&mut buf).unwrap();
458 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 fn pack_job_opts(&self, job: &Job) -> Vec<u8> {
471 use rmp::encode::*;
472
473 let opts = job.opts();
474
475 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 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; 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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut unique: Vec<u64> = Vec::new();
1185 let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1186 for p in priorities {
1187 if seen.insert(*p) {
1188 unique.push(*p);
1189 }
1190 }
1191
1192 let keys = vec![
1193 self.keys.wait(),
1194 self.keys.paused(),
1195 self.keys.meta(),
1196 self.keys.prioritized(),
1197 ];
1198 let prio_strs: Vec<String> = unique.iter().map(|p| p.to_string()).collect();
1199 let args: Vec<&[u8]> = prio_strs.iter().map(|s| s.as_bytes()).collect();
1200
1201 let mut conn = self.conn.conn();
1202 let result = script.execute(&mut conn, &keys, &args).await?;
1203
1204 let counts: Vec<u64> = match result {
1205 redis::Value::Array(arr) => arr
1206 .into_iter()
1207 .map(|v| match v {
1208 redis::Value::Int(n) => n.max(0) as u64,
1209 _ => 0,
1210 })
1211 .collect(),
1212 _ => Vec::new(),
1213 };
1214
1215 let mut map = HashMap::new();
1216 for (i, p) in unique.iter().enumerate() {
1217 map.insert(*p, counts.get(i).copied().unwrap_or(0));
1218 }
1219 Ok(map)
1220 }
1221
1222 pub async fn get_completed_count(&self) -> Result<u64, Error> {
1224 self.get_job_count_by_types(&["completed"]).await
1225 }
1226
1227 pub async fn get_failed_count(&self) -> Result<u64, Error> {
1229 self.get_job_count_by_types(&["failed"]).await
1230 }
1231
1232 pub async fn get_delayed_count(&self) -> Result<u64, Error> {
1234 self.get_job_count_by_types(&["delayed"]).await
1235 }
1236
1237 pub async fn get_active_count(&self) -> Result<u64, Error> {
1239 self.get_job_count_by_types(&["active"]).await
1240 }
1241
1242 pub async fn get_prioritized_count(&self) -> Result<u64, Error> {
1244 self.get_job_count_by_types(&["prioritized"]).await
1245 }
1246
1247 pub async fn get_waiting_count(&self) -> Result<u64, Error> {
1249 self.get_job_count_by_types(&["waiting"]).await
1250 }
1251
1252 pub async fn get_waiting_children_count(&self) -> Result<u64, Error> {
1254 self.get_job_count_by_types(&["waiting-children"]).await
1255 }
1256
1257 pub async fn get_workers(&self) -> Result<Vec<HashMap<String, String>>, Error> {
1268 let mut cmd = redis::cmd("CLIENT");
1269 cmd.arg("LIST");
1270 let list: String = match self.conn.cmd(&mut cmd).await {
1271 Ok(list) => list,
1272 Err(err) => {
1273 debug!(error = %err, "CLIENT LIST unavailable; returning empty worker list");
1274 return Ok(Vec::new());
1275 }
1276 };
1277
1278 let unnamed = self.keys.client_name("");
1279 let named_prefix = self.keys.client_name(":w:");
1280 Ok(parse_client_list(
1281 &list,
1282 &self.name,
1283 &unnamed,
1284 &named_prefix,
1285 ))
1286 }
1287
1288 pub async fn get_workers_count(&self) -> Result<usize, Error> {
1290 Ok(self.get_workers().await?.len())
1291 }
1292
1293 pub async fn get_meta(&self) -> Result<QueueMeta, Error> {
1300 let mut conn = self.conn.conn();
1301 let config: HashMap<String, String> = redis::cmd("HGETALL")
1302 .arg(self.keys.meta())
1303 .query_async(&mut conn)
1304 .await?;
1305
1306 let mut meta = QueueMeta::default();
1307 for (key, value) in config {
1308 match key.as_str() {
1309 "concurrency" => meta.concurrency = value.parse().ok(),
1310 "max" => meta.max = value.parse().ok(),
1311 "duration" => meta.duration = value.parse().ok(),
1312 "opts.maxLenEvents" => meta.max_len_events = value.parse().ok(),
1313 "paused" => meta.paused = value == "1",
1314 _ => {
1315 meta.other.insert(key, value);
1316 }
1317 }
1318 }
1319
1320 Ok(meta)
1321 }
1322
1323 pub async fn get_version(&self) -> Result<Option<String>, Error> {
1328 let mut conn = self.conn.conn();
1329 let value: Option<String> = redis::cmd("HGET")
1330 .arg(self.keys.meta())
1331 .arg("library")
1332 .query_async(&mut conn)
1333 .await?;
1334 Ok(value)
1335 }
1336
1337 pub async fn is_maxed(&self) -> Result<bool, Error> {
1341 let script = self
1342 .conn
1343 .scripts()
1344 .get("isMaxed")
1345 .ok_or_else(|| Error::InvalidConfig("isMaxed script not found".to_string()))?
1346 .clone();
1347
1348 let keys = vec![self.keys.meta(), self.keys.active()];
1349 let args: Vec<&[u8]> = vec![];
1350
1351 let mut conn = self.conn.conn();
1352 let result = script.execute(&mut conn, &keys, &args).await?;
1353 Ok(matches!(result, redis::Value::Int(1)) || matches!(result, redis::Value::Boolean(true)))
1354 }
1355
1356 pub async fn export_prometheus_metrics(
1365 &self,
1366 global_labels: &[(&str, &str)],
1367 ) -> Result<String, Error> {
1368 let counts = self.get_job_counts().await?;
1369 let mut metrics: Vec<String> = Vec::new();
1370
1371 metrics.push("# HELP bullmq_job_count Number of jobs in the queue by state".to_string());
1372 metrics.push("# TYPE bullmq_job_count gauge".to_string());
1373
1374 let escaped_queue = escape_prometheus_label_value(&self.name);
1375 let variables: String = global_labels
1376 .iter()
1377 .map(|(k, v)| format!(", {}=\"{}\"", k, escape_prometheus_label_value(v)))
1378 .collect();
1379
1380 let states: [(&str, u64); 8] = [
1381 ("active", counts.active),
1382 ("completed", counts.completed),
1383 ("delayed", counts.delayed),
1384 ("failed", counts.failed),
1385 ("paused", counts.paused),
1386 ("prioritized", counts.prioritized),
1387 ("waiting", counts.waiting),
1388 ("waiting-children", counts.waiting_children),
1389 ];
1390 for (state, count) in states {
1391 metrics.push(format!(
1392 "bullmq_job_count{{queue=\"{escaped_queue}\", state=\"{state}\"{variables}}} {count}"
1393 ));
1394 }
1395
1396 let completed_metrics = self.get_metrics("completed", 0, -1).await?;
1397 let failed_metrics = self.get_metrics("failed", 0, -1).await?;
1398
1399 metrics
1400 .push("# HELP bullmq_job_completed_total Total number of completed jobs".to_string());
1401 metrics.push("# TYPE bullmq_job_completed_total counter".to_string());
1402 metrics.push(format!(
1403 "bullmq_job_completed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1404 completed_metrics.meta.count
1405 ));
1406
1407 metrics.push("# HELP bullmq_job_failed_total Total number of failed jobs".to_string());
1408 metrics.push("# TYPE bullmq_job_failed_total counter".to_string());
1409 metrics.push(format!(
1410 "bullmq_job_failed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1411 failed_metrics.meta.count
1412 ));
1413
1414 Ok(metrics.join("\n"))
1415 }
1416
1417 pub async fn get_metrics(
1423 &self,
1424 metric_type: &str,
1425 start: i64,
1426 end: i64,
1427 ) -> Result<crate::types::Metrics, Error> {
1428 if metric_type != "completed" && metric_type != "failed" {
1433 return Err(Error::InvalidConfig(format!(
1434 "metric type must be \"completed\" or \"failed\", got \"{}\"",
1435 metric_type
1436 )));
1437 }
1438
1439 let script = self
1440 .conn
1441 .scripts()
1442 .get("getMetrics")
1443 .ok_or_else(|| Error::InvalidConfig("getMetrics script not found".to_string()))?
1444 .clone();
1445
1446 let metrics_key = self.keys.get(&format!("metrics:{}", metric_type));
1447 let data_key = self.keys.get(&format!("metrics:{}:data", metric_type));
1448 let keys = vec![metrics_key, data_key];
1449
1450 let start_s = start.to_string();
1451 let end_s = end.to_string();
1452 let args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes()];
1453
1454 let mut conn = self.conn.conn();
1455 let result = script.execute(&mut conn, &keys, &args).await?;
1456
1457 let parse_u64 = |v: &redis::Value| -> u64 {
1459 match v {
1460 redis::Value::BulkString(b) => String::from_utf8_lossy(b).parse().unwrap_or(0),
1461 redis::Value::SimpleString(s) => s.parse().unwrap_or(0),
1462 redis::Value::Int(n) => (*n).max(0) as u64,
1463 _ => 0,
1464 }
1465 };
1466
1467 let mut metrics = crate::types::Metrics::default();
1468 if let redis::Value::Array(parts) = result {
1469 if let Some(redis::Value::Array(meta)) = parts.first() {
1470 metrics.meta.count = meta.first().map(parse_u64).unwrap_or(0);
1471 metrics.meta.prev_ts = meta.get(1).map(parse_u64).unwrap_or(0);
1472 metrics.meta.prev_count = meta.get(2).map(parse_u64).unwrap_or(0);
1473 }
1474 if let Some(redis::Value::Array(data)) = parts.get(1) {
1475 metrics.data = data.iter().map(parse_u64).collect();
1476 }
1477 if let Some(count) = parts.get(2) {
1478 metrics.count = parse_u64(count);
1479 }
1480 }
1481
1482 Ok(metrics)
1483 }
1484
1485 pub async fn get_children_values(
1487 &self,
1488 job_id: &str,
1489 ) -> Result<HashMap<String, serde_json::Value>, Error> {
1490 let processed_key = format!("{}:processed", self.keys.job_key(job_id));
1491 let mut conn = self.conn.conn();
1492
1493 let result: HashMap<String, String> = redis::cmd("HGETALL")
1494 .arg(&processed_key)
1495 .query_async(&mut conn)
1496 .await?;
1497
1498 let mut parsed = HashMap::new();
1499 for (key, value) in result {
1500 let parsed_value: serde_json::Value =
1501 serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
1502 parsed.insert(key, parsed_value);
1503 }
1504 Ok(parsed)
1505 }
1506
1507 pub async fn get_failed_children_values(
1509 &self,
1510 job_id: &str,
1511 ) -> Result<HashMap<String, String>, Error> {
1512 let failed_key = format!("{}:failed", self.keys.job_key(job_id));
1513 let mut conn = self.conn.conn();
1514
1515 let result: HashMap<String, String> = redis::cmd("HGETALL")
1516 .arg(&failed_key)
1517 .query_async(&mut conn)
1518 .await?;
1519
1520 Ok(result)
1521 }
1522
1523 pub async fn get_dependencies_count(&self, job_id: &str) -> Result<DependenciesCount, Error> {
1525 let job_key = self.keys.job_key(job_id);
1526 let processed_key = format!("{}:processed", job_key);
1527 let deps_key = format!("{}:dependencies", job_key);
1528 let failed_key = format!("{}:failed", job_key);
1529 let unsuccessful_key = format!("{}:unsuccessful", job_key);
1530
1531 let mut conn = self.conn.conn();
1532 let mut pipe = redis::pipe();
1533 pipe.cmd("HLEN").arg(&processed_key);
1534 pipe.cmd("SCARD").arg(&deps_key);
1535 pipe.cmd("HLEN").arg(&failed_key);
1536 pipe.cmd("ZCARD").arg(&unsuccessful_key);
1537
1538 let (processed, unprocessed, ignored, failed): (u64, u64, u64, u64) =
1539 pipe.query_async(&mut conn).await?;
1540
1541 Ok(DependenciesCount {
1542 processed,
1543 unprocessed,
1544 ignored,
1545 failed,
1546 })
1547 }
1548
1549 pub async fn get_unprocessed_dependencies(&self, job_id: &str) -> Result<Vec<String>, Error> {
1551 let deps_key = format!("{}:dependencies", self.keys.job_key(job_id));
1552 let mut conn = self.conn.conn();
1553
1554 let result: Vec<String> = redis::cmd("SMEMBERS")
1555 .arg(&deps_key)
1556 .query_async(&mut conn)
1557 .await?;
1558
1559 Ok(result)
1560 }
1561
1562 pub async fn remove_child_dependency(
1569 &self,
1570 job_id: &str,
1571 parent_key: &str,
1572 ) -> Result<bool, Error> {
1573 let script = self
1574 .conn
1575 .scripts()
1576 .get("removeChildDependency")
1577 .ok_or_else(|| {
1578 Error::InvalidConfig("removeChildDependency script not found".to_string())
1579 })?
1580 .clone();
1581
1582 let prefix_key = self.keys.key_prefix().to_string();
1583 let job_key = self.keys.job_key(job_id);
1584
1585 let keys = vec![prefix_key];
1586 let args: Vec<&[u8]> = vec![job_key.as_bytes(), parent_key.as_bytes()];
1587
1588 let mut conn = self.conn.conn();
1589 let result = script.execute(&mut conn, &keys, &args).await?;
1590
1591 match result {
1592 redis::Value::Int(0) => Ok(true),
1593 redis::Value::Int(1) => Ok(false),
1594 redis::Value::Int(-1) => Err(Error::InvalidConfig(format!(
1595 "Missing key for job {}. removeChildDependency",
1596 job_id
1597 ))),
1598 redis::Value::Int(-5) => Err(Error::InvalidConfig(format!(
1599 "Missing key for parent job {}. removeChildDependency",
1600 parent_key
1601 ))),
1602 _ => Ok(false),
1603 }
1604 }
1605
1606 pub async fn get_job_state(&self, job_id: &str) -> Result<JobState, Error> {
1608 let script = self
1609 .conn
1610 .scripts()
1611 .get("getState")
1612 .ok_or_else(|| Error::InvalidConfig("getState script not found".to_string()))?
1613 .clone();
1614
1615 let keys = vec![
1616 self.keys.completed(),
1617 self.keys.failed(),
1618 self.keys.delayed(),
1619 self.keys.active(),
1620 self.keys.wait(),
1621 self.keys.paused(),
1622 self.keys.waiting_children(),
1623 self.keys.prioritized(),
1624 ];
1625
1626 let job_id_bytes = job_id.as_bytes().to_vec();
1627 let args: Vec<&[u8]> = vec![&job_id_bytes];
1628
1629 let mut conn = self.conn.conn();
1630 let result = script.execute(&mut conn, &keys, &args).await?;
1631
1632 match result {
1633 redis::Value::BulkString(bytes) => {
1634 let state_str = String::from_utf8_lossy(&bytes);
1635 Ok(JobState::from_redis_str(&state_str))
1636 }
1637 redis::Value::SimpleString(s) => Ok(JobState::from_redis_str(&s)),
1638 _ => Ok(JobState::Unknown),
1639 }
1640 }
1641
1642 pub async fn remove(&self, job_id: &str) -> Result<bool, Error> {
1644 self.remove_job(job_id, true).await
1645 }
1646
1647 pub async fn remove_without_children(&self, job_id: &str) -> Result<bool, Error> {
1651 self.remove_job(job_id, false).await
1652 }
1653
1654 async fn remove_job(&self, job_id: &str, remove_children: bool) -> Result<bool, Error> {
1655 let script = self
1656 .conn
1657 .scripts()
1658 .get("removeJob")
1659 .ok_or_else(|| Error::InvalidConfig("removeJob script not found".to_string()))?
1660 .clone();
1661
1662 let keys = vec![self.keys.job_key(job_id), self.keys.repeat()];
1663 let prefix = self.keys.key_prefix();
1664 let remove_children_flag = if remove_children { b"1" as &[u8] } else { b"0" };
1665 let args: Vec<&[u8]> = vec![job_id.as_bytes(), remove_children_flag, prefix.as_bytes()];
1666
1667 let mut conn = self.conn.conn();
1668 let result = script.execute(&mut conn, &keys, &args).await?;
1669
1670 match result {
1671 redis::Value::Int(1) => Ok(true),
1675 redis::Value::Int(0) => Ok(false),
1676 redis::Value::Int(code) if code < 0 => {
1677 if code == crate::error::error_code::JOB_BELONGS_TO_JOB_SCHEDULER {
1678 Err(Error::Script {
1679 code,
1680 message: format!(
1681 "Job {} belongs to a job scheduler and cannot be removed directly. removeJob",
1682 job_id
1683 ),
1684 })
1685 } else {
1686 Err(Error::from_script_code(code))
1687 }
1688 }
1689 _ => Ok(false),
1690 }
1691 }
1692
1693 pub async fn remove_unprocessed_children(&self, job_id: &str) -> Result<(), Error> {
1698 let script = self
1699 .conn
1700 .scripts()
1701 .get("removeUnprocessedChildren")
1702 .ok_or_else(|| {
1703 Error::InvalidConfig("removeUnprocessedChildren script not found".to_string())
1704 })?
1705 .clone();
1706
1707 let keys = vec![self.keys.job_key(job_id), self.keys.meta()];
1708 let prefix = self.keys.key_prefix();
1709 let args: Vec<&[u8]> = vec![prefix.as_bytes(), job_id.as_bytes()];
1710
1711 let mut conn = self.conn.conn();
1712 script.execute(&mut conn, &keys, &args).await?;
1713 Ok(())
1714 }
1715
1716 pub async fn clean(&self, grace: u64, limit: u32, state: &str) -> Result<Vec<String>, Error> {
1724 let script = self
1725 .conn
1726 .scripts()
1727 .get("cleanJobsInSet")
1728 .ok_or_else(|| Error::InvalidConfig("cleanJobsInSet script not found".to_string()))?
1729 .clone();
1730
1731 let now = std::time::SystemTime::now()
1732 .duration_since(std::time::UNIX_EPOCH)
1733 .unwrap()
1734 .as_millis() as u64;
1735 let timestamp = now.saturating_sub(grace);
1736
1737 let normalized = if state == "waiting" { "wait" } else { state };
1739
1740 let set_key = match normalized {
1741 "completed" => self.keys.completed(),
1742 "failed" => self.keys.failed(),
1743 "wait" => self.keys.wait(),
1744 "active" => self.keys.active(),
1745 "delayed" => self.keys.delayed(),
1746 "paused" => self.keys.paused(),
1747 "prioritized" => self.keys.prioritized(),
1748 _ => return Err(Error::InvalidConfig(format!("invalid state: {}", state))),
1749 };
1750
1751 let max_per_call = if limit == 0 {
1752 10000u32
1753 } else {
1754 limit.min(10000)
1755 };
1756 let max_total = if limit == 0 { u32::MAX } else { limit };
1757 let mut all_deleted: Vec<String> = Vec::new();
1758
1759 loop {
1760 let keys = vec![set_key.clone(), self.keys.events(), self.keys.repeat()];
1761
1762 let prefix = self.keys.key_prefix();
1763 let ts_str = timestamp.to_string();
1764 let limit_str = max_per_call.to_string();
1765
1766 let args: Vec<&[u8]> = vec![
1767 prefix.as_bytes(),
1768 ts_str.as_bytes(),
1769 limit_str.as_bytes(),
1770 normalized.as_bytes(),
1771 ];
1772
1773 let mut conn = self.conn.conn();
1774 let result = script.execute(&mut conn, &keys, &args).await?;
1775
1776 let batch: Vec<String> = match result {
1777 redis::Value::Array(arr) => arr
1778 .into_iter()
1779 .filter_map(|v| match v {
1780 redis::Value::BulkString(bytes) => {
1781 Some(String::from_utf8_lossy(&bytes).to_string())
1782 }
1783 redis::Value::SimpleString(s) => Some(s),
1784 _ => None,
1785 })
1786 .collect(),
1787 _ => Vec::new(),
1788 };
1789
1790 let batch_len = batch.len() as u32;
1791 all_deleted.extend(batch);
1792
1793 if batch_len < max_per_call || all_deleted.len() as u32 >= max_total {
1794 break;
1795 }
1796 }
1797
1798 Ok(all_deleted)
1799 }
1800
1801 pub async fn drain(&self, delayed: bool) -> Result<(), Error> {
1803 let script = self
1804 .conn
1805 .scripts()
1806 .get("drain")
1807 .ok_or_else(|| Error::InvalidConfig("drain script not found".to_string()))?
1808 .clone();
1809
1810 let keys = vec![
1811 self.keys.wait(),
1812 self.keys.paused(),
1813 self.keys.delayed(),
1814 self.keys.prioritized(),
1815 self.keys.repeat(),
1816 ];
1817
1818 let delayed_str = if delayed { "1" } else { "0" };
1819 let prefix = self.keys.key_prefix();
1820 let args: Vec<&[u8]> = vec![prefix.as_bytes(), delayed_str.as_bytes()];
1821
1822 let mut conn = self.conn.conn();
1823 script.execute(&mut conn, &keys, &args).await?;
1824
1825 debug!(delayed, "queue drained");
1826 Ok(())
1827 }
1828
1829 pub async fn retry_jobs(
1835 &self,
1836 state: &str,
1837 count: u32,
1838 timestamp: Option<u64>,
1839 ) -> Result<(), Error> {
1840 let script = self
1841 .conn
1842 .scripts()
1843 .get("moveJobsToWait")
1844 .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1845 .clone();
1846
1847 let ts = timestamp.unwrap_or_else(|| {
1848 std::time::SystemTime::now()
1849 .duration_since(std::time::UNIX_EPOCH)
1850 .unwrap()
1851 .as_millis() as u64
1852 });
1853
1854 let keys = vec![
1855 self.keys.key_prefix(),
1856 self.keys.events(),
1857 self.keys.get(state),
1858 self.keys.wait(),
1859 self.keys.paused(),
1860 self.keys.meta(),
1861 self.keys.active(),
1862 self.keys.marker(),
1863 ];
1864
1865 let count_str = count.to_string();
1866 let ts_str = ts.to_string();
1867
1868 let mut conn = self.conn.conn();
1869 loop {
1870 let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), state.as_bytes()];
1871 let result = script.execute(&mut conn, &keys, &args).await?;
1872
1873 match result {
1874 redis::Value::Int(1) => continue,
1875 _ => break,
1876 }
1877 }
1878
1879 debug!(state, "retry_jobs completed");
1880 Ok(())
1881 }
1882
1883 pub async fn promote_jobs(&self, count: u32) -> Result<(), Error> {
1887 let script = self
1888 .conn
1889 .scripts()
1890 .get("moveJobsToWait")
1891 .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1892 .clone();
1893
1894 let keys = vec![
1895 self.keys.key_prefix(),
1896 self.keys.events(),
1897 self.keys.delayed(),
1898 self.keys.wait(),
1899 self.keys.paused(),
1900 self.keys.meta(),
1901 self.keys.active(),
1902 self.keys.marker(),
1903 ];
1904
1905 let count_str = count.to_string();
1906 let ts_str = "9007199254740991".to_string(); let mut conn = self.conn.conn();
1910 loop {
1911 let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), b"delayed"];
1912 let result = script.execute(&mut conn, &keys, &args).await?;
1913
1914 match result {
1915 redis::Value::Int(1) => continue,
1916 _ => break,
1917 }
1918 }
1919
1920 debug!("promote_jobs completed");
1921 Ok(())
1922 }
1923
1924 pub async fn rate_limit(&self, expire_time_ms: u64) -> Result<(), Error> {
1929 let limiter_key = self.keys.limiter();
1930 let mut conn = self.conn.conn();
1931
1932 redis::cmd("SET")
1933 .arg(&limiter_key)
1934 .arg("9007199254740991") .arg("PX")
1936 .arg(expire_time_ms)
1937 .query_async::<()>(&mut conn)
1938 .await?;
1939
1940 Ok(())
1941 }
1942
1943 pub async fn remove_rate_limit_key(&self) -> Result<bool, Error> {
1945 let limiter_key = self.keys.limiter();
1946 let mut conn = self.conn.conn();
1947
1948 let result: u32 = redis::cmd("DEL")
1949 .arg(&limiter_key)
1950 .query_async(&mut conn)
1951 .await?;
1952
1953 Ok(result > 0)
1954 }
1955
1956 pub async fn set_global_concurrency(&self, concurrency: u64) -> Result<(), Error> {
1959 let meta_key = self.keys.meta();
1960 let mut conn = self.conn.conn();
1961
1962 redis::cmd("HSET")
1963 .arg(&meta_key)
1964 .arg("concurrency")
1965 .arg(concurrency)
1966 .query_async::<()>(&mut conn)
1967 .await?;
1968
1969 Ok(())
1970 }
1971
1972 pub async fn remove_global_concurrency(&self) -> Result<(), Error> {
1974 let meta_key = self.keys.meta();
1975 let mut conn = self.conn.conn();
1976
1977 redis::cmd("HDEL")
1978 .arg(&meta_key)
1979 .arg("concurrency")
1980 .query_async::<()>(&mut conn)
1981 .await?;
1982
1983 Ok(())
1984 }
1985
1986 pub async fn set_global_rate_limit(&self, max: u64, duration: u64) -> Result<(), Error> {
1988 let meta_key = self.keys.meta();
1989 let mut conn = self.conn.conn();
1990
1991 redis::cmd("HSET")
1992 .arg(&meta_key)
1993 .arg("max")
1994 .arg(max)
1995 .arg("duration")
1996 .arg(duration)
1997 .query_async::<()>(&mut conn)
1998 .await?;
1999
2000 Ok(())
2001 }
2002
2003 pub async fn remove_global_rate_limit(&self) -> Result<(), Error> {
2005 let meta_key = self.keys.meta();
2006 let mut conn = self.conn.conn();
2007
2008 redis::cmd("HDEL")
2009 .arg(&meta_key)
2010 .arg("max")
2011 .arg("duration")
2012 .query_async::<()>(&mut conn)
2013 .await?;
2014
2015 Ok(())
2016 }
2017
2018 pub async fn get_rate_limit_ttl(&self, max_jobs: Option<u64>) -> Result<i64, Error> {
2025 let script = self
2026 .conn
2027 .scripts()
2028 .get("getRateLimitTtl")
2029 .ok_or_else(|| Error::InvalidConfig("getRateLimitTtl script not found".to_string()))?
2030 .clone();
2031
2032 let keys = vec![self.keys.limiter(), self.keys.meta()];
2033 let max_jobs_str = max_jobs
2034 .map(|m| m.to_string())
2035 .unwrap_or_else(|| "0".to_string());
2036 let args: Vec<&[u8]> = vec![max_jobs_str.as_bytes()];
2037
2038 let mut conn = self.conn.conn();
2039 let result = script.execute(&mut conn, &keys, &args).await?;
2040
2041 match result {
2042 redis::Value::Int(n) => Ok(n),
2043 _ => Ok(0),
2044 }
2045 }
2046
2047 pub async fn get_global_concurrency(&self) -> Result<Option<u64>, Error> {
2049 let meta_key = self.keys.meta();
2050 let mut conn = self.conn.conn();
2051
2052 let value: Option<String> = redis::cmd("HGET")
2053 .arg(&meta_key)
2054 .arg("concurrency")
2055 .query_async(&mut conn)
2056 .await?;
2057
2058 Ok(value.and_then(|v| v.parse::<u64>().ok()))
2059 }
2060
2061 pub async fn get_global_rate_limit(&self) -> Result<Option<(u64, u64)>, Error> {
2063 let meta_key = self.keys.meta();
2064 let mut conn = self.conn.conn();
2065
2066 let values: Vec<Option<String>> = redis::cmd("HMGET")
2067 .arg(&meta_key)
2068 .arg("max")
2069 .arg("duration")
2070 .query_async(&mut conn)
2071 .await?;
2072
2073 let max = values
2074 .first()
2075 .and_then(|v| v.as_ref())
2076 .and_then(|v| v.parse::<u64>().ok());
2077 let duration = values
2078 .get(1)
2079 .and_then(|v| v.as_ref())
2080 .and_then(|v| v.parse::<u64>().ok());
2081
2082 match (max, duration) {
2083 (Some(m), Some(d)) => Ok(Some((m, d))),
2084 _ => Ok(None),
2085 }
2086 }
2087
2088 pub async fn remove_deduplication_key(
2092 &self,
2093 deduplication_id: &str,
2094 job_id: &str,
2095 ) -> Result<bool, Error> {
2096 let script = self
2097 .conn
2098 .scripts()
2099 .get("removeDeduplicationKey")
2100 .ok_or_else(|| {
2101 Error::InvalidConfig("removeDeduplicationKey script not found".to_string())
2102 })?
2103 .clone();
2104
2105 let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2106 let keys = vec![dedup_key];
2107 let args: Vec<&[u8]> = vec![job_id.as_bytes()];
2108
2109 let mut conn = self.conn.conn();
2110 let result = script.execute(&mut conn, &keys, &args).await?;
2111
2112 match result {
2113 redis::Value::Int(1) => Ok(true),
2114 _ => Ok(false),
2115 }
2116 }
2117
2118 pub async fn get_deduplication_job_id(
2122 &self,
2123 deduplication_id: &str,
2124 ) -> Result<Option<String>, Error> {
2125 let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
2126 let mut conn = self.conn.conn();
2127 let result: redis::Value = redis::cmd("GET")
2128 .arg(&dedup_key)
2129 .query_async(&mut conn)
2130 .await
2131 .map_err(Error::Redis)?;
2132
2133 match result {
2134 redis::Value::BulkString(bytes) => {
2135 Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
2136 }
2137 redis::Value::SimpleString(s) => Ok(Some(s)),
2138 redis::Value::Nil => Ok(None),
2139 _ => Ok(None),
2140 }
2141 }
2142
2143 pub async fn get_debounce_job_id(&self, id: &str) -> Result<Option<String>, Error> {
2148 self.get_deduplication_job_id(id).await
2149 }
2150
2151 pub async fn remove_debounce_key(&self, id: &str) -> Result<u64, Error> {
2159 let dedup_key = format!("{}:de:{}", self.keys.base(), id);
2160 let mut conn = self.conn.conn();
2161 let deleted: u64 = redis::cmd("DEL")
2162 .arg(&dedup_key)
2163 .query_async(&mut conn)
2164 .await?;
2165 Ok(deleted)
2166 }
2167
2168 pub async fn get_job_logs(
2172 &self,
2173 job_id: &str,
2174 start: isize,
2175 end: isize,
2176 asc: bool,
2177 ) -> Result<(Vec<String>, usize), Error> {
2178 let logs_key = format!("{}{}:logs", self.keys.key_prefix(), job_id);
2179 let mut conn = self.conn.conn();
2180
2181 let (logs, count): (Vec<String>, usize) = if asc {
2182 redis::pipe()
2183 .cmd("LRANGE")
2184 .arg(&logs_key)
2185 .arg(start)
2186 .arg(end)
2187 .cmd("LLEN")
2188 .arg(&logs_key)
2189 .query_async(&mut conn)
2190 .await?
2191 } else {
2192 let actual_start = -(end + 1);
2193 let actual_end = -(start + 1);
2194 let (mut logs, count): (Vec<String>, usize) = redis::pipe()
2195 .cmd("LRANGE")
2196 .arg(&logs_key)
2197 .arg(actual_start)
2198 .arg(actual_end)
2199 .cmd("LLEN")
2200 .arg(&logs_key)
2201 .query_async(&mut conn)
2202 .await?;
2203 logs.reverse();
2204 (logs, count)
2205 };
2206
2207 Ok((logs, count))
2208 }
2209
2210 pub async fn trim_events(&self, max_length: usize) -> Result<usize, Error> {
2212 let mut conn = self.conn.conn();
2213 let trimmed: usize = redis::cmd("XTRIM")
2214 .arg(self.keys.events())
2215 .arg("MAXLEN")
2216 .arg("~")
2217 .arg(max_length)
2218 .query_async(&mut conn)
2219 .await?;
2220 Ok(trimmed)
2221 }
2222
2223 pub async fn update_job_progress(
2229 &self,
2230 job_id: &str,
2231 progress: crate::types::JobProgress,
2232 ) -> Result<(), Error> {
2233 let script = self
2234 .conn
2235 .scripts()
2236 .get("updateProgress")
2237 .ok_or_else(|| Error::InvalidConfig("updateProgress script not found".to_string()))?
2238 .clone();
2239
2240 let job_key = self.keys.job_key(job_id);
2241 let events_key = self.keys.events();
2242 let meta_key = self.keys.meta();
2243 let progress_json = serialize_progress_for_script(&progress)?;
2244
2245 let keys = vec![job_key, events_key, meta_key];
2246 let args: Vec<&[u8]> = vec![job_id.as_bytes(), progress_json.as_bytes()];
2247
2248 let mut conn = self.conn.conn();
2249 let result: redis::Value = script.execute(&mut conn, &keys, &args).await?;
2250
2251 match result {
2252 redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2253 _ => Ok(()),
2254 }
2255 }
2256
2257 pub async fn obliterate(&self, force: bool, count: usize) -> Result<(), Error> {
2261 if force {
2263 let _ = self.pause().await;
2264 }
2265
2266 let script = self
2267 .conn
2268 .scripts()
2269 .get("obliterate")
2270 .ok_or_else(|| Error::InvalidConfig("obliterate script not found".to_string()))?
2271 .clone();
2272
2273 let keys = vec![self.keys.meta(), self.keys.key_prefix()];
2274 let count_str = count.to_string();
2275 let force_str = if force { "1" } else { "0" };
2276
2277 let mut conn = self.conn.conn();
2278 loop {
2279 let args: Vec<&[u8]> = vec![count_str.as_bytes(), force_str.as_bytes()];
2280 let result = script.execute(&mut conn, &keys, &args).await?;
2281
2282 match result {
2283 redis::Value::Nil => break,
2284 redis::Value::Int(0) => break,
2285 redis::Value::Int(1) => continue, redis::Value::Int(code) if code < 0 => {
2287 return Err(Error::from_script_code(code));
2288 }
2289 _ => break,
2290 }
2291 }
2292
2293 debug!(force, "queue obliterated");
2294 Ok(())
2295 }
2296
2297 pub async fn upsert_job_scheduler(
2312 &self,
2313 job_scheduler_id: &str,
2314 repeat_opts: crate::job_scheduler::RepeatOptions,
2315 job_name: Option<&str>,
2316 job_data: Option<serde_json::Value>,
2317 job_opts: Option<JobOptions>,
2318 ) -> Result<Option<Job>, Error> {
2319 use crate::job_scheduler::{next_cron_millis, pack_delayed_job_opts, pack_scheduler_opts};
2320
2321 if repeat_opts.pattern.is_some() && repeat_opts.every.is_some() {
2323 return Err(Error::InvalidConfig(
2324 "Both .pattern and .every options are defined; only one may be used".to_string(),
2325 ));
2326 }
2327 if repeat_opts.pattern.is_none() && repeat_opts.every.is_none() {
2328 return Err(Error::InvalidConfig(
2329 "Either .pattern or .every option must be defined".to_string(),
2330 ));
2331 }
2332 if repeat_opts.immediately == Some(true) && repeat_opts.start_date.is_some() {
2333 return Err(Error::InvalidConfig(
2334 "Both .immediately and .startDate options are defined; only one may be used"
2335 .to_string(),
2336 ));
2337 }
2338
2339 let now = std::time::SystemTime::now()
2340 .duration_since(std::time::UNIX_EPOCH)
2341 .unwrap()
2342 .as_millis() as u64;
2343
2344 if let Some(end_date) = repeat_opts.end_date {
2346 if end_date < now {
2347 return Err(Error::InvalidConfig(
2348 "End date must be greater than current timestamp".to_string(),
2349 ));
2350 }
2351 }
2352
2353 let iteration_count = repeat_opts.count.unwrap_or(0) + 1;
2355 if let Some(limit) = repeat_opts.limit {
2356 if iteration_count > limit {
2357 return Ok(None);
2358 }
2359 }
2360
2361 let next_millis: Option<u64> = if let Some(ref pattern) = repeat_opts.pattern {
2363 if repeat_opts.immediately == Some(true) {
2364 Some(now)
2365 } else {
2366 next_cron_millis(
2367 pattern,
2368 now,
2369 repeat_opts.tz.as_deref(),
2370 repeat_opts.start_date,
2371 )?
2372 }
2373 } else {
2374 None
2376 };
2377
2378 if next_millis.is_none() && repeat_opts.every.is_none() {
2380 return Ok(None);
2381 }
2382
2383 let effective_name = job_name.unwrap_or(job_scheduler_id);
2384 let effective_data = job_data.unwrap_or(serde_json::json!({}));
2385 let effective_opts = job_opts.unwrap_or_default();
2386
2387 let offset = if repeat_opts.every.is_some() {
2388 repeat_opts.offset
2389 } else {
2390 None
2391 };
2392
2393 let scheduler_opts_packed = pack_scheduler_opts(effective_name, &repeat_opts);
2395 let template_data_json =
2396 serde_json::to_string(&effective_data).unwrap_or_else(|_| "{}".to_string());
2397 let template_opts_packed = self.pack_job_opts_from_options(&effective_opts);
2398 let delayed_job_opts_packed = pack_delayed_job_opts(
2399 &effective_opts,
2400 job_scheduler_id,
2401 next_millis.unwrap_or(0),
2402 iteration_count,
2403 offset,
2404 &repeat_opts,
2405 );
2406
2407 let script = self
2408 .conn
2409 .scripts()
2410 .get("addJobScheduler")
2411 .ok_or_else(|| Error::InvalidConfig("addJobScheduler script not found".to_string()))?
2412 .clone();
2413
2414 let keys = vec![
2416 self.keys.repeat(), self.keys.delayed(), self.keys.wait(), self.keys.paused(), self.keys.meta(), self.keys.prioritized(), self.keys.marker(), self.keys.id(), self.keys.events(), self.keys.pc(), self.keys.active(), ];
2428
2429 let next_millis_str = next_millis.unwrap_or(0).to_string();
2431 let timestamp_str = now.to_string();
2432 let prefix = self.keys.key_prefix();
2433 let producer_key = Vec::new(); let args: Vec<&[u8]> = vec![
2436 next_millis_str.as_bytes(), &scheduler_opts_packed, job_scheduler_id.as_bytes(), template_data_json.as_bytes(), &template_opts_packed, &delayed_job_opts_packed, timestamp_str.as_bytes(), prefix.as_bytes(), &producer_key, ];
2446
2447 let mut conn = self.conn.conn();
2448 let result = script.execute(&mut conn, &keys, &args).await?;
2449
2450 match result {
2452 redis::Value::Array(ref arr) if arr.len() >= 2 => {
2453 let job_id = match &arr[0] {
2454 redis::Value::BulkString(bytes) => String::from_utf8_lossy(bytes).to_string(),
2455 redis::Value::SimpleString(s) => s.clone(),
2456 redis::Value::Int(n) => n.to_string(),
2457 _ => return Ok(None),
2458 };
2459
2460 let mut job = Job::new(effective_name, effective_data, Some(effective_opts));
2461 job.set_id(job_id);
2462 job.set_context(self.make_script_context());
2463 Ok(Some(job))
2464 }
2465 redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2466 _ => Ok(None),
2467 }
2468 }
2469
2470 pub async fn get_job_scheduler(
2472 &self,
2473 job_scheduler_id: &str,
2474 ) -> Result<Option<crate::job_scheduler::JobSchedulerJson>, Error> {
2475 let script = self
2476 .conn
2477 .scripts()
2478 .get("getJobScheduler")
2479 .ok_or_else(|| Error::InvalidConfig("getJobScheduler script not found".to_string()))?
2480 .clone();
2481
2482 let keys = vec![self.keys.repeat()];
2483 let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes()];
2484
2485 let mut conn = self.conn.conn();
2486 let result = script.execute(&mut conn, &keys, &args).await?;
2487
2488 match result {
2490 redis::Value::Array(ref arr) if arr.len() >= 2 => {
2491 let fields = Self::parse_hash_array(&arr[0]);
2492 if fields.is_empty() {
2493 return Ok(None);
2494 }
2495 let next_millis = match &arr[1] {
2496 redis::Value::BulkString(bytes) => {
2497 String::from_utf8_lossy(bytes).parse::<u64>().ok()
2498 }
2499 redis::Value::SimpleString(s) => s.parse::<u64>().ok(),
2500 redis::Value::Int(n) => Some(*n as u64),
2501 _ => None,
2502 };
2503 Ok(Some(crate::job_scheduler::parse_scheduler_hash(
2504 job_scheduler_id,
2505 &fields,
2506 next_millis,
2507 )))
2508 }
2509 redis::Value::Nil => Ok(None),
2510 redis::Value::Array(ref arr) if arr.is_empty() => Ok(None),
2511 _ => Ok(None),
2512 }
2513 }
2514
2515 pub async fn get_job_schedulers(
2519 &self,
2520 start: isize,
2521 end: isize,
2522 asc: bool,
2523 ) -> Result<Vec<crate::job_scheduler::JobSchedulerJson>, Error> {
2524 let mut conn = self.conn.conn();
2525 let repeat_key = self.keys.repeat();
2526
2527 let results: Vec<(String, f64)> = if asc {
2529 redis::cmd("ZRANGE")
2530 .arg(&repeat_key)
2531 .arg(start)
2532 .arg(end)
2533 .arg("WITHSCORES")
2534 .query_async(&mut conn)
2535 .await?
2536 } else {
2537 redis::cmd("ZREVRANGE")
2538 .arg(&repeat_key)
2539 .arg(start)
2540 .arg(end)
2541 .arg("WITHSCORES")
2542 .query_async(&mut conn)
2543 .await?
2544 };
2545
2546 let mut schedulers = Vec::with_capacity(results.len());
2547 for (scheduler_id, score) in &results {
2548 let scheduler_hash_key = format!("{}repeat:{}", self.keys.key_prefix(), scheduler_id);
2549 let fields: std::collections::HashMap<String, String> = redis::cmd("HGETALL")
2550 .arg(&scheduler_hash_key)
2551 .query_async(&mut conn)
2552 .await?;
2553
2554 if !fields.is_empty() {
2555 schedulers.push(crate::job_scheduler::parse_scheduler_hash(
2556 scheduler_id,
2557 &fields,
2558 Some(*score as u64),
2559 ));
2560 }
2561 }
2562
2563 Ok(schedulers)
2564 }
2565
2566 pub async fn get_job_schedulers_count(&self) -> Result<u64, Error> {
2568 let mut conn = self.conn.conn();
2569 let count: u64 = redis::cmd("ZCARD")
2570 .arg(self.keys.repeat())
2571 .query_async(&mut conn)
2572 .await?;
2573 Ok(count)
2574 }
2575
2576 pub async fn remove_job_scheduler(&self, job_scheduler_id: &str) -> Result<bool, Error> {
2581 let script = self
2582 .conn
2583 .scripts()
2584 .get("removeJobScheduler")
2585 .ok_or_else(|| Error::InvalidConfig("removeJobScheduler script not found".to_string()))?
2586 .clone();
2587
2588 let keys = vec![self.keys.repeat(), self.keys.delayed(), self.keys.events()];
2589
2590 let prefix = self.keys.key_prefix();
2591 let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes(), prefix.as_bytes()];
2592
2593 let mut conn = self.conn.conn();
2594 let result = script.execute(&mut conn, &keys, &args).await?;
2595
2596 match result {
2597 redis::Value::Int(0) => Ok(true), redis::Value::Int(1) => Ok(false), _ => Ok(false),
2600 }
2601 }
2602
2603 fn parse_hash_array(value: &redis::Value) -> std::collections::HashMap<String, String> {
2605 let mut map = std::collections::HashMap::new();
2606 if let redis::Value::Array(arr) = value {
2607 let mut iter = arr.iter();
2608 while let (Some(key_val), Some(val_val)) = (iter.next(), iter.next()) {
2609 let key = match key_val {
2610 redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2611 redis::Value::SimpleString(s) => s.clone(),
2612 _ => continue,
2613 };
2614 let val = match val_val {
2615 redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2616 redis::Value::SimpleString(s) => s.clone(),
2617 redis::Value::Int(n) => n.to_string(),
2618 _ => String::new(),
2619 };
2620 map.insert(key, val);
2621 }
2622 }
2623 map
2624 }
2625
2626 fn pack_job_opts_from_options(&self, opts: &JobOptions) -> Vec<u8> {
2628 use rmp::encode::*;
2629
2630 let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
2631
2632 if let Some(attempts) = opts.attempts {
2633 let mut b = Vec::new();
2634 write_uint(&mut b, attempts as u64).unwrap();
2635 entries.push(("attempts", b));
2636 }
2637 if let Some(ref backoff) = opts.backoff {
2638 let b = Self::encode_backoff(backoff);
2639 entries.push(("backoff", b));
2640 }
2641 if let Some(ref roc) = opts.remove_on_complete {
2642 let b = Self::encode_remove_on_finish(roc);
2643 entries.push(("removeOnComplete", b));
2644 }
2645 if let Some(ref rof) = opts.remove_on_fail {
2646 let b = Self::encode_remove_on_finish(rof);
2647 entries.push(("removeOnFail", b));
2648 }
2649 if let Some(priority) = opts.priority {
2650 if priority > 0 {
2651 let mut b = Vec::new();
2652 write_uint(&mut b, priority as u64).unwrap();
2653 entries.push(("priority", b));
2654 }
2655 }
2656
2657 let mut buf = Vec::with_capacity(64);
2658 write_map_len(&mut buf, entries.len() as u32).unwrap();
2659 for (key, val) in &entries {
2660 write_str(&mut buf, key).unwrap();
2661 buf.extend_from_slice(val);
2662 }
2663 buf
2664 }
2665
2666 pub async fn close(&self) {
2668 self.conn.close().await;
2669 }
2670}
2671
2672#[must_use = "an AddJob does nothing until awaited"]
2679pub struct AddJob<'a> {
2680 queue: &'a Queue,
2681 name: String,
2682 data: Result<serde_json::Value, Error>,
2683 opts: JobOptions,
2684}
2685
2686impl<'a> AddJob<'a> {
2687 fn new<T: Serialize>(queue: &'a Queue, name: &str, data: T) -> Self {
2688 Self {
2689 queue,
2690 name: name.to_string(),
2691 data: serde_json::to_value(data).map_err(Error::from),
2692 opts: JobOptions::default(),
2693 }
2694 }
2695
2696 pub fn options(mut self, opts: JobOptions) -> Self {
2701 self.opts = opts;
2702 self
2703 }
2704
2705 pub fn delay(mut self, delay: Duration) -> Self {
2707 self.opts.delay = Some(duration_as_millis(delay));
2708 self
2709 }
2710
2711 pub fn priority(mut self, priority: u32) -> Self {
2714 self.opts.priority = Some(priority);
2715 self
2716 }
2717
2718 pub fn attempts(mut self, attempts: u32) -> Self {
2720 self.opts.attempts = Some(attempts);
2721 self
2722 }
2723
2724 pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2726 self.opts.backoff = Some(backoff);
2727 self
2728 }
2729
2730 pub fn lifo(mut self) -> Self {
2732 self.opts.lifo = Some(true);
2733 self
2734 }
2735
2736 pub fn job_id(mut self, id: impl Into<String>) -> Self {
2738 self.opts.job_id = Some(id.into());
2739 self
2740 }
2741
2742 pub fn remove_on_complete(mut self, policy: RemoveOnFinish) -> Self {
2744 self.opts.remove_on_complete = Some(policy);
2745 self
2746 }
2747
2748 pub fn remove_on_fail(mut self, policy: RemoveOnFinish) -> Self {
2750 self.opts.remove_on_fail = Some(policy);
2751 self
2752 }
2753
2754 pub fn keep_logs(mut self, count: u32) -> Self {
2756 self.opts.keep_logs = Some(count);
2757 self
2758 }
2759
2760 pub fn parent(mut self, parent: ParentOptions) -> Self {
2762 self.opts.parent = Some(parent);
2763 self
2764 }
2765
2766 pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2768 self.opts.deduplication = Some(deduplication);
2769 self
2770 }
2771
2772 pub fn size_limit(mut self, bytes: usize) -> Self {
2774 self.opts.size_limit = Some(bytes);
2775 self
2776 }
2777}
2778
2779impl<'a> IntoFuture for AddJob<'a> {
2780 type Output = Result<Job, Error>;
2781 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
2782
2783 fn into_future(self) -> Self::IntoFuture {
2784 Box::pin(async move {
2785 let data = self.data?;
2786 self.queue
2787 .add_internal(&self.name, data, Some(self.opts))
2788 .await
2789 })
2790 }
2791}
2792
2793#[must_use = "a BulkJob must be passed to Queue::add_bulk to have any effect"]
2816pub struct BulkJob {
2817 name: String,
2818 data: Result<serde_json::Value, Error>,
2819 opts: JobOptions,
2820}
2821
2822impl BulkJob {
2823 pub fn new<T: Serialize>(name: impl Into<String>, data: T) -> Self {
2825 Self {
2826 name: name.into(),
2827 data: serde_json::to_value(data).map_err(Error::from),
2828 opts: JobOptions::default(),
2829 }
2830 }
2831
2832 pub fn with_options<T: Serialize>(name: impl Into<String>, data: T, opts: JobOptions) -> Self {
2834 Self {
2835 name: name.into(),
2836 data: serde_json::to_value(data).map_err(Error::from),
2837 opts,
2838 }
2839 }
2840
2841 pub fn options(mut self, opts: JobOptions) -> Self {
2843 self.opts = opts;
2844 self
2845 }
2846
2847 pub fn delay(mut self, delay: Duration) -> Self {
2849 self.opts = std::mem::take(&mut self.opts).delay(delay);
2850 self
2851 }
2852
2853 pub fn priority(mut self, priority: u32) -> Self {
2856 self.opts = std::mem::take(&mut self.opts).priority(priority);
2857 self
2858 }
2859
2860 pub fn attempts(mut self, attempts: u32) -> Self {
2862 self.opts = std::mem::take(&mut self.opts).attempts(attempts);
2863 self
2864 }
2865
2866 pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2868 self.opts = std::mem::take(&mut self.opts).backoff(backoff);
2869 self
2870 }
2871
2872 pub fn lifo(mut self) -> Self {
2874 self.opts = std::mem::take(&mut self.opts).lifo();
2875 self
2876 }
2877
2878 pub fn job_id(mut self, id: impl Into<String>) -> Self {
2880 self.opts = std::mem::take(&mut self.opts).job_id(id);
2881 self
2882 }
2883
2884 pub fn parent(mut self, parent: ParentOptions) -> Self {
2886 self.opts = std::mem::take(&mut self.opts).parent(parent);
2887 self
2888 }
2889
2890 pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2892 self.opts = std::mem::take(&mut self.opts).deduplication(deduplication);
2893 self
2894 }
2895
2896 pub fn size_limit(mut self, bytes: usize) -> Self {
2898 self.opts = std::mem::take(&mut self.opts).size_limit(bytes);
2899 self
2900 }
2901}
2902
2903fn duration_as_millis(duration: Duration) -> u64 {
2905 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2906}
2907
2908fn escape_prometheus_label_value(value: &str) -> String {
2912 value
2913 .replace('\\', "\\\\")
2914 .replace('"', "\\\"")
2915 .replace('\n', "\\n")
2916}
2917
2918fn parse_client_list(
2926 list: &str,
2927 queue_name: &str,
2928 unnamed: &str,
2929 named_prefix: &str,
2930) -> Vec<HashMap<String, String>> {
2931 let mut clients = Vec::new();
2932
2933 for line in list.split(['\r', '\n']).filter(|l| !l.is_empty()) {
2934 let mut client: HashMap<String, String> = HashMap::new();
2935 for key_value in line.split(' ') {
2936 if let Some(idx) = key_value.find('=') {
2937 let key = &key_value[..idx];
2938 let value = &key_value[idx + 1..];
2939 client.insert(key.to_string(), value.to_string());
2940 }
2941 }
2942
2943 let name = client.get("name").cloned().unwrap_or_default();
2944 if !name.is_empty() && (name == unnamed || name.starts_with(named_prefix)) {
2945 client.insert("name".to_string(), queue_name.to_string());
2946 client.insert("rawname".to_string(), name);
2947 clients.push(client);
2948 }
2949 }
2950
2951 clients
2952}
2953
2954fn serialize_progress_for_script(progress: &crate::types::JobProgress) -> Result<String, Error> {
2955 match progress {
2956 crate::types::JobProgress::Number(n) if !n.is_finite() => Ok("null".to_string()),
2958 _ => Ok(serde_json::to_string(progress)?),
2959 }
2960}
2961
2962#[cfg(test)]
2963mod client_list_tests {
2964 use super::parse_client_list;
2965
2966 #[test]
2967 fn matches_unnamed_and_named_workers() {
2968 let unnamed = "bull:dGVzdA==";
2969 let named_prefix = "bull:dGVzdA==:w:";
2970 let list = format!(
2971 "id=1 addr=127.0.0.1:1 name={unnamed} age=5\n\
2972 id=2 addr=127.0.0.1:2 name={named_prefix}alpha age=3\n\
2973 id=3 addr=127.0.0.1:3 name=bull:b3RoZXI= age=1\n\
2974 id=4 addr=127.0.0.1:4 name= age=1\n"
2975 );
2976
2977 let clients = parse_client_list(&list, "test", unnamed, named_prefix);
2978 assert_eq!(clients.len(), 2);
2979 assert_eq!(clients[0].get("name"), Some(&"test".to_string()));
2980 assert_eq!(clients[0].get("rawname"), Some(&unnamed.to_string()));
2981 assert_eq!(
2982 clients[1].get("rawname"),
2983 Some(&format!("{named_prefix}alpha"))
2984 );
2985 assert_eq!(clients[1].get("name"), Some(&"test".to_string()));
2986 }
2987
2988 #[test]
2989 fn returns_empty_when_no_match() {
2990 let list = "id=1 addr=127.0.0.1:1 name=other age=5\n";
2991 let clients = parse_client_list(list, "test", "bull:dGVzdA==", "bull:dGVzdA==:w:");
2992 assert!(clients.is_empty());
2993 }
2994}
2995
2996#[cfg(test)]
2997mod prometheus_tests {
2998 use super::escape_prometheus_label_value;
2999
3000 #[test]
3001 fn escapes_special_characters() {
3002 assert_eq!(escape_prometheus_label_value("plain"), "plain");
3003 assert_eq!(escape_prometheus_label_value("a\\b"), "a\\\\b");
3004 assert_eq!(escape_prometheus_label_value("a\"b"), "a\\\"b");
3005 assert_eq!(escape_prometheus_label_value("a\nb"), "a\\nb");
3006 }
3007}
3008
3009#[cfg(test)]
3010mod progress_serialization_tests {
3011 use super::serialize_progress_for_script;
3012 use crate::types::JobProgress;
3013
3014 #[test]
3015 fn serializes_non_finite_numbers_as_null() {
3016 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3017 let serialized = serialize_progress_for_script(&JobProgress::Number(value)).unwrap();
3018 assert_eq!(serialized, "null");
3019 }
3020 }
3021
3022 #[test]
3023 fn serializes_regular_values_as_json() {
3024 let serialized = serialize_progress_for_script(&JobProgress::Number(42.0)).unwrap();
3025 assert_eq!(serialized, "42.0");
3026 }
3027}