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![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 pub async fn get_completed_count(&self) -> Result<u64, Error> {
1219 self.get_job_count_by_types(&["completed"]).await
1220 }
1221
1222 pub async fn get_failed_count(&self) -> Result<u64, Error> {
1224 self.get_job_count_by_types(&["failed"]).await
1225 }
1226
1227 pub async fn get_delayed_count(&self) -> Result<u64, Error> {
1229 self.get_job_count_by_types(&["delayed"]).await
1230 }
1231
1232 pub async fn get_active_count(&self) -> Result<u64, Error> {
1234 self.get_job_count_by_types(&["active"]).await
1235 }
1236
1237 pub async fn get_prioritized_count(&self) -> Result<u64, Error> {
1239 self.get_job_count_by_types(&["prioritized"]).await
1240 }
1241
1242 pub async fn get_waiting_count(&self) -> Result<u64, Error> {
1244 self.get_job_count_by_types(&["waiting"]).await
1245 }
1246
1247 pub async fn get_waiting_children_count(&self) -> Result<u64, Error> {
1249 self.get_job_count_by_types(&["waiting-children"]).await
1250 }
1251
1252 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 pub async fn get_workers_count(&self) -> Result<usize, Error> {
1285 Ok(self.get_workers().await?.len())
1286 }
1287
1288 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 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 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 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 pub async fn get_metrics(
1418 &self,
1419 metric_type: &str,
1420 start: i64,
1421 end: i64,
1422 ) -> Result<crate::types::Metrics, Error> {
1423 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 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 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 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 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 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 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 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 pub async fn remove(&self, job_id: &str) -> Result<bool, Error> {
1654 self.remove_job(job_id, true).await
1655 }
1656
1657 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 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 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 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 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 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 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 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 let ts_str = "9007199254740991".to_string(); 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 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") .arg("PX")
1947 .arg(expire_time_ms)
1948 .query_async::<()>(&mut conn)
1949 .await?;
1950
1951 Ok(())
1952 }
1953
1954 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn obliterate(&self, force: bool, count: usize) -> Result<(), Error> {
2272 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, 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 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 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 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 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 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 None
2387 };
2388
2389 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 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 let keys = vec![
2427 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(), ];
2439
2440 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(); let args: Vec<&[u8]> = vec![
2447 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, ];
2457
2458 let mut conn = self.conn.conn();
2459 let result = script.execute(&mut conn, &keys, &args).await?;
2460
2461 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 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 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 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 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 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 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), redis::Value::Int(1) => Ok(false), _ => Ok(false),
2611 }
2612 }
2613
2614 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 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 pub async fn close(&self) {
2679 self.conn.close().await;
2680 }
2681}
2682
2683#[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 pub fn options(mut self, opts: JobOptions) -> Self {
2712 self.opts = opts;
2713 self
2714 }
2715
2716 pub fn delay(mut self, delay: Duration) -> Self {
2718 self.opts.delay = Some(duration_as_millis(delay));
2719 self
2720 }
2721
2722 pub fn priority(mut self, priority: u32) -> Self {
2725 self.opts.priority = Some(priority);
2726 self
2727 }
2728
2729 pub fn attempts(mut self, attempts: u32) -> Self {
2731 self.opts.attempts = Some(attempts);
2732 self
2733 }
2734
2735 pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2737 self.opts.backoff = Some(backoff);
2738 self
2739 }
2740
2741 pub fn lifo(mut self) -> Self {
2743 self.opts.lifo = Some(true);
2744 self
2745 }
2746
2747 pub fn job_id(mut self, id: impl Into<String>) -> Self {
2749 self.opts.job_id = Some(id.into());
2750 self
2751 }
2752
2753 pub fn remove_on_complete(mut self, policy: RemoveOnFinish) -> Self {
2755 self.opts.remove_on_complete = Some(policy);
2756 self
2757 }
2758
2759 pub fn remove_on_fail(mut self, policy: RemoveOnFinish) -> Self {
2761 self.opts.remove_on_fail = Some(policy);
2762 self
2763 }
2764
2765 pub fn keep_logs(mut self, count: u32) -> Self {
2767 self.opts.keep_logs = Some(count);
2768 self
2769 }
2770
2771 pub fn parent(mut self, parent: ParentOptions) -> Self {
2773 self.opts.parent = Some(parent);
2774 self
2775 }
2776
2777 pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2779 self.opts.deduplication = Some(deduplication);
2780 self
2781 }
2782
2783 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#[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 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 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 pub fn options(mut self, opts: JobOptions) -> Self {
2854 self.opts = opts;
2855 self
2856 }
2857
2858 pub fn delay(mut self, delay: Duration) -> Self {
2860 self.opts = std::mem::take(&mut self.opts).delay(delay);
2861 self
2862 }
2863
2864 pub fn priority(mut self, priority: u32) -> Self {
2867 self.opts = std::mem::take(&mut self.opts).priority(priority);
2868 self
2869 }
2870
2871 pub fn attempts(mut self, attempts: u32) -> Self {
2873 self.opts = std::mem::take(&mut self.opts).attempts(attempts);
2874 self
2875 }
2876
2877 pub fn backoff(mut self, backoff: BackoffStrategy) -> Self {
2879 self.opts = std::mem::take(&mut self.opts).backoff(backoff);
2880 self
2881 }
2882
2883 pub fn lifo(mut self) -> Self {
2885 self.opts = std::mem::take(&mut self.opts).lifo();
2886 self
2887 }
2888
2889 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 pub fn parent(mut self, parent: ParentOptions) -> Self {
2897 self.opts = std::mem::take(&mut self.opts).parent(parent);
2898 self
2899 }
2900
2901 pub fn deduplication(mut self, deduplication: DeduplicationOptions) -> Self {
2903 self.opts = std::mem::take(&mut self.opts).deduplication(deduplication);
2904 self
2905 }
2906
2907 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
2914fn duration_as_millis(duration: Duration) -> u64 {
2916 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2917}
2918
2919fn escape_prometheus_label_value(value: &str) -> String {
2923 value
2924 .replace('\\', "\\\\")
2925 .replace('"', "\\\"")
2926 .replace('\n', "\\n")
2927}
2928
2929fn 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 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}