#![cfg(feature = "redis")]
use std::time::Duration;
use chrono::Utc;
use rainier_drivers::RedisConnector;
use rainier_queue::{Queue, QueuedJob, RedisQueue};
use serde_json::json;
async fn queue(name: &str) -> Option<RedisQueue> {
let url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".to_string());
let connector = RedisConnector::open(&url).ok()?;
let queue = RedisQueue::connect(&connector)
.await
.ok()?
.with_prefix(format!("rainier-test:{name}:"))
.with_reservation(Duration::from_millis(200))
.as_consumer(format!("{name}-worker"));
queue.clear("default").await.ok()?;
Some(queue)
}
macro_rules! redis_or_skip {
($name:literal) => {
match queue($name).await {
Some(queue) => queue,
None if std::env::var("REDIS_REQUIRED").is_ok() => {
panic!(
"REDIS_REQUIRED is set and no Redis answered at {}",
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".into())
)
}
None => {
eprintln!("skipping `{}`: no Redis at REDIS_URL", $name);
return;
}
}
};
}
fn job(id: &str) -> QueuedJob {
QueuedJob {
id: id.into(),
name: "test.job".into(),
payload: json!({ "n": 1 }),
queue: "default".into(),
attempts: 0,
max_attempts: 3,
available_at: Utc::now(),
created_at: Utc::now(),
unique_key: None,
}
}
#[tokio::test]
async fn a_pushed_job_comes_back_from_reserve() {
let queue = redis_or_skip!("roundtrip");
queue.push(job("j1")).await.expect("push");
assert_eq!(queue.size("default").await.unwrap(), 1);
let reserved = queue.reserve("default").await.expect("reserve").expect("a job");
assert_eq!(reserved.id, "j1");
assert_eq!(reserved.attempts, 1, "reserving is an attempt");
assert_eq!(reserved.payload["n"], 1);
queue.acknowledge(&reserved).await.expect("ack");
assert_eq!(queue.size("default").await.unwrap(), 0, "an acked job is gone from the stream");
}
#[tokio::test]
async fn an_empty_queue_reserves_nothing() {
let queue = redis_or_skip!("empty");
assert!(queue.reserve("default").await.expect("reserve").is_none());
}
#[tokio::test]
async fn two_workers_do_not_get_the_same_job() {
let queue = redis_or_skip!("exclusive");
queue.push(job("j1")).await.expect("push");
let first = queue.reserve("default").await.expect("reserve");
let second = queue.reserve("default").await.expect("reserve");
assert!(first.is_some());
assert!(second.is_none(), "the second worker should find nothing");
}
#[tokio::test]
async fn a_job_a_dead_worker_held_is_redelivered() {
let queue = redis_or_skip!("redelivery");
queue.push(job("j1")).await.expect("push");
let abandoned = queue.reserve("default").await.expect("reserve").expect("a job");
assert_eq!(abandoned.attempts, 1);
assert!(queue.reserve("default").await.expect("reserve").is_none());
tokio::time::sleep(Duration::from_millis(400)).await;
let reclaimed = queue.reserve("default").await.expect("reserve").expect("redelivered");
assert_eq!(reclaimed.id, "j1", "the same job comes back");
queue.acknowledge(&reclaimed).await.expect("ack");
}
#[tokio::test]
async fn a_delayed_job_is_not_available_until_it_is_due() {
let queue = redis_or_skip!("delayed");
let mut delayed = job("j1");
delayed.available_at = Utc::now() + chrono::Duration::milliseconds(400);
queue.push(delayed).await.expect("push");
assert_eq!(queue.size("default").await.unwrap(), 1, "queued, just not due");
assert!(queue.reserve("default").await.expect("reserve").is_none());
tokio::time::sleep(Duration::from_millis(600)).await;
let due = queue.reserve("default").await.expect("reserve").expect("now due");
assert_eq!(due.id, "j1");
}
#[tokio::test]
async fn releasing_puts_a_job_back_for_another_attempt() {
let queue = redis_or_skip!("release");
queue.push(job("j1")).await.expect("push");
let reserved = queue.reserve("default").await.expect("reserve").expect("a job");
queue.release(&reserved, Duration::ZERO).await.expect("release");
let again = queue.reserve("default").await.expect("reserve").expect("back again");
assert_eq!(again.id, "j1");
assert_eq!(again.attempts, 2, "the attempt count carries across the release");
}
#[tokio::test]
async fn a_released_job_with_a_delay_waits_for_it() {
let queue = redis_or_skip!("backoff");
queue.push(job("j1")).await.expect("push");
let reserved = queue.reserve("default").await.expect("reserve").expect("a job");
queue.release(&reserved, Duration::from_millis(400)).await.expect("release");
assert!(queue.reserve("default").await.expect("reserve").is_none(), "still backing off");
tokio::time::sleep(Duration::from_millis(600)).await;
assert!(queue.reserve("default").await.expect("reserve").is_some());
}
#[tokio::test]
async fn a_failed_job_leaves_the_stream_and_lands_in_the_failed_list() {
let queue = redis_or_skip!("failed");
queue.push(job("j1")).await.expect("push");
let reserved = queue.reserve("default").await.expect("reserve").expect("a job");
queue.fail(&reserved, "it did not work").await.expect("fail");
assert_eq!(queue.size("default").await.unwrap(), 0);
let failed = queue.failed_jobs("default").await.expect("read the failed list");
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].job.id, "j1");
assert_eq!(failed[0].error, "it did not work");
assert!(failed[0].job.payload.get("__redis_entry_id").is_none());
}
#[tokio::test]
async fn clearing_removes_ready_delayed_and_failed_alike() {
let queue = redis_or_skip!("clear");
queue.push(job("ready")).await.expect("push");
let mut later = job("later");
later.available_at = Utc::now() + chrono::Duration::seconds(60);
queue.push(later).await.expect("push");
assert_eq!(queue.size("default").await.unwrap(), 2);
let removed = queue.clear("default").await.expect("clear");
assert_eq!(removed, 2);
assert_eq!(queue.size("default").await.unwrap(), 0);
}
#[tokio::test]
async fn two_queues_on_one_redis_do_not_see_each_other() {
let queue = redis_or_skip!("isolation");
queue.push(job("on-default")).await.expect("push");
queue.push(QueuedJob { queue: "mail".into(), ..job("on-mail") }).await.expect("push");
assert_eq!(queue.size("default").await.unwrap(), 1);
assert_eq!(queue.size("mail").await.unwrap(), 1);
let reserved = queue.reserve("mail").await.expect("reserve").expect("a job");
assert_eq!(reserved.id, "on-mail");
queue.acknowledge(&reserved).await.expect("ack");
queue.clear("mail").await.expect("clear");
}
#[tokio::test]
async fn the_eviction_policy_is_reported() {
let queue = redis_or_skip!("eviction");
if let Some(policy) = queue.check_eviction_policy().await {
assert!(!policy.is_empty());
}
}