1use std::collections::HashMap;
2use tracing::{debug, instrument};
3
4use crate::error::Error;
5use crate::job::{Job, ScriptContext};
6use crate::keys::{resolve_parent_queue_key, validate_queue_name, QueueKeys};
7use crate::options::{JobOptions, QueueOptions};
8use crate::redis_connection::RedisConnection;
9use crate::types::{DependenciesCount, JobCounts, JobState, QueueMeta};
10
11const BULLMQ_VERSION: &str = "bullmq-official:0.1.0";
13
14pub type JobCountRecorder<'a> = &'a dyn Fn(&str, u64);
18
19#[derive(Clone)]
24pub struct Queue {
25 name: String,
26 keys: QueueKeys,
27 conn: RedisConnection,
28 default_job_options: JobOptions,
29}
30
31impl Queue {
32 fn validate_job_size(job: &Job, argv2: &str) -> Result<(), Error> {
33 if let Some(size_limit) = job.opts().size_limit {
34 if argv2.len() > size_limit {
35 return Err(Error::InvalidConfig(format!(
36 "The size of job {} exceeds the limit {} bytes",
37 job.name(),
38 size_limit
39 )));
40 }
41 }
42
43 Ok(())
44 }
45
46 pub async fn new(name: &str, opts: QueueOptions) -> Result<Self, Error> {
48 validate_queue_name(name)?;
49 let conn = RedisConnection::new(&opts.connection).await?;
50 let keys = QueueKeys::new(name, Some(&opts.prefix));
51
52 let queue = Self {
53 name: name.to_string(),
54 keys,
55 conn,
56 default_job_options: opts.default_job_options,
57 };
58
59 queue.update_meta().await?;
60
61 Ok(queue)
62 }
63
64 pub async fn with_connection(
66 name: &str,
67 conn: RedisConnection,
68 opts: QueueOptions,
69 ) -> Result<Self, Error> {
70 validate_queue_name(name)?;
71 let keys = QueueKeys::new(name, Some(&opts.prefix));
72
73 let queue = Self {
74 name: name.to_string(),
75 keys,
76 conn,
77 default_job_options: opts.default_job_options,
78 };
79
80 queue.update_meta().await?;
81
82 Ok(queue)
83 }
84
85 pub fn name(&self) -> &str {
87 &self.name
88 }
89
90 pub fn keys(&self) -> &QueueKeys {
92 &self.keys
93 }
94
95 pub fn connection(&self) -> &RedisConnection {
97 &self.conn
98 }
99
100 #[instrument(skip(self, data, opts), fields(queue = %self.name))]
102 pub async fn add(
103 &self,
104 name: &str,
105 data: serde_json::Value,
106 opts: Option<JobOptions>,
107 ) -> Result<Job, Error> {
108 let merged_opts = self.merge_job_options(opts);
109 let mut job = Job::new(name, data, merged_opts);
110 self.add_job(&mut job).await?;
111 job.set_context(self.make_script_context());
112 Ok(job)
113 }
114
115 #[instrument(skip(self, jobs), fields(queue = %self.name, count = jobs.len()))]
119 pub async fn add_bulk(
120 &self,
121 jobs: Vec<(String, serde_json::Value, Option<JobOptions>)>,
122 ) -> Result<Vec<Job>, Error> {
123 if jobs.is_empty() {
124 return Ok(Vec::new());
125 }
126
127 let mut job_objects: Vec<Job> = jobs
128 .into_iter()
129 .map(|(name, data, opts)| {
130 let merged = self.merge_job_options(opts);
131 Job::new(&name, data, merged)
132 })
133 .collect();
134
135 let serialized_job_data: Vec<String> = job_objects
136 .iter()
137 .map(|job| {
138 let argv2 = serde_json::to_string(job.data())?;
139 Self::validate_job_size(job, &argv2)?;
140 Ok(argv2)
141 })
142 .collect::<Result<_, Error>>()?;
143
144 let futures: Vec<_> = job_objects
146 .iter()
147 .zip(serialized_job_data)
148 .map(|(job, argv2)| {
149 let delay = job.delay();
150 let priority = job.priority();
151 let timestamp = job.timestamp();
152 let custom_job_id = job.opts().job_id.as_deref().unwrap_or("").to_string();
153
154 let script_name = if delay > 0 {
155 "addDelayedJob"
156 } else if priority > 0 {
157 "addPrioritizedJob"
158 } else {
159 "addStandardJob"
160 };
161
162 let script = self
163 .conn
164 .scripts()
165 .get(script_name)
166 .ok_or_else(|| {
167 Error::InvalidConfig(format!("script '{}' not found", script_name))
168 })
169 .cloned();
170 let keys = self.add_job_keys(script_name);
171 let packed_args = self.pack_add_args(job, &custom_job_id, timestamp);
172 let argv3 = self.pack_job_opts(job);
173 let mut conn = self.conn.conn();
174
175 async move {
176 let script = script?;
177 let argv1 = packed_args?;
178 let argv2_bytes = argv2.into_bytes();
179 let args: Vec<&[u8]> = vec![&argv1, &argv2_bytes, &argv3];
180 let result = script.execute(&mut conn, &keys, &args).await?;
181 Self::parse_added_job_id(result)
182 }
183 })
184 .collect();
185
186 let results = futures::future::join_all(futures).await;
187
188 let ctx = self.make_script_context();
189 for (job, result) in job_objects.iter_mut().zip(results.into_iter()) {
190 let id = result?;
191 job.set_id(id);
192 job.set_context(ctx.clone());
193 }
194
195 Ok(job_objects)
196 }
197
198 async fn add_job(&self, job: &mut Job) -> Result<(), Error> {
200 let delay = job.delay();
201 let priority = job.priority();
202 let timestamp = job.timestamp();
203
204 let custom_job_id = job.opts().job_id.as_deref().unwrap_or("");
205
206 let script_name = if delay > 0 {
207 "addDelayedJob"
208 } else if priority > 0 {
209 "addPrioritizedJob"
210 } else {
211 "addStandardJob"
212 };
213
214 let script = self
215 .conn
216 .scripts()
217 .get(script_name)
218 .ok_or_else(|| Error::InvalidConfig(format!("script '{}' not found", script_name)))?
219 .clone();
220
221 let keys = self.add_job_keys(script_name);
223
224 let argv1 = self.pack_add_args(job, custom_job_id, timestamp)?;
226
227 let argv2 = serde_json::to_string(job.data())?;
229
230 Self::validate_job_size(job, &argv2)?;
233
234 let argv3 = self.pack_job_opts(job);
236
237 let argv2_bytes = argv2.into_bytes();
238 let args: Vec<&[u8]> = vec![&argv1, &argv2_bytes, &argv3];
239
240 let mut conn = self.conn.conn();
241 let result = script.execute(&mut conn, &keys, &args).await?;
242
243 let returned_job_id = Self::parse_added_job_id(result)?;
244 job.set_id(returned_job_id);
245
246 debug!(job_id = %job.id(), name = %job.name(), "job added");
247 Ok(())
248 }
249
250 fn make_script_context(&self) -> ScriptContext {
254 let (progress_tx, _) = tokio::sync::broadcast::channel(1);
255 ScriptContext {
256 conn: self.conn.clone(),
257 keys: self.keys.clone(),
258 progress_tx,
259 token: String::new(),
260 lock_duration: 0,
261 }
262 }
263
264 fn merge_job_options(&self, opts: Option<JobOptions>) -> Option<JobOptions> {
265 let defaults = &self.default_job_options;
266 let is_default = defaults.attempts.is_none()
267 && defaults.backoff.is_none()
268 && defaults.remove_on_complete.is_none()
269 && defaults.remove_on_fail.is_none()
270 && defaults.delay.is_none()
271 && defaults.priority.is_none()
272 && defaults.lifo.is_none();
273
274 if is_default {
275 return opts;
276 }
277
278 let job_opts = opts.unwrap_or_default();
279 Some(JobOptions {
280 attempts: job_opts.attempts.or(defaults.attempts),
281 backoff: job_opts
282 .backoff
283 .clone()
284 .or_else(|| defaults.backoff.clone()),
285 remove_on_complete: job_opts
286 .remove_on_complete
287 .clone()
288 .or_else(|| defaults.remove_on_complete.clone()),
289 remove_on_fail: job_opts
290 .remove_on_fail
291 .clone()
292 .or_else(|| defaults.remove_on_fail.clone()),
293 delay: job_opts.delay.or(defaults.delay),
294 priority: job_opts.priority.or(defaults.priority),
295 lifo: job_opts.lifo.or(defaults.lifo),
296 job_id: job_opts.job_id,
297 ..job_opts
298 })
299 }
300
301 fn add_job_keys(&self, script_name: &str) -> Vec<String> {
303 match script_name {
304 "addStandardJob" => vec![
305 self.keys.wait(),
306 self.keys.paused(),
307 self.keys.meta(),
308 self.keys.id(),
309 self.keys.completed(),
310 self.keys.delayed(),
311 self.keys.active(),
312 self.keys.events(),
313 self.keys.marker(),
314 ],
315 "addDelayedJob" => vec![
316 self.keys.marker(),
317 self.keys.meta(),
318 self.keys.id(),
319 self.keys.delayed(),
320 self.keys.completed(),
321 self.keys.events(),
322 ],
323 "addPrioritizedJob" => vec![
324 self.keys.marker(),
325 self.keys.meta(),
326 self.keys.id(),
327 self.keys.prioritized(),
328 self.keys.delayed(),
329 self.keys.completed(),
330 self.keys.active(),
331 self.keys.events(),
332 self.keys.pc(),
333 ],
334 _ => vec![],
335 }
336 }
337
338 fn pack_add_args(&self, job: &Job, job_id: &str, timestamp: u64) -> Result<Vec<u8>, Error> {
342 use rmp::encode::*;
343
344 let mut buf = Vec::with_capacity(128);
345 write_array_len(&mut buf, 9).unwrap();
346
347 write_str(&mut buf, &self.keys.key_prefix()).unwrap();
349 write_str(&mut buf, job_id).unwrap();
351 write_str(&mut buf, job.name()).unwrap();
353 write_uint(&mut buf, timestamp).unwrap();
355
356 if let Some(ref parent) = job.opts().parent {
358 let parent_queue_key = resolve_parent_queue_key(self.keys.prefix(), &parent.queue)?;
359 let parent_key = format!("{}:{}", parent_queue_key, parent.id);
360 let parent_deps_key = format!("{}:dependencies", parent_key);
361 let opts = job.opts();
362 let mut parent_map_len = 2u32;
363 if opts.fail_parent_on_failure == Some(true) {
364 parent_map_len += 1;
365 }
366 if opts.ignore_dependency_on_failure == Some(true) {
367 parent_map_len += 1;
368 }
369 if opts.remove_dependency_on_failure == Some(true) {
370 parent_map_len += 1;
371 }
372 if opts.continue_parent_on_failure == Some(true) {
373 parent_map_len += 1;
374 }
375
376 write_str(&mut buf, &parent_key).unwrap();
377 write_str(&mut buf, &parent_deps_key).unwrap();
378 write_map_len(&mut buf, parent_map_len).unwrap();
379 write_str(&mut buf, "id").unwrap();
380 write_str(&mut buf, &parent.id).unwrap();
381 write_str(&mut buf, "queueKey").unwrap();
382 write_str(&mut buf, &parent_queue_key).unwrap();
383 if opts.fail_parent_on_failure == Some(true) {
384 write_str(&mut buf, "fpof").unwrap();
385 write_bool(&mut buf, true).unwrap();
386 }
387 if opts.ignore_dependency_on_failure == Some(true) {
388 write_str(&mut buf, "idof").unwrap();
389 write_bool(&mut buf, true).unwrap();
390 }
391 if opts.remove_dependency_on_failure == Some(true) {
392 write_str(&mut buf, "rdof").unwrap();
393 write_bool(&mut buf, true).unwrap();
394 }
395 if opts.continue_parent_on_failure == Some(true) {
396 write_str(&mut buf, "cpof").unwrap();
397 write_bool(&mut buf, true).unwrap();
398 }
399 } else {
400 write_nil(&mut buf).unwrap();
401 write_nil(&mut buf).unwrap();
402 write_nil(&mut buf).unwrap();
403 }
404
405 write_nil(&mut buf).unwrap();
407 if let Some(ref dedup) = job.opts().deduplication {
409 let key = format!("{}:de:{}", self.keys.base(), dedup.id);
410 write_str(&mut buf, &key).unwrap();
411 } else {
412 write_nil(&mut buf).unwrap();
413 }
414
415 Ok(buf)
416 }
417
418 fn pack_job_opts(&self, job: &Job) -> Vec<u8> {
420 use rmp::encode::*;
421
422 let opts = job.opts();
423
424 let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
426
427 if let Some(delay) = opts.delay {
428 if delay > 0 {
429 let mut b = Vec::new();
430 write_uint(&mut b, delay).unwrap();
431 entries.push(("delay", b));
432 }
433 }
434 if let Some(priority) = opts.priority {
435 if priority > 0 {
436 let mut b = Vec::new();
437 write_uint(&mut b, priority as u64).unwrap();
438 entries.push(("priority", b));
439 }
440 }
441 if let Some(attempts) = opts.attempts {
442 let mut b = Vec::new();
443 write_uint(&mut b, attempts as u64).unwrap();
444 entries.push(("attempts", b));
445 }
446 if let Some(true) = opts.lifo {
447 let mut b = Vec::new();
448 write_bool(&mut b, true).unwrap();
449 entries.push(("lifo", b));
450 }
451
452 if let Some(ref roc) = opts.remove_on_complete {
453 let b = Self::encode_remove_on_finish(roc);
454 entries.push(("removeOnComplete", b));
455 }
456
457 if let Some(ref rof) = opts.remove_on_fail {
458 let b = Self::encode_remove_on_finish(rof);
459 entries.push(("removeOnFail", b));
460 }
461
462 if let Some(ref backoff) = opts.backoff {
463 let b = Self::encode_backoff(backoff);
464 entries.push(("backoff", b));
465 }
466
467 if let Some(ref dedup) = opts.deduplication {
468 let b = Self::encode_deduplication(dedup);
469 entries.push(("de", b));
470 }
471
472 let mut buf = Vec::with_capacity(64);
474 write_map_len(&mut buf, entries.len() as u32).unwrap();
475 for (key, val) in &entries {
476 write_str(&mut buf, key).unwrap();
477 buf.extend_from_slice(val);
478 }
479
480 buf
481 }
482
483 pub(crate) fn encode_remove_on_finish(rof: &crate::types::RemoveOnFinish) -> Vec<u8> {
484 use rmp::encode::*;
485 let mut b = Vec::new();
486 match rof {
487 crate::types::RemoveOnFinish::Bool(val) => {
488 write_bool(&mut b, *val).unwrap();
489 }
490 crate::types::RemoveOnFinish::Count(n) => {
491 write_uint(&mut b, *n as u64).unwrap();
492 }
493 crate::types::RemoveOnFinish::Options(keep) => {
494 let mut count = 0u32;
495 if keep.age.is_some() {
496 count += 1;
497 }
498 if keep.count.is_some() {
499 count += 1;
500 }
501 if keep.limit.is_some() {
502 count += 1;
503 }
504 write_map_len(&mut b, count).unwrap();
505 if let Some(age) = keep.age {
506 write_str(&mut b, "age").unwrap();
507 write_uint(&mut b, age).unwrap();
508 }
509 if let Some(cnt) = keep.count {
510 write_str(&mut b, "count").unwrap();
511 write_uint(&mut b, cnt as u64).unwrap();
512 }
513 if let Some(limit) = keep.limit {
514 write_str(&mut b, "limit").unwrap();
515 write_uint(&mut b, limit as u64).unwrap();
516 }
517 }
518 }
519 b
520 }
521
522 pub(crate) fn encode_backoff(backoff: &crate::types::BackoffStrategy) -> Vec<u8> {
523 use rmp::encode::*;
524 let mut b = Vec::new();
525 match backoff {
526 crate::types::BackoffStrategy::Fixed(delay) => {
527 write_map_len(&mut b, 2).unwrap();
528 write_str(&mut b, "type").unwrap();
529 write_str(&mut b, "fixed").unwrap();
530 write_str(&mut b, "delay").unwrap();
531 write_uint(&mut b, *delay).unwrap();
532 }
533 crate::types::BackoffStrategy::Exponential(delay) => {
534 write_map_len(&mut b, 2).unwrap();
535 write_str(&mut b, "type").unwrap();
536 write_str(&mut b, "exponential").unwrap();
537 write_str(&mut b, "delay").unwrap();
538 write_uint(&mut b, *delay).unwrap();
539 }
540 crate::types::BackoffStrategy::Custom(name) => {
541 write_map_len(&mut b, 2).unwrap();
542 write_str(&mut b, "type").unwrap();
543 write_str(&mut b, name).unwrap();
544 write_str(&mut b, "delay").unwrap();
545 write_uint(&mut b, 0).unwrap();
546 }
547 }
548 b
549 }
550
551 pub(crate) fn encode_deduplication(dedup: &crate::options::DeduplicationOptions) -> Vec<u8> {
552 use rmp::encode::*;
553 let mut b = Vec::new();
554
555 let mut count = 1u32; if dedup.ttl.is_some() {
557 count += 1;
558 }
559 if dedup.extend.is_some() {
560 count += 1;
561 }
562 if dedup.replace.is_some() {
563 count += 1;
564 }
565 if dedup.keep_last_if_active.is_some() {
566 count += 1;
567 }
568
569 write_map_len(&mut b, count).unwrap();
570 write_str(&mut b, "id").unwrap();
571 write_str(&mut b, &dedup.id).unwrap();
572
573 if let Some(ttl) = dedup.ttl {
574 write_str(&mut b, "ttl").unwrap();
575 write_uint(&mut b, ttl).unwrap();
576 }
577 if let Some(extend) = dedup.extend {
578 write_str(&mut b, "extend").unwrap();
579 write_bool(&mut b, extend).unwrap();
580 }
581 if let Some(replace) = dedup.replace {
582 write_str(&mut b, "replace").unwrap();
583 write_bool(&mut b, replace).unwrap();
584 }
585 if let Some(keep_last) = dedup.keep_last_if_active {
586 write_str(&mut b, "keepLastIfActive").unwrap();
587 write_bool(&mut b, keep_last).unwrap();
588 }
589
590 b
591 }
592
593 fn parse_added_job_id(result: redis::Value) -> Result<String, Error> {
594 match result {
595 redis::Value::BulkString(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()),
596 redis::Value::SimpleString(value) => Ok(value),
597 redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
598 redis::Value::Int(job_id) => Ok(job_id.to_string()),
599 value => Err(Error::InvalidConfig(format!(
600 "unexpected add job script result: {:?}",
601 value
602 ))),
603 }
604 }
605
606 async fn update_meta(&self) -> Result<(), Error> {
608 let mut conn = self.conn.conn();
609 let meta_key = self.keys.meta();
610
611 redis::cmd("HSET")
612 .arg(&meta_key)
613 .arg("library")
614 .arg(BULLMQ_VERSION)
615 .query_async::<()>(&mut conn)
616 .await?;
617
618 Ok(())
619 }
620
621 #[instrument(skip(self), fields(queue = %self.name))]
623 pub async fn pause(&self) -> Result<(), Error> {
624 let script = self
625 .conn
626 .scripts()
627 .get("pause")
628 .ok_or_else(|| Error::InvalidConfig("pause script not found".to_string()))?
629 .clone();
630
631 let keys = vec![
632 self.keys.wait(),
633 self.keys.paused(),
634 self.keys.meta(),
635 self.keys.prioritized(),
636 self.keys.events(),
637 self.keys.delayed(),
638 self.keys.marker(),
639 ];
640
641 let args: Vec<&[u8]> = vec![b"paused"];
642
643 let mut conn = self.conn.conn();
644 script.execute(&mut conn, &keys, &args).await?;
645
646 debug!("queue paused");
647 Ok(())
648 }
649
650 #[instrument(skip(self), fields(queue = %self.name))]
652 pub async fn resume(&self) -> Result<(), Error> {
653 let script = self
654 .conn
655 .scripts()
656 .get("pause")
657 .ok_or_else(|| Error::InvalidConfig("pause script not found".to_string()))?
658 .clone();
659
660 let keys = vec![
661 self.keys.paused(),
662 self.keys.wait(),
663 self.keys.meta(),
664 self.keys.prioritized(),
665 self.keys.events(),
666 self.keys.delayed(),
667 self.keys.marker(),
668 ];
669
670 let args: Vec<&[u8]> = vec![b"resumed"];
671
672 let mut conn = self.conn.conn();
673 script.execute(&mut conn, &keys, &args).await?;
674
675 debug!("queue resumed");
676 Ok(())
677 }
678
679 pub async fn is_paused(&self) -> Result<bool, Error> {
681 let mut conn = self.conn.conn();
682 let paused: Option<String> = redis::cmd("HGET")
683 .arg(self.keys.meta())
684 .arg("paused")
685 .query_async(&mut conn)
686 .await?;
687 Ok(paused.as_deref() == Some("1"))
688 }
689
690 pub async fn get_job(&self, job_id: &str) -> Result<Option<Job>, Error> {
692 let job = Job::from_id(&self.conn, &self.keys, job_id).await?;
693 Ok(job.map(|mut j| {
694 j.set_context(self.make_script_context());
695 j
696 }))
697 }
698
699 pub async fn get_job_counts(&self) -> Result<JobCounts, Error> {
701 let mut conn = self.conn.conn();
702 let mut pipe = redis::pipe();
703
704 pipe.cmd("LLEN").arg(self.keys.wait());
705 pipe.cmd("LLEN").arg(self.keys.active());
706 pipe.cmd("ZCARD").arg(self.keys.delayed());
707 pipe.cmd("ZCARD").arg(self.keys.prioritized());
708 pipe.cmd("ZCARD").arg(self.keys.completed());
709 pipe.cmd("ZCARD").arg(self.keys.failed());
710 pipe.cmd("ZCARD").arg(self.keys.waiting_children());
711 pipe.cmd("LLEN").arg(self.keys.paused());
712
713 let result: Vec<u64> = pipe.query_async(&mut conn).await?;
714
715 Ok(JobCounts {
716 waiting: result.first().copied().unwrap_or(0),
717 active: result.get(1).copied().unwrap_or(0),
718 delayed: result.get(2).copied().unwrap_or(0),
719 prioritized: result.get(3).copied().unwrap_or(0),
720 completed: result.get(4).copied().unwrap_or(0),
721 failed: result.get(5).copied().unwrap_or(0),
722 waiting_children: result.get(6).copied().unwrap_or(0),
723 paused: result.get(7).copied().unwrap_or(0),
724 })
725 }
726
727 fn sanitize_job_types(types: &[&str]) -> Vec<String> {
739 if types.is_empty() {
740 return [
741 "active",
742 "completed",
743 "delayed",
744 "failed",
745 "paused",
746 "prioritized",
747 "waiting",
748 "waiting-children",
749 ]
750 .iter()
751 .map(|s| s.to_string())
752 .collect();
753 }
754
755 let mut out: Vec<String> = Vec::new();
756 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
757 for t in types {
758 if seen.insert((*t).to_string()) {
759 out.push((*t).to_string());
760 }
761 }
762 if types.contains(&"waiting") && seen.insert("paused".to_string()) {
763 out.push("paused".to_string());
764 }
765 out
766 }
767
768 pub async fn get_ranges(
779 &self,
780 types: &[&str],
781 start: i64,
782 end: i64,
783 asc: bool,
784 ) -> Result<Vec<String>, Error> {
785 let script = self
786 .conn
787 .scripts()
788 .get("getRanges")
789 .ok_or_else(|| Error::InvalidConfig("getRanges script not found".to_string()))?
790 .clone();
791
792 let keys = vec![self.keys.key_prefix()];
794
795 let start_s = start.to_string();
796 let end_s = end.to_string();
797 let asc_s = if asc { "1" } else { "0" };
798 let sanitized = Self::sanitize_job_types(types);
803 let transformed: Vec<String> = sanitized
804 .iter()
805 .map(|t| {
806 if t == "waiting" {
807 "wait".to_string()
808 } else {
809 t.clone()
810 }
811 })
812 .collect();
813
814 let mut args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes(), asc_s.as_bytes()];
815 for t in &transformed {
816 args.push(t.as_bytes());
817 }
818
819 let mut conn = self.conn.conn();
820 let result = script.execute(&mut conn, &keys, &args).await?;
821
822 let extract_id = |v: redis::Value| -> Option<String> {
827 match v {
828 redis::Value::BulkString(bytes) => {
829 Some(String::from_utf8_lossy(&bytes).to_string())
830 }
831 redis::Value::SimpleString(s) => Some(s),
832 _ => None,
833 }
834 };
835
836 let mut out: Vec<String> = Vec::new();
837 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
838
839 if let redis::Value::Array(groups) = result {
840 for (i, group) in groups.into_iter().enumerate() {
841 let is_list = matches!(
842 transformed.get(i).map(|s| s.as_str()),
843 Some("wait") | Some("paused") | Some("active")
844 );
845 let mut ids: Vec<String> = match group {
846 redis::Value::Array(items) => {
847 items.into_iter().filter_map(extract_id).collect()
848 }
849 other => extract_id(other).into_iter().collect(),
850 };
851 if asc && is_list {
852 ids.reverse();
853 }
854 for id in ids {
855 if seen.insert(id.clone()) {
856 out.push(id);
857 }
858 }
859 }
860 }
861
862 Ok(out)
863 }
864
865 pub async fn get_jobs(
869 &self,
870 types: &[&str],
871 start: i64,
872 end: i64,
873 asc: bool,
874 ) -> Result<Vec<Job>, Error> {
875 let ids = self.get_ranges(types, start, end, asc).await?;
878
879 let fetches = ids.iter().map(|id| self.get_job(id));
881 let results = futures::future::join_all(fetches).await;
882
883 let mut jobs = Vec::with_capacity(ids.len());
884 for result in results {
885 if let Some(job) = result? {
886 jobs.push(job);
887 }
888 }
889 Ok(jobs)
890 }
891
892 pub async fn get_waiting(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
894 self.get_jobs(&["waiting"], start, end, true).await
895 }
896
897 pub async fn get_waiting_children(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
899 self.get_jobs(&["waiting-children"], start, end, true).await
900 }
901
902 pub async fn get_active(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
904 self.get_jobs(&["active"], start, end, true).await
905 }
906
907 pub async fn get_delayed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
909 self.get_jobs(&["delayed"], start, end, true).await
910 }
911
912 pub async fn get_prioritized(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
914 self.get_jobs(&["prioritized"], start, end, true).await
915 }
916
917 pub async fn get_completed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
919 self.get_jobs(&["completed"], start, end, false).await
920 }
921
922 pub async fn get_failed(&self, start: i64, end: i64) -> Result<Vec<Job>, Error> {
924 self.get_jobs(&["failed"], start, end, false).await
925 }
926
927 async fn get_counts_raw(&self, types: &[String]) -> Result<Vec<u64>, Error> {
930 let script = self
931 .conn
932 .scripts()
933 .get("getCounts")
934 .ok_or_else(|| Error::InvalidConfig("getCounts script not found".to_string()))?
935 .clone();
936
937 let keys = vec![self.keys.key_prefix()];
938 let transformed: Vec<String> = types
939 .iter()
940 .map(|t| {
941 if t == "waiting" {
942 "wait".to_string()
943 } else {
944 t.clone()
945 }
946 })
947 .collect();
948 let args: Vec<&[u8]> = transformed.iter().map(|s| s.as_bytes()).collect();
949
950 let mut conn = self.conn.conn();
951 let result = script.execute(&mut conn, &keys, &args).await?;
952
953 let counts = match result {
954 redis::Value::Array(arr) => arr
955 .into_iter()
956 .map(|v| match v {
957 redis::Value::Int(n) => n.max(0) as u64,
958 _ => 0,
959 })
960 .collect(),
961 _ => Vec::new(),
962 };
963 Ok(counts)
964 }
965
966 pub async fn get_job_counts_by_types(
970 &self,
971 types: &[&str],
972 ) -> Result<HashMap<String, u64>, Error> {
973 let sanitized = Self::sanitize_job_types(types);
974 let counts = self.get_counts_raw(&sanitized).await?;
975
976 let mut map = HashMap::new();
977 for (i, t) in sanitized.iter().enumerate() {
978 map.insert(t.clone(), counts.get(i).copied().unwrap_or(0));
979 }
980 Ok(map)
981 }
982
983 pub async fn get_job_count_by_types(&self, types: &[&str]) -> Result<u64, Error> {
985 let map = self.get_job_counts_by_types(types).await?;
986 Ok(map.values().sum())
987 }
988
989 pub async fn record_job_counts_metric(
997 &self,
998 types: &[&str],
999 recorder: Option<JobCountRecorder<'_>>,
1000 ) -> Result<HashMap<String, u64>, Error> {
1001 let counts = self.get_job_counts_by_types(types).await?;
1002 if let Some(record) = recorder {
1003 for (state, count) in &counts {
1004 record(state, *count);
1005 }
1006 }
1007 Ok(counts)
1008 }
1009
1010 pub async fn count(&self) -> Result<u64, Error> {
1013 self.get_job_count_by_types(&[
1014 "waiting",
1015 "paused",
1016 "delayed",
1017 "prioritized",
1018 "waiting-children",
1019 ])
1020 .await
1021 }
1022
1023 pub async fn get_counts_per_priority(
1027 &self,
1028 priorities: &[u64],
1029 ) -> Result<HashMap<u64, u64>, Error> {
1030 let script = self
1031 .conn
1032 .scripts()
1033 .get("getCountsPerPriority")
1034 .ok_or_else(|| {
1035 Error::InvalidConfig("getCountsPerPriority script not found".to_string())
1036 })?
1037 .clone();
1038
1039 let mut unique: Vec<u64> = Vec::new();
1041 let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1042 for p in priorities {
1043 if seen.insert(*p) {
1044 unique.push(*p);
1045 }
1046 }
1047
1048 let keys = vec![
1049 self.keys.wait(),
1050 self.keys.paused(),
1051 self.keys.meta(),
1052 self.keys.prioritized(),
1053 ];
1054 let prio_strs: Vec<String> = unique.iter().map(|p| p.to_string()).collect();
1055 let args: Vec<&[u8]> = prio_strs.iter().map(|s| s.as_bytes()).collect();
1056
1057 let mut conn = self.conn.conn();
1058 let result = script.execute(&mut conn, &keys, &args).await?;
1059
1060 let counts: Vec<u64> = match result {
1061 redis::Value::Array(arr) => arr
1062 .into_iter()
1063 .map(|v| match v {
1064 redis::Value::Int(n) => n.max(0) as u64,
1065 _ => 0,
1066 })
1067 .collect(),
1068 _ => Vec::new(),
1069 };
1070
1071 let mut map = HashMap::new();
1072 for (i, p) in unique.iter().enumerate() {
1073 map.insert(*p, counts.get(i).copied().unwrap_or(0));
1074 }
1075 Ok(map)
1076 }
1077
1078 pub async fn get_completed_count(&self) -> Result<u64, Error> {
1080 self.get_job_count_by_types(&["completed"]).await
1081 }
1082
1083 pub async fn get_failed_count(&self) -> Result<u64, Error> {
1085 self.get_job_count_by_types(&["failed"]).await
1086 }
1087
1088 pub async fn get_delayed_count(&self) -> Result<u64, Error> {
1090 self.get_job_count_by_types(&["delayed"]).await
1091 }
1092
1093 pub async fn get_active_count(&self) -> Result<u64, Error> {
1095 self.get_job_count_by_types(&["active"]).await
1096 }
1097
1098 pub async fn get_prioritized_count(&self) -> Result<u64, Error> {
1100 self.get_job_count_by_types(&["prioritized"]).await
1101 }
1102
1103 pub async fn get_waiting_count(&self) -> Result<u64, Error> {
1105 self.get_job_count_by_types(&["waiting"]).await
1106 }
1107
1108 pub async fn get_waiting_children_count(&self) -> Result<u64, Error> {
1110 self.get_job_count_by_types(&["waiting-children"]).await
1111 }
1112
1113 pub async fn get_workers(&self) -> Result<Vec<HashMap<String, String>>, Error> {
1124 let mut cmd = redis::cmd("CLIENT");
1125 cmd.arg("LIST");
1126 let list: String = match self.conn.cmd(&mut cmd).await {
1127 Ok(list) => list,
1128 Err(err) => {
1129 debug!(error = %err, "CLIENT LIST unavailable; returning empty worker list");
1130 return Ok(Vec::new());
1131 }
1132 };
1133
1134 let unnamed = self.keys.client_name("");
1135 let named_prefix = self.keys.client_name(":w:");
1136 Ok(parse_client_list(
1137 &list,
1138 &self.name,
1139 &unnamed,
1140 &named_prefix,
1141 ))
1142 }
1143
1144 pub async fn get_workers_count(&self) -> Result<usize, Error> {
1146 Ok(self.get_workers().await?.len())
1147 }
1148
1149 pub async fn get_meta(&self) -> Result<QueueMeta, Error> {
1156 let mut conn = self.conn.conn();
1157 let config: HashMap<String, String> = redis::cmd("HGETALL")
1158 .arg(self.keys.meta())
1159 .query_async(&mut conn)
1160 .await?;
1161
1162 let mut meta = QueueMeta::default();
1163 for (key, value) in config {
1164 match key.as_str() {
1165 "concurrency" => meta.concurrency = value.parse().ok(),
1166 "max" => meta.max = value.parse().ok(),
1167 "duration" => meta.duration = value.parse().ok(),
1168 "opts.maxLenEvents" => meta.max_len_events = value.parse().ok(),
1169 "paused" => meta.paused = value == "1",
1170 _ => {
1171 meta.other.insert(key, value);
1172 }
1173 }
1174 }
1175
1176 Ok(meta)
1177 }
1178
1179 pub async fn get_version(&self) -> Result<Option<String>, Error> {
1184 let mut conn = self.conn.conn();
1185 let value: Option<String> = redis::cmd("HGET")
1186 .arg(self.keys.meta())
1187 .arg("library")
1188 .query_async(&mut conn)
1189 .await?;
1190 Ok(value)
1191 }
1192
1193 pub async fn is_maxed(&self) -> Result<bool, Error> {
1197 let script = self
1198 .conn
1199 .scripts()
1200 .get("isMaxed")
1201 .ok_or_else(|| Error::InvalidConfig("isMaxed script not found".to_string()))?
1202 .clone();
1203
1204 let keys = vec![self.keys.meta(), self.keys.active()];
1205 let args: Vec<&[u8]> = vec![];
1206
1207 let mut conn = self.conn.conn();
1208 let result = script.execute(&mut conn, &keys, &args).await?;
1209 Ok(matches!(result, redis::Value::Int(1)) || matches!(result, redis::Value::Boolean(true)))
1210 }
1211
1212 pub async fn export_prometheus_metrics(
1221 &self,
1222 global_labels: &[(&str, &str)],
1223 ) -> Result<String, Error> {
1224 let counts = self.get_job_counts().await?;
1225 let mut metrics: Vec<String> = Vec::new();
1226
1227 metrics.push("# HELP bullmq_job_count Number of jobs in the queue by state".to_string());
1228 metrics.push("# TYPE bullmq_job_count gauge".to_string());
1229
1230 let escaped_queue = escape_prometheus_label_value(&self.name);
1231 let variables: String = global_labels
1232 .iter()
1233 .map(|(k, v)| format!(", {}=\"{}\"", k, escape_prometheus_label_value(v)))
1234 .collect();
1235
1236 let states: [(&str, u64); 8] = [
1237 ("active", counts.active),
1238 ("completed", counts.completed),
1239 ("delayed", counts.delayed),
1240 ("failed", counts.failed),
1241 ("paused", counts.paused),
1242 ("prioritized", counts.prioritized),
1243 ("waiting", counts.waiting),
1244 ("waiting-children", counts.waiting_children),
1245 ];
1246 for (state, count) in states {
1247 metrics.push(format!(
1248 "bullmq_job_count{{queue=\"{escaped_queue}\", state=\"{state}\"{variables}}} {count}"
1249 ));
1250 }
1251
1252 let completed_metrics = self.get_metrics("completed", 0, -1).await?;
1253 let failed_metrics = self.get_metrics("failed", 0, -1).await?;
1254
1255 metrics
1256 .push("# HELP bullmq_job_completed_total Total number of completed jobs".to_string());
1257 metrics.push("# TYPE bullmq_job_completed_total counter".to_string());
1258 metrics.push(format!(
1259 "bullmq_job_completed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1260 completed_metrics.meta.count
1261 ));
1262
1263 metrics.push("# HELP bullmq_job_failed_total Total number of failed jobs".to_string());
1264 metrics.push("# TYPE bullmq_job_failed_total counter".to_string());
1265 metrics.push(format!(
1266 "bullmq_job_failed_total{{queue=\"{escaped_queue}\"{variables}}} {}",
1267 failed_metrics.meta.count
1268 ));
1269
1270 Ok(metrics.join("\n"))
1271 }
1272
1273 pub async fn get_metrics(
1279 &self,
1280 metric_type: &str,
1281 start: i64,
1282 end: i64,
1283 ) -> Result<crate::types::Metrics, Error> {
1284 if metric_type != "completed" && metric_type != "failed" {
1289 return Err(Error::InvalidConfig(format!(
1290 "metric type must be \"completed\" or \"failed\", got \"{}\"",
1291 metric_type
1292 )));
1293 }
1294
1295 let script = self
1296 .conn
1297 .scripts()
1298 .get("getMetrics")
1299 .ok_or_else(|| Error::InvalidConfig("getMetrics script not found".to_string()))?
1300 .clone();
1301
1302 let metrics_key = self.keys.get(&format!("metrics:{}", metric_type));
1303 let data_key = self.keys.get(&format!("metrics:{}:data", metric_type));
1304 let keys = vec![metrics_key, data_key];
1305
1306 let start_s = start.to_string();
1307 let end_s = end.to_string();
1308 let args: Vec<&[u8]> = vec![start_s.as_bytes(), end_s.as_bytes()];
1309
1310 let mut conn = self.conn.conn();
1311 let result = script.execute(&mut conn, &keys, &args).await?;
1312
1313 let parse_u64 = |v: &redis::Value| -> u64 {
1315 match v {
1316 redis::Value::BulkString(b) => String::from_utf8_lossy(b).parse().unwrap_or(0),
1317 redis::Value::SimpleString(s) => s.parse().unwrap_or(0),
1318 redis::Value::Int(n) => (*n).max(0) as u64,
1319 _ => 0,
1320 }
1321 };
1322
1323 let mut metrics = crate::types::Metrics::default();
1324 if let redis::Value::Array(parts) = result {
1325 if let Some(redis::Value::Array(meta)) = parts.first() {
1326 metrics.meta.count = meta.first().map(parse_u64).unwrap_or(0);
1327 metrics.meta.prev_ts = meta.get(1).map(parse_u64).unwrap_or(0);
1328 metrics.meta.prev_count = meta.get(2).map(parse_u64).unwrap_or(0);
1329 }
1330 if let Some(redis::Value::Array(data)) = parts.get(1) {
1331 metrics.data = data.iter().map(parse_u64).collect();
1332 }
1333 if let Some(count) = parts.get(2) {
1334 metrics.count = parse_u64(count);
1335 }
1336 }
1337
1338 Ok(metrics)
1339 }
1340
1341 pub async fn get_children_values(
1343 &self,
1344 job_id: &str,
1345 ) -> Result<HashMap<String, serde_json::Value>, Error> {
1346 let processed_key = format!("{}:processed", self.keys.job_key(job_id));
1347 let mut conn = self.conn.conn();
1348
1349 let result: HashMap<String, String> = redis::cmd("HGETALL")
1350 .arg(&processed_key)
1351 .query_async(&mut conn)
1352 .await?;
1353
1354 let mut parsed = HashMap::new();
1355 for (key, value) in result {
1356 let parsed_value: serde_json::Value =
1357 serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
1358 parsed.insert(key, parsed_value);
1359 }
1360 Ok(parsed)
1361 }
1362
1363 pub async fn get_failed_children_values(
1365 &self,
1366 job_id: &str,
1367 ) -> Result<HashMap<String, String>, Error> {
1368 let failed_key = format!("{}:failed", self.keys.job_key(job_id));
1369 let mut conn = self.conn.conn();
1370
1371 let result: HashMap<String, String> = redis::cmd("HGETALL")
1372 .arg(&failed_key)
1373 .query_async(&mut conn)
1374 .await?;
1375
1376 Ok(result)
1377 }
1378
1379 pub async fn get_dependencies_count(&self, job_id: &str) -> Result<DependenciesCount, Error> {
1381 let job_key = self.keys.job_key(job_id);
1382 let processed_key = format!("{}:processed", job_key);
1383 let deps_key = format!("{}:dependencies", job_key);
1384 let failed_key = format!("{}:failed", job_key);
1385 let unsuccessful_key = format!("{}:unsuccessful", job_key);
1386
1387 let mut conn = self.conn.conn();
1388 let mut pipe = redis::pipe();
1389 pipe.cmd("HLEN").arg(&processed_key);
1390 pipe.cmd("SCARD").arg(&deps_key);
1391 pipe.cmd("HLEN").arg(&failed_key);
1392 pipe.cmd("ZCARD").arg(&unsuccessful_key);
1393
1394 let (processed, unprocessed, ignored, failed): (u64, u64, u64, u64) =
1395 pipe.query_async(&mut conn).await?;
1396
1397 Ok(DependenciesCount {
1398 processed,
1399 unprocessed,
1400 ignored,
1401 failed,
1402 })
1403 }
1404
1405 pub async fn get_unprocessed_dependencies(&self, job_id: &str) -> Result<Vec<String>, Error> {
1407 let deps_key = format!("{}:dependencies", self.keys.job_key(job_id));
1408 let mut conn = self.conn.conn();
1409
1410 let result: Vec<String> = redis::cmd("SMEMBERS")
1411 .arg(&deps_key)
1412 .query_async(&mut conn)
1413 .await?;
1414
1415 Ok(result)
1416 }
1417
1418 pub async fn remove_child_dependency(
1425 &self,
1426 job_id: &str,
1427 parent_key: &str,
1428 ) -> Result<bool, Error> {
1429 let script = self
1430 .conn
1431 .scripts()
1432 .get("removeChildDependency")
1433 .ok_or_else(|| {
1434 Error::InvalidConfig("removeChildDependency script not found".to_string())
1435 })?
1436 .clone();
1437
1438 let prefix_key = self.keys.key_prefix().to_string();
1439 let job_key = self.keys.job_key(job_id);
1440
1441 let keys = vec![prefix_key];
1442 let args: Vec<&[u8]> = vec![job_key.as_bytes(), parent_key.as_bytes()];
1443
1444 let mut conn = self.conn.conn();
1445 let result = script.execute(&mut conn, &keys, &args).await?;
1446
1447 match result {
1448 redis::Value::Int(0) => Ok(true),
1449 redis::Value::Int(1) => Ok(false),
1450 redis::Value::Int(-1) => Err(Error::InvalidConfig(format!(
1451 "Missing key for job {}. removeChildDependency",
1452 job_id
1453 ))),
1454 redis::Value::Int(-5) => Err(Error::InvalidConfig(format!(
1455 "Missing key for parent job {}. removeChildDependency",
1456 parent_key
1457 ))),
1458 _ => Ok(false),
1459 }
1460 }
1461
1462 pub async fn get_job_state(&self, job_id: &str) -> Result<JobState, Error> {
1464 let script = self
1465 .conn
1466 .scripts()
1467 .get("getState")
1468 .ok_or_else(|| Error::InvalidConfig("getState script not found".to_string()))?
1469 .clone();
1470
1471 let keys = vec![
1472 self.keys.completed(),
1473 self.keys.failed(),
1474 self.keys.delayed(),
1475 self.keys.active(),
1476 self.keys.wait(),
1477 self.keys.paused(),
1478 self.keys.waiting_children(),
1479 self.keys.prioritized(),
1480 ];
1481
1482 let job_id_bytes = job_id.as_bytes().to_vec();
1483 let args: Vec<&[u8]> = vec![&job_id_bytes];
1484
1485 let mut conn = self.conn.conn();
1486 let result = script.execute(&mut conn, &keys, &args).await?;
1487
1488 match result {
1489 redis::Value::BulkString(bytes) => {
1490 let state_str = String::from_utf8_lossy(&bytes);
1491 Ok(JobState::from_redis_str(&state_str))
1492 }
1493 redis::Value::SimpleString(s) => Ok(JobState::from_redis_str(&s)),
1494 _ => Ok(JobState::Unknown),
1495 }
1496 }
1497
1498 pub async fn remove(&self, job_id: &str) -> Result<bool, Error> {
1500 self.remove_job(job_id, true).await
1501 }
1502
1503 pub async fn remove_without_children(&self, job_id: &str) -> Result<bool, Error> {
1507 self.remove_job(job_id, false).await
1508 }
1509
1510 async fn remove_job(&self, job_id: &str, remove_children: bool) -> Result<bool, Error> {
1511 let script = self
1512 .conn
1513 .scripts()
1514 .get("removeJob")
1515 .ok_or_else(|| Error::InvalidConfig("removeJob script not found".to_string()))?
1516 .clone();
1517
1518 let keys = vec![self.keys.job_key(job_id), self.keys.repeat()];
1519 let prefix = self.keys.key_prefix();
1520 let remove_children_flag = if remove_children { b"1" as &[u8] } else { b"0" };
1521 let args: Vec<&[u8]> = vec![job_id.as_bytes(), remove_children_flag, prefix.as_bytes()];
1522
1523 let mut conn = self.conn.conn();
1524 let result = script.execute(&mut conn, &keys, &args).await?;
1525
1526 match result {
1527 redis::Value::Int(1) => Ok(true),
1531 redis::Value::Int(0) => Ok(false),
1532 redis::Value::Int(code) if code < 0 => {
1533 if code == crate::error::error_code::JOB_BELONGS_TO_JOB_SCHEDULER {
1534 Err(Error::Script {
1535 code,
1536 message: format!(
1537 "Job {} belongs to a job scheduler and cannot be removed directly. removeJob",
1538 job_id
1539 ),
1540 })
1541 } else {
1542 Err(Error::from_script_code(code))
1543 }
1544 }
1545 _ => Ok(false),
1546 }
1547 }
1548
1549 pub async fn remove_unprocessed_children(&self, job_id: &str) -> Result<(), Error> {
1554 let script = self
1555 .conn
1556 .scripts()
1557 .get("removeUnprocessedChildren")
1558 .ok_or_else(|| {
1559 Error::InvalidConfig("removeUnprocessedChildren script not found".to_string())
1560 })?
1561 .clone();
1562
1563 let keys = vec![self.keys.job_key(job_id), self.keys.meta()];
1564 let prefix = self.keys.key_prefix();
1565 let args: Vec<&[u8]> = vec![prefix.as_bytes(), job_id.as_bytes()];
1566
1567 let mut conn = self.conn.conn();
1568 script.execute(&mut conn, &keys, &args).await?;
1569 Ok(())
1570 }
1571
1572 pub async fn clean(&self, grace: u64, limit: u32, state: &str) -> Result<Vec<String>, Error> {
1580 let script = self
1581 .conn
1582 .scripts()
1583 .get("cleanJobsInSet")
1584 .ok_or_else(|| Error::InvalidConfig("cleanJobsInSet script not found".to_string()))?
1585 .clone();
1586
1587 let now = std::time::SystemTime::now()
1588 .duration_since(std::time::UNIX_EPOCH)
1589 .unwrap()
1590 .as_millis() as u64;
1591 let timestamp = now.saturating_sub(grace);
1592
1593 let normalized = if state == "waiting" { "wait" } else { state };
1595
1596 let set_key = match normalized {
1597 "completed" => self.keys.completed(),
1598 "failed" => self.keys.failed(),
1599 "wait" => self.keys.wait(),
1600 "active" => self.keys.active(),
1601 "delayed" => self.keys.delayed(),
1602 "paused" => self.keys.paused(),
1603 "prioritized" => self.keys.prioritized(),
1604 _ => return Err(Error::InvalidConfig(format!("invalid state: {}", state))),
1605 };
1606
1607 let max_per_call = if limit == 0 {
1608 10000u32
1609 } else {
1610 limit.min(10000)
1611 };
1612 let max_total = if limit == 0 { u32::MAX } else { limit };
1613 let mut all_deleted: Vec<String> = Vec::new();
1614
1615 loop {
1616 let keys = vec![set_key.clone(), self.keys.events(), self.keys.repeat()];
1617
1618 let prefix = self.keys.key_prefix();
1619 let ts_str = timestamp.to_string();
1620 let limit_str = max_per_call.to_string();
1621
1622 let args: Vec<&[u8]> = vec![
1623 prefix.as_bytes(),
1624 ts_str.as_bytes(),
1625 limit_str.as_bytes(),
1626 normalized.as_bytes(),
1627 ];
1628
1629 let mut conn = self.conn.conn();
1630 let result = script.execute(&mut conn, &keys, &args).await?;
1631
1632 let batch: Vec<String> = match result {
1633 redis::Value::Array(arr) => arr
1634 .into_iter()
1635 .filter_map(|v| match v {
1636 redis::Value::BulkString(bytes) => {
1637 Some(String::from_utf8_lossy(&bytes).to_string())
1638 }
1639 redis::Value::SimpleString(s) => Some(s),
1640 _ => None,
1641 })
1642 .collect(),
1643 _ => Vec::new(),
1644 };
1645
1646 let batch_len = batch.len() as u32;
1647 all_deleted.extend(batch);
1648
1649 if batch_len < max_per_call || all_deleted.len() as u32 >= max_total {
1650 break;
1651 }
1652 }
1653
1654 Ok(all_deleted)
1655 }
1656
1657 pub async fn drain(&self, delayed: bool) -> Result<(), Error> {
1659 let script = self
1660 .conn
1661 .scripts()
1662 .get("drain")
1663 .ok_or_else(|| Error::InvalidConfig("drain script not found".to_string()))?
1664 .clone();
1665
1666 let keys = vec![
1667 self.keys.wait(),
1668 self.keys.paused(),
1669 self.keys.delayed(),
1670 self.keys.prioritized(),
1671 self.keys.repeat(),
1672 ];
1673
1674 let delayed_str = if delayed { "1" } else { "0" };
1675 let prefix = self.keys.key_prefix();
1676 let args: Vec<&[u8]> = vec![prefix.as_bytes(), delayed_str.as_bytes()];
1677
1678 let mut conn = self.conn.conn();
1679 script.execute(&mut conn, &keys, &args).await?;
1680
1681 debug!(delayed, "queue drained");
1682 Ok(())
1683 }
1684
1685 pub async fn retry_jobs(
1691 &self,
1692 state: &str,
1693 count: u32,
1694 timestamp: Option<u64>,
1695 ) -> Result<(), Error> {
1696 let script = self
1697 .conn
1698 .scripts()
1699 .get("moveJobsToWait")
1700 .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1701 .clone();
1702
1703 let ts = timestamp.unwrap_or_else(|| {
1704 std::time::SystemTime::now()
1705 .duration_since(std::time::UNIX_EPOCH)
1706 .unwrap()
1707 .as_millis() as u64
1708 });
1709
1710 let keys = vec![
1711 self.keys.key_prefix(),
1712 self.keys.events(),
1713 self.keys.get(state),
1714 self.keys.wait(),
1715 self.keys.paused(),
1716 self.keys.meta(),
1717 self.keys.active(),
1718 self.keys.marker(),
1719 ];
1720
1721 let count_str = count.to_string();
1722 let ts_str = ts.to_string();
1723
1724 let mut conn = self.conn.conn();
1725 loop {
1726 let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), state.as_bytes()];
1727 let result = script.execute(&mut conn, &keys, &args).await?;
1728
1729 match result {
1730 redis::Value::Int(1) => continue,
1731 _ => break,
1732 }
1733 }
1734
1735 debug!(state, "retry_jobs completed");
1736 Ok(())
1737 }
1738
1739 pub async fn promote_jobs(&self, count: u32) -> Result<(), Error> {
1743 let script = self
1744 .conn
1745 .scripts()
1746 .get("moveJobsToWait")
1747 .ok_or_else(|| Error::InvalidConfig("moveJobsToWait script not found".to_string()))?
1748 .clone();
1749
1750 let keys = vec![
1751 self.keys.key_prefix(),
1752 self.keys.events(),
1753 self.keys.delayed(),
1754 self.keys.wait(),
1755 self.keys.paused(),
1756 self.keys.meta(),
1757 self.keys.active(),
1758 self.keys.marker(),
1759 ];
1760
1761 let count_str = count.to_string();
1762 let ts_str = "9007199254740991".to_string(); let mut conn = self.conn.conn();
1766 loop {
1767 let args: Vec<&[u8]> = vec![count_str.as_bytes(), ts_str.as_bytes(), b"delayed"];
1768 let result = script.execute(&mut conn, &keys, &args).await?;
1769
1770 match result {
1771 redis::Value::Int(1) => continue,
1772 _ => break,
1773 }
1774 }
1775
1776 debug!("promote_jobs completed");
1777 Ok(())
1778 }
1779
1780 pub async fn rate_limit(&self, expire_time_ms: u64) -> Result<(), Error> {
1785 let limiter_key = self.keys.limiter();
1786 let mut conn = self.conn.conn();
1787
1788 redis::cmd("SET")
1789 .arg(&limiter_key)
1790 .arg("9007199254740991") .arg("PX")
1792 .arg(expire_time_ms)
1793 .query_async::<()>(&mut conn)
1794 .await?;
1795
1796 Ok(())
1797 }
1798
1799 pub async fn remove_rate_limit_key(&self) -> Result<bool, Error> {
1801 let limiter_key = self.keys.limiter();
1802 let mut conn = self.conn.conn();
1803
1804 let result: u32 = redis::cmd("DEL")
1805 .arg(&limiter_key)
1806 .query_async(&mut conn)
1807 .await?;
1808
1809 Ok(result > 0)
1810 }
1811
1812 pub async fn set_global_concurrency(&self, concurrency: u64) -> Result<(), Error> {
1815 let meta_key = self.keys.meta();
1816 let mut conn = self.conn.conn();
1817
1818 redis::cmd("HSET")
1819 .arg(&meta_key)
1820 .arg("concurrency")
1821 .arg(concurrency)
1822 .query_async::<()>(&mut conn)
1823 .await?;
1824
1825 Ok(())
1826 }
1827
1828 pub async fn remove_global_concurrency(&self) -> Result<(), Error> {
1830 let meta_key = self.keys.meta();
1831 let mut conn = self.conn.conn();
1832
1833 redis::cmd("HDEL")
1834 .arg(&meta_key)
1835 .arg("concurrency")
1836 .query_async::<()>(&mut conn)
1837 .await?;
1838
1839 Ok(())
1840 }
1841
1842 pub async fn set_global_rate_limit(&self, max: u64, duration: u64) -> Result<(), Error> {
1844 let meta_key = self.keys.meta();
1845 let mut conn = self.conn.conn();
1846
1847 redis::cmd("HSET")
1848 .arg(&meta_key)
1849 .arg("max")
1850 .arg(max)
1851 .arg("duration")
1852 .arg(duration)
1853 .query_async::<()>(&mut conn)
1854 .await?;
1855
1856 Ok(())
1857 }
1858
1859 pub async fn remove_global_rate_limit(&self) -> Result<(), Error> {
1861 let meta_key = self.keys.meta();
1862 let mut conn = self.conn.conn();
1863
1864 redis::cmd("HDEL")
1865 .arg(&meta_key)
1866 .arg("max")
1867 .arg("duration")
1868 .query_async::<()>(&mut conn)
1869 .await?;
1870
1871 Ok(())
1872 }
1873
1874 pub async fn get_rate_limit_ttl(&self, max_jobs: Option<u64>) -> Result<i64, Error> {
1881 let script = self
1882 .conn
1883 .scripts()
1884 .get("getRateLimitTtl")
1885 .ok_or_else(|| Error::InvalidConfig("getRateLimitTtl script not found".to_string()))?
1886 .clone();
1887
1888 let keys = vec![self.keys.limiter(), self.keys.meta()];
1889 let max_jobs_str = max_jobs
1890 .map(|m| m.to_string())
1891 .unwrap_or_else(|| "0".to_string());
1892 let args: Vec<&[u8]> = vec![max_jobs_str.as_bytes()];
1893
1894 let mut conn = self.conn.conn();
1895 let result = script.execute(&mut conn, &keys, &args).await?;
1896
1897 match result {
1898 redis::Value::Int(n) => Ok(n),
1899 _ => Ok(0),
1900 }
1901 }
1902
1903 pub async fn get_global_concurrency(&self) -> Result<Option<u64>, Error> {
1905 let meta_key = self.keys.meta();
1906 let mut conn = self.conn.conn();
1907
1908 let value: Option<String> = redis::cmd("HGET")
1909 .arg(&meta_key)
1910 .arg("concurrency")
1911 .query_async(&mut conn)
1912 .await?;
1913
1914 Ok(value.and_then(|v| v.parse::<u64>().ok()))
1915 }
1916
1917 pub async fn get_global_rate_limit(&self) -> Result<Option<(u64, u64)>, Error> {
1919 let meta_key = self.keys.meta();
1920 let mut conn = self.conn.conn();
1921
1922 let values: Vec<Option<String>> = redis::cmd("HMGET")
1923 .arg(&meta_key)
1924 .arg("max")
1925 .arg("duration")
1926 .query_async(&mut conn)
1927 .await?;
1928
1929 let max = values
1930 .first()
1931 .and_then(|v| v.as_ref())
1932 .and_then(|v| v.parse::<u64>().ok());
1933 let duration = values
1934 .get(1)
1935 .and_then(|v| v.as_ref())
1936 .and_then(|v| v.parse::<u64>().ok());
1937
1938 match (max, duration) {
1939 (Some(m), Some(d)) => Ok(Some((m, d))),
1940 _ => Ok(None),
1941 }
1942 }
1943
1944 pub async fn remove_deduplication_key(
1948 &self,
1949 deduplication_id: &str,
1950 job_id: &str,
1951 ) -> Result<bool, Error> {
1952 let script = self
1953 .conn
1954 .scripts()
1955 .get("removeDeduplicationKey")
1956 .ok_or_else(|| {
1957 Error::InvalidConfig("removeDeduplicationKey script not found".to_string())
1958 })?
1959 .clone();
1960
1961 let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
1962 let keys = vec![dedup_key];
1963 let args: Vec<&[u8]> = vec![job_id.as_bytes()];
1964
1965 let mut conn = self.conn.conn();
1966 let result = script.execute(&mut conn, &keys, &args).await?;
1967
1968 match result {
1969 redis::Value::Int(1) => Ok(true),
1970 _ => Ok(false),
1971 }
1972 }
1973
1974 pub async fn get_deduplication_job_id(
1978 &self,
1979 deduplication_id: &str,
1980 ) -> Result<Option<String>, Error> {
1981 let dedup_key = format!("{}:de:{}", self.keys.base(), deduplication_id);
1982 let mut conn = self.conn.conn();
1983 let result: redis::Value = redis::cmd("GET")
1984 .arg(&dedup_key)
1985 .query_async(&mut conn)
1986 .await
1987 .map_err(Error::Redis)?;
1988
1989 match result {
1990 redis::Value::BulkString(bytes) => {
1991 Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
1992 }
1993 redis::Value::SimpleString(s) => Ok(Some(s)),
1994 redis::Value::Nil => Ok(None),
1995 _ => Ok(None),
1996 }
1997 }
1998
1999 pub async fn get_debounce_job_id(&self, id: &str) -> Result<Option<String>, Error> {
2004 self.get_deduplication_job_id(id).await
2005 }
2006
2007 pub async fn remove_debounce_key(&self, id: &str) -> Result<u64, Error> {
2015 let dedup_key = format!("{}:de:{}", self.keys.base(), id);
2016 let mut conn = self.conn.conn();
2017 let deleted: u64 = redis::cmd("DEL")
2018 .arg(&dedup_key)
2019 .query_async(&mut conn)
2020 .await?;
2021 Ok(deleted)
2022 }
2023
2024 pub async fn get_job_logs(
2028 &self,
2029 job_id: &str,
2030 start: isize,
2031 end: isize,
2032 asc: bool,
2033 ) -> Result<(Vec<String>, usize), Error> {
2034 let logs_key = format!("{}{}:logs", self.keys.key_prefix(), job_id);
2035 let mut conn = self.conn.conn();
2036
2037 let (logs, count): (Vec<String>, usize) = if asc {
2038 redis::pipe()
2039 .cmd("LRANGE")
2040 .arg(&logs_key)
2041 .arg(start)
2042 .arg(end)
2043 .cmd("LLEN")
2044 .arg(&logs_key)
2045 .query_async(&mut conn)
2046 .await?
2047 } else {
2048 let actual_start = -(end + 1);
2049 let actual_end = -(start + 1);
2050 let (mut logs, count): (Vec<String>, usize) = redis::pipe()
2051 .cmd("LRANGE")
2052 .arg(&logs_key)
2053 .arg(actual_start)
2054 .arg(actual_end)
2055 .cmd("LLEN")
2056 .arg(&logs_key)
2057 .query_async(&mut conn)
2058 .await?;
2059 logs.reverse();
2060 (logs, count)
2061 };
2062
2063 Ok((logs, count))
2064 }
2065
2066 pub async fn trim_events(&self, max_length: usize) -> Result<usize, Error> {
2068 let mut conn = self.conn.conn();
2069 let trimmed: usize = redis::cmd("XTRIM")
2070 .arg(self.keys.events())
2071 .arg("MAXLEN")
2072 .arg("~")
2073 .arg(max_length)
2074 .query_async(&mut conn)
2075 .await?;
2076 Ok(trimmed)
2077 }
2078
2079 pub async fn update_job_progress(
2085 &self,
2086 job_id: &str,
2087 progress: crate::types::JobProgress,
2088 ) -> Result<(), Error> {
2089 let script = self
2090 .conn
2091 .scripts()
2092 .get("updateProgress")
2093 .ok_or_else(|| Error::InvalidConfig("updateProgress script not found".to_string()))?
2094 .clone();
2095
2096 let job_key = self.keys.job_key(job_id);
2097 let events_key = self.keys.events();
2098 let meta_key = self.keys.meta();
2099 let progress_json = serialize_progress_for_script(&progress)?;
2100
2101 let keys = vec![job_key, events_key, meta_key];
2102 let args: Vec<&[u8]> = vec![job_id.as_bytes(), progress_json.as_bytes()];
2103
2104 let mut conn = self.conn.conn();
2105 let result: redis::Value = script.execute(&mut conn, &keys, &args).await?;
2106
2107 match result {
2108 redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2109 _ => Ok(()),
2110 }
2111 }
2112
2113 pub async fn obliterate(&self, force: bool, count: usize) -> Result<(), Error> {
2117 if force {
2119 let _ = self.pause().await;
2120 }
2121
2122 let script = self
2123 .conn
2124 .scripts()
2125 .get("obliterate")
2126 .ok_or_else(|| Error::InvalidConfig("obliterate script not found".to_string()))?
2127 .clone();
2128
2129 let keys = vec![self.keys.meta(), self.keys.key_prefix()];
2130 let count_str = count.to_string();
2131 let force_str = if force { "1" } else { "0" };
2132
2133 let mut conn = self.conn.conn();
2134 loop {
2135 let args: Vec<&[u8]> = vec![count_str.as_bytes(), force_str.as_bytes()];
2136 let result = script.execute(&mut conn, &keys, &args).await?;
2137
2138 match result {
2139 redis::Value::Nil => break,
2140 redis::Value::Int(0) => break,
2141 redis::Value::Int(1) => continue, redis::Value::Int(code) if code < 0 => {
2143 return Err(Error::from_script_code(code));
2144 }
2145 _ => break,
2146 }
2147 }
2148
2149 debug!(force, "queue obliterated");
2150 Ok(())
2151 }
2152
2153 pub async fn upsert_job_scheduler(
2168 &self,
2169 job_scheduler_id: &str,
2170 repeat_opts: crate::job_scheduler::RepeatOptions,
2171 job_name: Option<&str>,
2172 job_data: Option<serde_json::Value>,
2173 job_opts: Option<JobOptions>,
2174 ) -> Result<Option<Job>, Error> {
2175 use crate::job_scheduler::{next_cron_millis, pack_delayed_job_opts, pack_scheduler_opts};
2176
2177 if repeat_opts.pattern.is_some() && repeat_opts.every.is_some() {
2179 return Err(Error::InvalidConfig(
2180 "Both .pattern and .every options are defined; only one may be used".to_string(),
2181 ));
2182 }
2183 if repeat_opts.pattern.is_none() && repeat_opts.every.is_none() {
2184 return Err(Error::InvalidConfig(
2185 "Either .pattern or .every option must be defined".to_string(),
2186 ));
2187 }
2188 if repeat_opts.immediately == Some(true) && repeat_opts.start_date.is_some() {
2189 return Err(Error::InvalidConfig(
2190 "Both .immediately and .startDate options are defined; only one may be used"
2191 .to_string(),
2192 ));
2193 }
2194
2195 let now = std::time::SystemTime::now()
2196 .duration_since(std::time::UNIX_EPOCH)
2197 .unwrap()
2198 .as_millis() as u64;
2199
2200 if let Some(end_date) = repeat_opts.end_date {
2202 if end_date < now {
2203 return Err(Error::InvalidConfig(
2204 "End date must be greater than current timestamp".to_string(),
2205 ));
2206 }
2207 }
2208
2209 let iteration_count = repeat_opts.count.unwrap_or(0) + 1;
2211 if let Some(limit) = repeat_opts.limit {
2212 if iteration_count > limit {
2213 return Ok(None);
2214 }
2215 }
2216
2217 let next_millis: Option<u64> = if let Some(ref pattern) = repeat_opts.pattern {
2219 if repeat_opts.immediately == Some(true) {
2220 Some(now)
2221 } else {
2222 next_cron_millis(
2223 pattern,
2224 now,
2225 repeat_opts.tz.as_deref(),
2226 repeat_opts.start_date,
2227 )?
2228 }
2229 } else {
2230 None
2232 };
2233
2234 if next_millis.is_none() && repeat_opts.every.is_none() {
2236 return Ok(None);
2237 }
2238
2239 let effective_name = job_name.unwrap_or(job_scheduler_id);
2240 let effective_data = job_data.unwrap_or(serde_json::json!({}));
2241 let effective_opts = job_opts.unwrap_or_default();
2242
2243 let offset = if repeat_opts.every.is_some() {
2244 repeat_opts.offset
2245 } else {
2246 None
2247 };
2248
2249 let scheduler_opts_packed = pack_scheduler_opts(effective_name, &repeat_opts);
2251 let template_data_json =
2252 serde_json::to_string(&effective_data).unwrap_or_else(|_| "{}".to_string());
2253 let template_opts_packed = self.pack_job_opts_from_options(&effective_opts);
2254 let delayed_job_opts_packed = pack_delayed_job_opts(
2255 &effective_opts,
2256 job_scheduler_id,
2257 next_millis.unwrap_or(0),
2258 iteration_count,
2259 offset,
2260 &repeat_opts,
2261 );
2262
2263 let script = self
2264 .conn
2265 .scripts()
2266 .get("addJobScheduler")
2267 .ok_or_else(|| Error::InvalidConfig("addJobScheduler script not found".to_string()))?
2268 .clone();
2269
2270 let keys = vec![
2272 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(), ];
2284
2285 let next_millis_str = next_millis.unwrap_or(0).to_string();
2287 let timestamp_str = now.to_string();
2288 let prefix = self.keys.key_prefix();
2289 let producer_key = Vec::new(); let args: Vec<&[u8]> = vec![
2292 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, ];
2302
2303 let mut conn = self.conn.conn();
2304 let result = script.execute(&mut conn, &keys, &args).await?;
2305
2306 match result {
2308 redis::Value::Array(ref arr) if arr.len() >= 2 => {
2309 let job_id = match &arr[0] {
2310 redis::Value::BulkString(bytes) => String::from_utf8_lossy(bytes).to_string(),
2311 redis::Value::SimpleString(s) => s.clone(),
2312 redis::Value::Int(n) => n.to_string(),
2313 _ => return Ok(None),
2314 };
2315
2316 let mut job = Job::new(effective_name, effective_data, Some(effective_opts));
2317 job.set_id(job_id);
2318 job.set_context(self.make_script_context());
2319 Ok(Some(job))
2320 }
2321 redis::Value::Int(code) if code < 0 => Err(Error::from_script_code(code)),
2322 _ => Ok(None),
2323 }
2324 }
2325
2326 pub async fn get_job_scheduler(
2328 &self,
2329 job_scheduler_id: &str,
2330 ) -> Result<Option<crate::job_scheduler::JobSchedulerJson>, Error> {
2331 let script = self
2332 .conn
2333 .scripts()
2334 .get("getJobScheduler")
2335 .ok_or_else(|| Error::InvalidConfig("getJobScheduler script not found".to_string()))?
2336 .clone();
2337
2338 let keys = vec![self.keys.repeat()];
2339 let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes()];
2340
2341 let mut conn = self.conn.conn();
2342 let result = script.execute(&mut conn, &keys, &args).await?;
2343
2344 match result {
2346 redis::Value::Array(ref arr) if arr.len() >= 2 => {
2347 let fields = Self::parse_hash_array(&arr[0]);
2348 if fields.is_empty() {
2349 return Ok(None);
2350 }
2351 let next_millis = match &arr[1] {
2352 redis::Value::BulkString(bytes) => {
2353 String::from_utf8_lossy(bytes).parse::<u64>().ok()
2354 }
2355 redis::Value::SimpleString(s) => s.parse::<u64>().ok(),
2356 redis::Value::Int(n) => Some(*n as u64),
2357 _ => None,
2358 };
2359 Ok(Some(crate::job_scheduler::parse_scheduler_hash(
2360 job_scheduler_id,
2361 &fields,
2362 next_millis,
2363 )))
2364 }
2365 redis::Value::Nil => Ok(None),
2366 redis::Value::Array(ref arr) if arr.is_empty() => Ok(None),
2367 _ => Ok(None),
2368 }
2369 }
2370
2371 pub async fn get_job_schedulers(
2375 &self,
2376 start: isize,
2377 end: isize,
2378 asc: bool,
2379 ) -> Result<Vec<crate::job_scheduler::JobSchedulerJson>, Error> {
2380 let mut conn = self.conn.conn();
2381 let repeat_key = self.keys.repeat();
2382
2383 let results: Vec<(String, f64)> = if asc {
2385 redis::cmd("ZRANGE")
2386 .arg(&repeat_key)
2387 .arg(start)
2388 .arg(end)
2389 .arg("WITHSCORES")
2390 .query_async(&mut conn)
2391 .await?
2392 } else {
2393 redis::cmd("ZREVRANGE")
2394 .arg(&repeat_key)
2395 .arg(start)
2396 .arg(end)
2397 .arg("WITHSCORES")
2398 .query_async(&mut conn)
2399 .await?
2400 };
2401
2402 let mut schedulers = Vec::with_capacity(results.len());
2403 for (scheduler_id, score) in &results {
2404 let scheduler_hash_key = format!("{}repeat:{}", self.keys.key_prefix(), scheduler_id);
2405 let fields: std::collections::HashMap<String, String> = redis::cmd("HGETALL")
2406 .arg(&scheduler_hash_key)
2407 .query_async(&mut conn)
2408 .await?;
2409
2410 if !fields.is_empty() {
2411 schedulers.push(crate::job_scheduler::parse_scheduler_hash(
2412 scheduler_id,
2413 &fields,
2414 Some(*score as u64),
2415 ));
2416 }
2417 }
2418
2419 Ok(schedulers)
2420 }
2421
2422 pub async fn get_job_schedulers_count(&self) -> Result<u64, Error> {
2424 let mut conn = self.conn.conn();
2425 let count: u64 = redis::cmd("ZCARD")
2426 .arg(self.keys.repeat())
2427 .query_async(&mut conn)
2428 .await?;
2429 Ok(count)
2430 }
2431
2432 pub async fn remove_job_scheduler(&self, job_scheduler_id: &str) -> Result<bool, Error> {
2437 let script = self
2438 .conn
2439 .scripts()
2440 .get("removeJobScheduler")
2441 .ok_or_else(|| Error::InvalidConfig("removeJobScheduler script not found".to_string()))?
2442 .clone();
2443
2444 let keys = vec![self.keys.repeat(), self.keys.delayed(), self.keys.events()];
2445
2446 let prefix = self.keys.key_prefix();
2447 let args: Vec<&[u8]> = vec![job_scheduler_id.as_bytes(), prefix.as_bytes()];
2448
2449 let mut conn = self.conn.conn();
2450 let result = script.execute(&mut conn, &keys, &args).await?;
2451
2452 match result {
2453 redis::Value::Int(0) => Ok(true), redis::Value::Int(1) => Ok(false), _ => Ok(false),
2456 }
2457 }
2458
2459 fn parse_hash_array(value: &redis::Value) -> std::collections::HashMap<String, String> {
2461 let mut map = std::collections::HashMap::new();
2462 if let redis::Value::Array(arr) = value {
2463 let mut iter = arr.iter();
2464 while let (Some(key_val), Some(val_val)) = (iter.next(), iter.next()) {
2465 let key = match key_val {
2466 redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2467 redis::Value::SimpleString(s) => s.clone(),
2468 _ => continue,
2469 };
2470 let val = match val_val {
2471 redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),
2472 redis::Value::SimpleString(s) => s.clone(),
2473 redis::Value::Int(n) => n.to_string(),
2474 _ => String::new(),
2475 };
2476 map.insert(key, val);
2477 }
2478 }
2479 map
2480 }
2481
2482 fn pack_job_opts_from_options(&self, opts: &JobOptions) -> Vec<u8> {
2484 use rmp::encode::*;
2485
2486 let mut entries: Vec<(&str, Vec<u8>)> = Vec::new();
2487
2488 if let Some(attempts) = opts.attempts {
2489 let mut b = Vec::new();
2490 write_uint(&mut b, attempts as u64).unwrap();
2491 entries.push(("attempts", b));
2492 }
2493 if let Some(ref backoff) = opts.backoff {
2494 let b = Self::encode_backoff(backoff);
2495 entries.push(("backoff", b));
2496 }
2497 if let Some(ref roc) = opts.remove_on_complete {
2498 let b = Self::encode_remove_on_finish(roc);
2499 entries.push(("removeOnComplete", b));
2500 }
2501 if let Some(ref rof) = opts.remove_on_fail {
2502 let b = Self::encode_remove_on_finish(rof);
2503 entries.push(("removeOnFail", b));
2504 }
2505 if let Some(priority) = opts.priority {
2506 if priority > 0 {
2507 let mut b = Vec::new();
2508 write_uint(&mut b, priority as u64).unwrap();
2509 entries.push(("priority", b));
2510 }
2511 }
2512
2513 let mut buf = Vec::with_capacity(64);
2514 write_map_len(&mut buf, entries.len() as u32).unwrap();
2515 for (key, val) in &entries {
2516 write_str(&mut buf, key).unwrap();
2517 buf.extend_from_slice(val);
2518 }
2519 buf
2520 }
2521
2522 pub async fn close(&self) {
2524 self.conn.close().await;
2525 }
2526}
2527
2528fn escape_prometheus_label_value(value: &str) -> String {
2532 value
2533 .replace('\\', "\\\\")
2534 .replace('"', "\\\"")
2535 .replace('\n', "\\n")
2536}
2537
2538fn parse_client_list(
2546 list: &str,
2547 queue_name: &str,
2548 unnamed: &str,
2549 named_prefix: &str,
2550) -> Vec<HashMap<String, String>> {
2551 let mut clients = Vec::new();
2552
2553 for line in list.split(['\r', '\n']).filter(|l| !l.is_empty()) {
2554 let mut client: HashMap<String, String> = HashMap::new();
2555 for key_value in line.split(' ') {
2556 if let Some(idx) = key_value.find('=') {
2557 let key = &key_value[..idx];
2558 let value = &key_value[idx + 1..];
2559 client.insert(key.to_string(), value.to_string());
2560 }
2561 }
2562
2563 let name = client.get("name").cloned().unwrap_or_default();
2564 if !name.is_empty() && (name == unnamed || name.starts_with(named_prefix)) {
2565 client.insert("name".to_string(), queue_name.to_string());
2566 client.insert("rawname".to_string(), name);
2567 clients.push(client);
2568 }
2569 }
2570
2571 clients
2572}
2573
2574fn serialize_progress_for_script(progress: &crate::types::JobProgress) -> Result<String, Error> {
2575 match progress {
2576 crate::types::JobProgress::Number(n) if !n.is_finite() => Ok("null".to_string()),
2578 _ => Ok(serde_json::to_string(progress)?),
2579 }
2580}
2581
2582#[cfg(test)]
2583mod client_list_tests {
2584 use super::parse_client_list;
2585
2586 #[test]
2587 fn matches_unnamed_and_named_workers() {
2588 let unnamed = "bull:dGVzdA==";
2589 let named_prefix = "bull:dGVzdA==:w:";
2590 let list = format!(
2591 "id=1 addr=127.0.0.1:1 name={unnamed} age=5\n\
2592 id=2 addr=127.0.0.1:2 name={named_prefix}alpha age=3\n\
2593 id=3 addr=127.0.0.1:3 name=bull:b3RoZXI= age=1\n\
2594 id=4 addr=127.0.0.1:4 name= age=1\n"
2595 );
2596
2597 let clients = parse_client_list(&list, "test", unnamed, named_prefix);
2598 assert_eq!(clients.len(), 2);
2599 assert_eq!(clients[0].get("name"), Some(&"test".to_string()));
2600 assert_eq!(clients[0].get("rawname"), Some(&unnamed.to_string()));
2601 assert_eq!(
2602 clients[1].get("rawname"),
2603 Some(&format!("{named_prefix}alpha"))
2604 );
2605 assert_eq!(clients[1].get("name"), Some(&"test".to_string()));
2606 }
2607
2608 #[test]
2609 fn returns_empty_when_no_match() {
2610 let list = "id=1 addr=127.0.0.1:1 name=other age=5\n";
2611 let clients = parse_client_list(list, "test", "bull:dGVzdA==", "bull:dGVzdA==:w:");
2612 assert!(clients.is_empty());
2613 }
2614}
2615
2616#[cfg(test)]
2617mod prometheus_tests {
2618 use super::escape_prometheus_label_value;
2619
2620 #[test]
2621 fn escapes_special_characters() {
2622 assert_eq!(escape_prometheus_label_value("plain"), "plain");
2623 assert_eq!(escape_prometheus_label_value("a\\b"), "a\\\\b");
2624 assert_eq!(escape_prometheus_label_value("a\"b"), "a\\\"b");
2625 assert_eq!(escape_prometheus_label_value("a\nb"), "a\\nb");
2626 }
2627}
2628
2629#[cfg(test)]
2630mod progress_serialization_tests {
2631 use super::serialize_progress_for_script;
2632 use crate::types::JobProgress;
2633
2634 #[test]
2635 fn serializes_non_finite_numbers_as_null() {
2636 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2637 let serialized = serialize_progress_for_script(&JobProgress::Number(value)).unwrap();
2638 assert_eq!(serialized, "null");
2639 }
2640 }
2641
2642 #[test]
2643 fn serializes_regular_values_as_json() {
2644 let serialized = serialize_progress_for_script(&JobProgress::Number(42.0)).unwrap();
2645 assert_eq!(serialized, "42.0");
2646 }
2647}