1use std::collections::HashMap;
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::Arc;
33use std::time::Duration;
34
35use redis::streams::{StreamReadOptions, StreamReadReply};
36use redis::AsyncCommands;
37use tokio::sync::{mpsc, Mutex};
38use tokio::task::JoinHandle;
39use tracing::debug;
40
41use crate::error::Error;
42use crate::keys::{validate_queue_name, QueueKeys};
43use crate::options::{QueueOptions, RedisConnectionOptions};
44use crate::redis_connection::RedisConnection;
45
46#[derive(Clone)]
48pub struct QueueEventsOptions {
49 pub connection: RedisConnectionOptions,
51 pub prefix: String,
53 pub blocking_timeout: u64,
58 pub last_event_id: Option<String>,
63 pub autorun: bool,
65}
66
67impl Default for QueueEventsOptions {
68 fn default() -> Self {
69 Self {
70 connection: RedisConnectionOptions::default(),
71 prefix: "bull".to_string(),
72 blocking_timeout: 5000,
73 last_event_id: None,
74 autorun: true,
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct QueueEventEntry {
82 pub id: String,
86 pub event: QueueEvent,
88}
89
90#[derive(Debug, Clone, PartialEq)]
94pub enum QueueEvent {
95 Active {
97 job_id: String,
99 prev: Option<String>,
101 },
102 Added {
104 job_id: String,
106 name: String,
108 },
109 Cleaned {
111 count: String,
113 },
114 Completed {
116 job_id: String,
118 return_value: String,
120 prev: Option<String>,
122 },
123 Debounced {
125 job_id: String,
127 debounce_id: String,
129 },
130 Deduplicated {
132 job_id: String,
134 deduplication_id: String,
136 deduplicated_job_id: Option<String>,
138 },
139 Delayed {
141 job_id: String,
143 delay: i64,
145 },
146 Drained,
148 Duplicated {
150 job_id: String,
152 },
153 Error {
155 message: String,
157 },
158 Failed {
160 job_id: String,
162 failed_reason: String,
164 prev: Option<String>,
166 },
167 Paused,
169 Progress {
171 job_id: String,
173 data: serde_json::Value,
175 },
176 Removed {
178 job_id: String,
180 prev: Option<String>,
182 },
183 Resumed,
185 RetriesExhausted {
187 job_id: String,
189 attempts_made: String,
191 },
192 Stalled {
194 job_id: String,
196 },
197 Waiting {
199 job_id: String,
201 prev: Option<String>,
203 },
204 WaitingChildren {
206 job_id: String,
208 },
209 Other {
211 event: String,
213 fields: HashMap<String, String>,
215 },
216}
217
218impl QueueEvent {
219 pub fn job_id(&self) -> Option<&str> {
221 match self {
222 QueueEvent::Active { job_id, .. }
223 | QueueEvent::Added { job_id, .. }
224 | QueueEvent::Completed { job_id, .. }
225 | QueueEvent::Debounced { job_id, .. }
226 | QueueEvent::Deduplicated { job_id, .. }
227 | QueueEvent::Delayed { job_id, .. }
228 | QueueEvent::Duplicated { job_id }
229 | QueueEvent::Failed { job_id, .. }
230 | QueueEvent::Progress { job_id, .. }
231 | QueueEvent::Removed { job_id, .. }
232 | QueueEvent::RetriesExhausted { job_id, .. }
233 | QueueEvent::Stalled { job_id }
234 | QueueEvent::Waiting { job_id, .. }
235 | QueueEvent::WaitingChildren { job_id } => Some(job_id),
236 _ => None,
237 }
238 }
239
240 pub fn name(&self) -> &str {
242 match self {
243 QueueEvent::Active { .. } => "active",
244 QueueEvent::Added { .. } => "added",
245 QueueEvent::Cleaned { .. } => "cleaned",
246 QueueEvent::Completed { .. } => "completed",
247 QueueEvent::Debounced { .. } => "debounced",
248 QueueEvent::Deduplicated { .. } => "deduplicated",
249 QueueEvent::Delayed { .. } => "delayed",
250 QueueEvent::Drained => "drained",
251 QueueEvent::Duplicated { .. } => "duplicated",
252 QueueEvent::Error { .. } => "error",
253 QueueEvent::Failed { .. } => "failed",
254 QueueEvent::Paused => "paused",
255 QueueEvent::Progress { .. } => "progress",
256 QueueEvent::Removed { .. } => "removed",
257 QueueEvent::Resumed => "resumed",
258 QueueEvent::RetriesExhausted { .. } => "retries-exhausted",
259 QueueEvent::Stalled { .. } => "stalled",
260 QueueEvent::Waiting { .. } => "waiting",
261 QueueEvent::WaitingChildren { .. } => "waiting-children",
262 QueueEvent::Other { event, .. } => event,
263 }
264 }
265}
266
267fn parse_event(mut fields: HashMap<String, String>) -> QueueEvent {
269 let event = fields.remove("event").unwrap_or_default();
270 let job_id = |f: &mut HashMap<String, String>| f.remove("jobId").unwrap_or_default();
271
272 match event.as_str() {
273 "active" => QueueEvent::Active {
274 job_id: job_id(&mut fields),
275 prev: fields.remove("prev"),
276 },
277 "added" => QueueEvent::Added {
278 job_id: job_id(&mut fields),
279 name: fields.remove("name").unwrap_or_default(),
280 },
281 "cleaned" => QueueEvent::Cleaned {
282 count: fields.remove("count").unwrap_or_default(),
283 },
284 "completed" => QueueEvent::Completed {
285 job_id: job_id(&mut fields),
286 return_value: fields.remove("returnvalue").unwrap_or_default(),
287 prev: fields.remove("prev"),
288 },
289 "debounced" => QueueEvent::Debounced {
290 job_id: job_id(&mut fields),
291 debounce_id: fields.remove("debounceId").unwrap_or_default(),
292 },
293 "deduplicated" => QueueEvent::Deduplicated {
294 job_id: job_id(&mut fields),
295 deduplication_id: fields.remove("deduplicationId").unwrap_or_default(),
296 deduplicated_job_id: fields.remove("deduplicatedJobId"),
297 },
298 "delayed" => QueueEvent::Delayed {
299 job_id: job_id(&mut fields),
300 delay: fields
301 .remove("delay")
302 .and_then(|d| d.parse().ok())
303 .unwrap_or(0),
304 },
305 "drained" => QueueEvent::Drained,
306 "duplicated" => QueueEvent::Duplicated {
307 job_id: job_id(&mut fields),
308 },
309 "error" => QueueEvent::Error {
310 message: fields.remove("message").unwrap_or_default(),
311 },
312 "failed" => QueueEvent::Failed {
313 job_id: job_id(&mut fields),
314 failed_reason: fields.remove("failedReason").unwrap_or_default(),
315 prev: fields.remove("prev"),
316 },
317 "paused" => QueueEvent::Paused,
318 "progress" => {
319 let raw = fields.remove("data").unwrap_or_default();
320 let data = serde_json::from_str(&raw).unwrap_or(serde_json::Value::String(raw));
321 QueueEvent::Progress {
322 job_id: job_id(&mut fields),
323 data,
324 }
325 }
326 "removed" => QueueEvent::Removed {
327 job_id: job_id(&mut fields),
328 prev: fields.remove("prev"),
329 },
330 "resumed" => QueueEvent::Resumed,
331 "retries-exhausted" => QueueEvent::RetriesExhausted {
332 job_id: job_id(&mut fields),
333 attempts_made: fields.remove("attemptsMade").unwrap_or_default(),
334 },
335 "stalled" => QueueEvent::Stalled {
336 job_id: job_id(&mut fields),
337 },
338 "waiting" => QueueEvent::Waiting {
339 job_id: job_id(&mut fields),
340 prev: fields.remove("prev"),
341 },
342 "waiting-children" => QueueEvent::WaitingChildren {
343 job_id: job_id(&mut fields),
344 },
345 _ => QueueEvent::Other { event, fields },
346 }
347}
348
349fn map_to_strings(map: &HashMap<String, redis::Value>) -> HashMap<String, String> {
351 map.iter()
352 .filter_map(|(k, v)| {
353 redis::from_redis_value::<String>(v)
354 .ok()
355 .map(|s| (k.clone(), s))
356 })
357 .collect()
358}
359
360pub struct QueueEvents {
364 name: String,
365 keys: QueueKeys,
366 conn: RedisConnection,
367 opts: QueueEventsOptions,
368 closing: Arc<AtomicBool>,
369 running: Arc<AtomicBool>,
370 event_tx: Arc<Mutex<Option<mpsc::UnboundedSender<QueueEventEntry>>>>,
371 event_rx: Arc<Mutex<mpsc::UnboundedReceiver<QueueEventEntry>>>,
372 task: Arc<Mutex<Option<JoinHandle<()>>>>,
373}
374
375impl QueueEvents {
376 pub async fn new(name: &str, opts: QueueEventsOptions) -> Result<Self, Error> {
378 validate_queue_name(name)?;
379 let conn = RedisConnection::new(&opts.connection).await?;
380 Self::build(name, conn, opts).await
381 }
382
383 pub async fn with_connection(
388 name: &str,
389 conn: RedisConnection,
390 opts: QueueEventsOptions,
391 ) -> Result<Self, Error> {
392 validate_queue_name(name)?;
393 Self::build(name, conn, opts).await
394 }
395
396 pub async fn from_queue_options(name: &str, opts: &QueueOptions) -> Result<Self, Error> {
398 Self::new(
399 name,
400 QueueEventsOptions {
401 connection: opts.connection.clone(),
402 prefix: opts.prefix.clone(),
403 ..Default::default()
404 },
405 )
406 .await
407 }
408
409 async fn build(
410 name: &str,
411 conn: RedisConnection,
412 opts: QueueEventsOptions,
413 ) -> Result<Self, Error> {
414 let keys = QueueKeys::new(name, Some(&opts.prefix));
415 let (event_tx, event_rx) = mpsc::unbounded_channel();
416
417 let events = Self {
418 name: name.to_string(),
419 keys,
420 conn,
421 opts,
422 closing: Arc::new(AtomicBool::new(false)),
423 running: Arc::new(AtomicBool::new(false)),
424 event_tx: Arc::new(Mutex::new(Some(event_tx))),
425 event_rx: Arc::new(Mutex::new(event_rx)),
426 task: Arc::new(Mutex::new(None)),
427 };
428
429 if events.opts.autorun {
430 events.run().await?;
431 }
432
433 Ok(events)
434 }
435
436 pub fn name(&self) -> &str {
438 &self.name
439 }
440
441 pub fn keys(&self) -> &QueueKeys {
443 &self.keys
444 }
445
446 pub fn is_running(&self) -> bool {
448 self.running.load(Ordering::Relaxed)
449 }
450
451 pub async fn run(&self) -> Result<(), Error> {
456 if self.running.swap(true, Ordering::SeqCst) {
457 return Err(Error::InvalidConfig(
458 "QueueEvents is already running".to_string(),
459 ));
460 }
461
462 let conn = self.conn.clone();
463 let key = self.keys.events();
464 let opts = self.opts.clone();
465 let closing = self.closing.clone();
466 let tx = match self.event_tx.lock().await.as_ref().cloned() {
467 Some(tx) => tx,
468 None => {
469 self.running.store(false, Ordering::SeqCst);
470 return Err(Error::InvalidConfig(
471 "Cannot run QueueEvents after close()".to_string(),
472 ));
473 }
474 };
475
476 let handle = tokio::spawn(async move {
477 Self::consume(conn, key, opts, closing, tx).await;
478 });
479
480 *self.task.lock().await = Some(handle);
481 Ok(())
482 }
483
484 async fn consume(
485 conn: RedisConnection,
486 key: String,
487 opts: QueueEventsOptions,
488 closing: Arc<AtomicBool>,
489 tx: mpsc::UnboundedSender<QueueEventEntry>,
490 ) {
491 let mut redis_conn = match conn.dedicated_connection().await {
492 Ok(c) => c,
493 Err(e) => {
494 let _ = tx.send(QueueEventEntry {
495 id: String::new(),
496 event: QueueEvent::Error {
497 message: e.to_string(),
498 },
499 });
500 return;
501 }
502 };
503
504 let mut id = opts
505 .last_event_id
506 .clone()
507 .unwrap_or_else(|| "$".to_string());
508 let read_opts = StreamReadOptions::default().block(opts.blocking_timeout as usize);
509
510 while !closing.load(Ordering::Relaxed) {
511 let reply: Result<Option<StreamReadReply>, _> =
512 redis_conn.xread_options(&[&key], &[&id], &read_opts).await;
513
514 match reply {
515 Ok(Some(reply)) => {
516 for stream_key in reply.keys {
517 for entry in stream_key.ids {
518 id = entry.id.clone();
519 let fields = map_to_strings(&entry.map);
520 let event = parse_event(fields);
521 if tx
522 .send(QueueEventEntry {
523 id: entry.id,
524 event,
525 })
526 .is_err()
527 {
528 return;
530 }
531 }
532 }
533 }
534 Ok(None) => {
535 }
537 Err(e) => {
538 if closing.load(Ordering::Relaxed) {
539 break;
540 }
541 let _ = tx.send(QueueEventEntry {
542 id: String::new(),
543 event: QueueEvent::Error {
544 message: e.to_string(),
545 },
546 });
547 tokio::time::sleep(Duration::from_millis(100)).await;
548 }
549 }
550 }
551
552 debug!(queue = %key, "queue events loop stopped");
553 }
554
555 pub async fn next_event(&self) -> Option<QueueEventEntry> {
558 let mut rx = self.event_rx.lock().await;
559 rx.recv().await
560 }
561
562 pub async fn try_next_event(&self) -> Option<QueueEventEntry> {
564 let mut rx = self.event_rx.lock().await;
565 rx.try_recv().ok()
566 }
567
568 pub async fn close(&self) {
570 self.closing.store(true, Ordering::SeqCst);
571 if let Some(handle) = self.task.lock().await.take() {
572 handle.abort();
573 }
574 self.event_tx.lock().await.take();
575 self.running.store(false, Ordering::SeqCst);
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 fn fields(pairs: &[(&str, &str)]) -> HashMap<String, String> {
584 pairs
585 .iter()
586 .map(|(k, v)| (k.to_string(), v.to_string()))
587 .collect()
588 }
589
590 #[test]
591 fn parses_completed_event() {
592 let event = parse_event(fields(&[
593 ("event", "completed"),
594 ("jobId", "42"),
595 ("returnvalue", "{\"ok\":true}"),
596 ("prev", "active"),
597 ]));
598 assert_eq!(
599 event,
600 QueueEvent::Completed {
601 job_id: "42".to_string(),
602 return_value: "{\"ok\":true}".to_string(),
603 prev: Some("active".to_string()),
604 }
605 );
606 assert_eq!(event.job_id(), Some("42"));
607 assert_eq!(event.name(), "completed");
608 }
609
610 #[test]
611 fn parses_progress_json() {
612 let event = parse_event(fields(&[
613 ("event", "progress"),
614 ("jobId", "7"),
615 ("data", "{\"pct\":50}"),
616 ]));
617 match event {
618 QueueEvent::Progress { job_id, data } => {
619 assert_eq!(job_id, "7");
620 assert_eq!(data, serde_json::json!({ "pct": 50 }));
621 }
622 other => panic!("expected progress, got {other:?}"),
623 }
624 }
625
626 #[test]
627 fn parses_progress_string_fallback() {
628 let event = parse_event(fields(&[
629 ("event", "progress"),
630 ("jobId", "7"),
631 ("data", "not-json"),
632 ]));
633 match event {
634 QueueEvent::Progress { data, .. } => {
635 assert_eq!(data, serde_json::Value::String("not-json".to_string()));
636 }
637 other => panic!("expected progress, got {other:?}"),
638 }
639 }
640
641 #[test]
642 fn parses_delayed_number() {
643 let event = parse_event(fields(&[
644 ("event", "delayed"),
645 ("jobId", "1"),
646 ("delay", "1700000000000"),
647 ]));
648 assert_eq!(
649 event,
650 QueueEvent::Delayed {
651 job_id: "1".to_string(),
652 delay: 1_700_000_000_000,
653 }
654 );
655 }
656
657 #[test]
658 fn parses_drained() {
659 let event = parse_event(fields(&[("event", "drained")]));
660 assert_eq!(event, QueueEvent::Drained);
661 assert_eq!(event.job_id(), None);
662 }
663
664 #[test]
665 fn unknown_event_is_other() {
666 let event = parse_event(fields(&[("event", "future-event"), ("jobId", "9")]));
667 match event {
668 QueueEvent::Other { event, fields } => {
669 assert_eq!(event, "future-event");
670 assert_eq!(fields.get("jobId"), Some(&"9".to_string()));
671 }
672 other => panic!("expected other, got {other:?}"),
673 }
674 }
675
676 #[test]
677 fn parses_failed_event() {
678 let event = parse_event(fields(&[
679 ("event", "failed"),
680 ("jobId", "5"),
681 ("failedReason", "boom"),
682 ("prev", "active"),
683 ]));
684 assert_eq!(
685 event,
686 QueueEvent::Failed {
687 job_id: "5".to_string(),
688 failed_reason: "boom".to_string(),
689 prev: Some("active".to_string()),
690 }
691 );
692 assert_eq!(event.name(), "failed");
693 }
694
695 #[test]
696 fn parses_active_and_waiting_with_prev() {
697 let active = parse_event(fields(&[
698 ("event", "active"),
699 ("jobId", "1"),
700 ("prev", "waiting"),
701 ]));
702 assert_eq!(
703 active,
704 QueueEvent::Active {
705 job_id: "1".to_string(),
706 prev: Some("waiting".to_string()),
707 }
708 );
709
710 let waiting = parse_event(fields(&[("event", "waiting"), ("jobId", "2")]));
711 assert_eq!(
712 waiting,
713 QueueEvent::Waiting {
714 job_id: "2".to_string(),
715 prev: None,
716 }
717 );
718 }
719
720 #[test]
721 fn parses_added_cleaned_and_removed() {
722 assert_eq!(
723 parse_event(fields(&[
724 ("event", "added"),
725 ("jobId", "1"),
726 ("name", "job")
727 ])),
728 QueueEvent::Added {
729 job_id: "1".to_string(),
730 name: "job".to_string(),
731 }
732 );
733 assert_eq!(
734 parse_event(fields(&[("event", "cleaned"), ("count", "50")])),
735 QueueEvent::Cleaned {
736 count: "50".to_string(),
737 }
738 );
739 assert_eq!(
740 parse_event(fields(&[
741 ("event", "removed"),
742 ("jobId", "3"),
743 ("prev", "delayed")
744 ])),
745 QueueEvent::Removed {
746 job_id: "3".to_string(),
747 prev: Some("delayed".to_string()),
748 }
749 );
750 }
751
752 #[test]
753 fn parses_retries_exhausted_and_waiting_children() {
754 assert_eq!(
755 parse_event(fields(&[
756 ("event", "retries-exhausted"),
757 ("jobId", "7"),
758 ("attemptsMade", "3"),
759 ])),
760 QueueEvent::RetriesExhausted {
761 job_id: "7".to_string(),
762 attempts_made: "3".to_string(),
763 }
764 );
765 let wc = parse_event(fields(&[("event", "waiting-children"), ("jobId", "8")]));
766 assert_eq!(
767 wc,
768 QueueEvent::WaitingChildren {
769 job_id: "8".to_string(),
770 }
771 );
772 assert_eq!(wc.name(), "waiting-children");
773 }
774
775 #[test]
776 fn parses_deduplicated_and_duplicated() {
777 assert_eq!(
778 parse_event(fields(&[
779 ("event", "deduplicated"),
780 ("jobId", "1"),
781 ("deduplicationId", "d1"),
782 ("deduplicatedJobId", "2"),
783 ])),
784 QueueEvent::Deduplicated {
785 job_id: "1".to_string(),
786 deduplication_id: "d1".to_string(),
787 deduplicated_job_id: Some("2".to_string()),
788 }
789 );
790 assert_eq!(
791 parse_event(fields(&[("event", "duplicated"), ("jobId", "9")])),
792 QueueEvent::Duplicated {
793 job_id: "9".to_string(),
794 }
795 );
796 }
797
798 #[test]
799 fn parses_paused_resumed_and_error() {
800 assert_eq!(
801 parse_event(fields(&[("event", "paused")])),
802 QueueEvent::Paused
803 );
804 assert_eq!(
805 parse_event(fields(&[("event", "resumed")])),
806 QueueEvent::Resumed
807 );
808 assert_eq!(
809 parse_event(fields(&[("event", "error"), ("message", "bad")])),
810 QueueEvent::Error {
811 message: "bad".to_string(),
812 }
813 );
814 }
815
816 #[test]
817 fn job_id_and_name_helpers_are_consistent() {
818 assert_eq!(QueueEvent::Drained.job_id(), None);
820 assert_eq!(QueueEvent::Paused.job_id(), None);
821 assert_eq!(QueueEvent::Resumed.job_id(), None);
822 assert_eq!(
823 QueueEvent::Cleaned {
824 count: "1".to_string()
825 }
826 .job_id(),
827 None
828 );
829 for ev in [
831 "active",
832 "added",
833 "cleaned",
834 "completed",
835 "debounced",
836 "deduplicated",
837 "delayed",
838 "drained",
839 "duplicated",
840 "error",
841 "failed",
842 "paused",
843 "progress",
844 "removed",
845 "resumed",
846 "retries-exhausted",
847 "stalled",
848 "waiting",
849 "waiting-children",
850 ] {
851 assert_eq!(parse_event(fields(&[("event", ev)])).name(), ev);
852 }
853 }
854}