use std::time::Duration;
use queuey_core::{Backoff, Job, QueueSet, RetryPolicy};
use queuey_macros::{Job, Queues};
use serde::{Deserialize, Serialize};
pub mod facade {
pub use queuey_core::*;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
#[queues(prefix = "myapp")]
enum AppQueues {
#[queue(prefetch = 10)]
Emails,
#[queue(
name = "img",
retry(max_attempts = 3, backoff = "exponential", base = "1s", max = "2m")
)]
Images,
HTTPCalls,
SendEmails,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
enum Plain {
#[queue(prefetch = 1, durable = false, message_ttl = "30s")]
Fast,
#[queue(retry(max_attempts = 2, backoff = "fixed", delay = "500ms"))]
Slow,
#[queue(name = "custom.name", retry(backoff = "none"))]
Custom,
#[queue(message_ttl = "2 m")]
WhitespaceTtl,
#[queue(message_ttl = "45")]
BareSeconds,
#[queue(retry())]
DefaultRetry,
#[queue(retry(
max_attempts = 9,
backoff = "exponential",
base = "250ms",
factor = 1.5,
max = "1h",
jitter = false
))]
FullyTuned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
enum Priorities {
#[queue(max_priority = 3)]
Tiered,
#[queue(max_priority = 0)]
Unprioritised,
#[queue(prefetch = 4)]
Defaulted,
#[queue(max_priority = 255)]
EveryLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
#[queues(crate = "crate::facade", prefix = "aliased")]
enum ViaFacade {
Work,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
enum ApproxConstants {
#[queue(retry(factor = 3.14159265358979))]
Pi,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails)]
struct SendEmail {
to: String,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Images, name = "images.resize")]
struct ResizeImage {
path: String,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails, retry(max_attempts = 5))]
struct RetryingJob;
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails, retry(max_attempts = 4, backoff = "fixed", delay = "2s"))]
struct FixedRetryJob;
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails, retry(max_attempts = 1, backoff = "none"))]
struct NoBackoffJob;
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = self::ViaFacade::Work, crate = "crate::facade")]
enum FacadeJob {
Only,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = crate::Plain::Custom)]
struct FullyQualifiedQueue {
id: u64,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = ApproxConstants::Pi, retry(factor = 2.71828182845905, max = "10m"))]
struct ApproxConstantJob;
fn queue_of<J: Job>() -> J::Queue {
J::QUEUE
}
#[test]
fn all_returns_variants_in_declaration_order() {
assert_eq!(
AppQueues::all(),
&[
AppQueues::Emails,
AppQueues::Images,
AppQueues::HTTPCalls,
AppQueues::SendEmails
]
);
assert_eq!(Plain::all().len(), 7);
assert_eq!(Plain::all()[0], Plain::Fast);
}
#[test]
fn names_apply_the_prefix_and_snake_case_default() {
assert_eq!(AppQueues::Emails.name(), "myapp.emails");
assert_eq!(AppQueues::Images.name(), "myapp.img");
assert_eq!(AppQueues::HTTPCalls.name(), "myapp.http_calls");
assert_eq!(AppQueues::SendEmails.name(), "myapp.send_emails");
}
#[test]
fn names_without_a_prefix_are_bare() {
assert_eq!(Plain::Fast.name(), "fast");
assert_eq!(Plain::Slow.name(), "slow");
assert_eq!(Plain::Custom.name(), "custom.name");
assert_eq!(Plain::WhitespaceTtl.name(), "whitespace_ttl");
assert_eq!(ViaFacade::Work.name(), "aliased.work");
}
#[test]
fn names_are_static_strings() {
fn keep(name: &'static str) -> &'static str {
name
}
assert_eq!(keep(AppQueues::Emails.name()), "myapp.emails");
}
#[test]
fn from_name_round_trips() {
for queue in AppQueues::all() {
assert_eq!(AppQueues::from_name(queue.name()), Some(*queue));
}
assert_eq!(Plain::from_name("custom.name"), Some(Plain::Custom));
assert_eq!(AppQueues::from_name("emails"), None);
assert_eq!(AppQueues::from_name("nope"), None);
}
#[test]
fn config_defaults_match_queue_config_new() {
let config = AppQueues::HTTPCalls.config();
assert_eq!(config.name, "myapp.http_calls");
assert_eq!(config.prefetch, 16);
assert!(config.durable);
assert_eq!(config.message_ttl, None);
assert_eq!(config.retry, RetryPolicy::default());
}
#[test]
fn config_carries_prefetch_durable_and_ttl() {
let config = Plain::Fast.config();
assert_eq!(config.name, "fast");
assert_eq!(config.prefetch, 1);
assert!(!config.durable);
assert_eq!(config.message_ttl, Some(Duration::from_secs(30)));
assert_eq!(AppQueues::Emails.config().prefetch, 10);
assert_eq!(
Plain::WhitespaceTtl.config().message_ttl,
Some(Duration::from_secs(120))
);
assert_eq!(
Plain::BareSeconds.config().message_ttl,
Some(Duration::from_secs(45))
);
}
#[test]
fn config_carries_max_priority() {
assert_eq!(Priorities::Tiered.config().max_priority, Some(3));
assert_eq!(Priorities::Unprioritised.config().max_priority, None);
assert_eq!(Priorities::Defaulted.config().max_priority, Some(10));
assert_eq!(Priorities::EveryLevel.config().max_priority, Some(255));
}
#[test]
fn config_retry_exponential_with_explicit_values() {
assert_eq!(
AppQueues::Images.config().retry,
RetryPolicy {
max_attempts: 3,
backoff: Backoff::Exponential {
base: Duration::from_secs(1),
factor: 2.0,
max: Duration::from_secs(120),
jitter: true,
},
}
);
}
#[test]
fn config_retry_defaults_mirror_backoff_exponential() {
assert_eq!(
Plain::DefaultRetry.config().retry,
RetryPolicy {
max_attempts: 3,
backoff: Backoff::exponential(),
}
);
}
#[test]
fn config_retry_fully_tuned_exponential() {
assert_eq!(
Plain::FullyTuned.config().retry,
RetryPolicy {
max_attempts: 9,
backoff: Backoff::Exponential {
base: Duration::from_millis(250),
factor: 1.5,
max: Duration::from_secs(3600),
jitter: false,
},
}
);
}
#[test]
fn config_retry_fixed_and_none() {
assert_eq!(
Plain::Slow.config().retry,
RetryPolicy {
max_attempts: 2,
backoff: Backoff::Fixed(Duration::from_millis(500)),
}
);
assert_eq!(
Plain::Custom.config().retry,
RetryPolicy {
max_attempts: 3,
backoff: Backoff::None,
}
);
}
#[test]
fn job_name_defaults_to_the_type_path() {
assert_eq!(
SendEmail::NAME,
concat!(module_path!(), "::", stringify!(SendEmail))
);
assert!(SendEmail::NAME.ends_with("::SendEmail"));
}
#[test]
fn job_name_can_be_overridden() {
assert_eq!(ResizeImage::NAME, "images.resize");
}
#[test]
fn job_queue_associates_the_right_set_and_variant() {
assert_eq!(SendEmail::QUEUE, AppQueues::Emails);
assert_eq!(ResizeImage::QUEUE, AppQueues::Images);
assert_eq!(FacadeJob::QUEUE, ViaFacade::Work);
assert_eq!(FullyQualifiedQueue::QUEUE, Plain::Custom);
assert_eq!(queue_of::<SendEmail>(), AppQueues::Emails);
assert_eq!(queue_of::<FullyQualifiedQueue>(), Plain::Custom);
let queue: <SendEmail as Job>::Queue = AppQueues::Images;
assert_eq!(queue.name(), "myapp.img");
}
#[test]
fn retry_policy_is_none_without_the_attribute() {
assert_eq!(SendEmail::retry_policy(), None);
assert_eq!(ResizeImage::retry_policy(), None);
assert_eq!(FacadeJob::retry_policy(), None);
}
#[test]
fn retry_policy_uses_exponential_defaults() {
assert_eq!(
RetryingJob::retry_policy(),
Some(RetryPolicy {
max_attempts: 5,
backoff: Backoff::exponential(),
})
);
}
#[test]
fn retry_policy_supports_fixed_and_none_backoff() {
assert_eq!(
FixedRetryJob::retry_policy(),
Some(RetryPolicy {
max_attempts: 4,
backoff: Backoff::Fixed(Duration::from_secs(2)),
})
);
assert_eq!(
NoBackoffJob::retry_policy(),
Some(RetryPolicy {
max_attempts: 1,
backoff: Backoff::None,
})
);
}
#[test]
fn constant_like_factors_round_trip_without_clippy_complaining() {
let Backoff::Exponential { factor, .. } = ApproxConstants::Pi.config().retry.backoff else {
panic!("expected an exponential backoff");
};
assert!((factor - std::f64::consts::PI).abs() < 1e-12, "{factor}");
let Some(RetryPolicy {
backoff: Backoff::Exponential { factor, max, .. },
..
}) = ApproxConstantJob::retry_policy()
else {
panic!("expected an exponential backoff");
};
assert!((factor - std::f64::consts::E).abs() < 1e-12, "{factor}");
assert_eq!(max, Duration::from_secs(600));
}
#[test]
fn jobs_stay_serializable() {
let job = SendEmail {
to: "someone@example.com".to_owned(),
};
let encoded = serde_json::to_string(&job).unwrap();
let decoded: SendEmail = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.to, job.to);
}