use std::mem::size_of;
use std::process::Command;
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::{Duration, UNIX_EPOCH};
use armdb::armour::Db;
use armdb::{DEAD_CLASS, DbError, Key, QueueConfig, QueueKey, QueueSpec, queue};
use armour_core::GetType;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(transparent)]
#[allow(dead_code)]
pub struct FixedJob(u64);
queue!(FixedJobs, payload = FixedJob, name = "queue_fixed_jobs");
queue!(
FixedJobsSchemaDrift,
payload = FixedJob,
name = "queue_fixed_schema_drift"
);
#[derive(
Clone,
Copy,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(transparent)]
pub struct ChangedFixedJob(u64);
queue!(
FixedJobsSchemaDriftChanged,
payload = ChangedFixedJob,
name = "queue_fixed_schema_drift"
);
#[test]
fn queue_macro_sets_the_exact_physical_names_and_key_order() {
assert_eq!(FixedJobs::NAME, "queue_fixed_jobs");
assert_eq!(FixedJobs::PAYLOAD_NAME, "queue_fixed_jobs.payload");
let a = QueueKey::live(7, 3);
let b = QueueKey::live(8, 1);
assert!(a.as_bytes() < b.as_bytes());
assert_eq!(
a.as_bytes(),
&[0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 3]
);
assert_eq!(size_of::<QueueKey>(), 16);
assert_eq!(a.as_bytes().len(), 16);
let dead = QueueKey::dead(7, 3);
assert_eq!(dead.class(), DEAD_CLASS);
assert_eq!(dead.visible_at_ms(), 7);
assert_eq!(dead.id(), 3);
assert_eq!(QueueKey::from_bytes(dead.as_bytes()), dead);
assert!(std::panic::catch_unwind(|| QueueKey::live(1 << 63, 3)).is_err());
}
#[test]
fn queue_config_rejects_an_unusable_retry_policy() {
assert!(QueueConfig::balanced().max_attempts(0).build().is_err());
assert!(QueueConfig::balanced().backoff_secs(&[]).build().is_err());
assert!(matches!(DbError::LeaseExpired, DbError::LeaseExpired));
}
#[test]
fn open_queue_rejects_unbuilt_invalid_fixed_policies_before_nack_can_panic() {
let assert_invalid = |config| {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
assert!(matches!(
db.open_queue::<FixedJobs>(config),
Err(DbError::Config(_))
));
};
assert_invalid(QueueConfig::balanced().max_attempts(0));
assert_invalid(QueueConfig::balanced().backoff_secs(&[]));
assert_invalid(QueueConfig::balanced().lease_timeout(Duration::ZERO));
assert_invalid(QueueConfig::balanced().wakeup_recheck(Duration::ZERO));
}
#[test]
fn push_is_durable_and_open_seeds_live_count() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let q = db
.open_queue::<FixedJobs>(QueueConfig::balanced().build().unwrap())
.unwrap();
let first = q.push(FixedJob(11), None).unwrap();
let second = q.push(FixedJob(12), None).unwrap();
assert_eq!(q.len(), 2);
drop(q);
drop(db);
let reopened = armdb::armour::Db::open_test(dir.path()).unwrap();
let q = reopened
.open_queue::<FixedJobs>(QueueConfig::balanced().build().unwrap())
.unwrap();
assert_eq!(q.len(), 2);
assert!(q.push(FixedJob(13), None).unwrap() > second);
assert!(second > first);
}
#[test]
fn fixed_payload_schema_drift_is_rejected_on_reopen() {
let dir = tempfile::tempdir().unwrap();
Db::open_test(dir.path())
.unwrap()
.open_queue::<FixedJobsSchemaDrift>(QueueConfig::balanced().build().unwrap())
.unwrap();
let result = Db::open_test(dir.path())
.unwrap()
.open_queue::<FixedJobsSchemaDriftChanged>(QueueConfig::balanced().build().unwrap());
assert!(matches!(result, Err(DbError::SchemaMismatch { .. })));
}
#[test]
fn queue_header_config_is_ascending_and_has_small_queue_baseline() {
let cfg = QueueConfig::balanced().build().unwrap().fixed_config();
assert!(!cfg.reversed);
assert_eq!(cfg.grow_step, 100_000);
}
#[test]
fn depth_cap_refuses_without_creating_a_row() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let q = db
.open_queue::<FixedJobs>(QueueConfig::balanced().max_depth(Some(1)).build().unwrap())
.unwrap();
q.push(FixedJob(1), None).unwrap();
assert!(matches!(q.push(FixedJob(2), None), Err(DbError::QueueFull)));
assert_eq!(q.len(), 1);
}
#[test]
fn concurrent_pushes_do_not_exceed_the_documented_soft_depth_bound() {
const MAX_DEPTH: u64 = 1;
const PUSHERS: usize = 8;
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let queue = db
.open_queue::<FixedJobs>(
QueueConfig::balanced()
.max_depth(Some(MAX_DEPTH))
.build()
.unwrap(),
)
.unwrap();
let barrier = Arc::new(Barrier::new(PUSHERS));
armdb::queue_test_support::set_fixed_queue_push_admission_barrier(Some(Arc::clone(&barrier)));
let pushers: Vec<_> = (0..PUSHERS)
.map(|id| {
let queue = Arc::clone(&queue);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
queue.push(FixedJob(id as u64 + 1), None)
})
})
.collect();
let results: Vec<_> = pushers
.into_iter()
.map(|pusher| pusher.join().unwrap())
.collect();
armdb::queue_test_support::set_fixed_queue_push_admission_barrier(None);
assert_eq!(
results.iter().filter(|result| result.is_ok()).count(),
PUSHERS,
"every producer passed the same below-cap admission check"
);
assert!(
queue.len() <= MAX_DEPTH + (PUSHERS as u64 - 1),
"concurrent pushers exceeded the documented soft-cap race bound"
);
assert_eq!(queue.len(), PUSHERS as u64);
}
#[test]
fn fixed_queue_leases_due_rows_by_visible_at_across_retry_and_expiry() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let queue = db
.open_queue::<FixedJobs>(
QueueConfig::balanced()
.lease_timeout(Duration::from_millis(1))
.max_attempts(2)
.backoff_secs(&[0])
.build()
.unwrap(),
)
.unwrap();
let first_visible = UNIX_EPOCH + Duration::from_millis(100);
let second_visible = UNIX_EPOCH + Duration::from_millis(200);
let third_visible = UNIX_EPOCH + Duration::from_millis(300);
let future = std::time::SystemTime::now() + Duration::from_secs(60);
queue.push(FixedJob(10), Some(first_visible)).unwrap();
queue.push(FixedJob(20), Some(second_visible)).unwrap();
queue.push(FixedJob(30), Some(third_visible)).unwrap();
queue.push(FixedJob(40), Some(future)).unwrap();
let first = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(*first.payload(), FixedJob(10));
assert_eq!(first.key().visible_at_ms(), 100);
queue.nack(first.token()).unwrap();
let second = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(*second.payload(), FixedJob(20));
assert_eq!(second.key().visible_at_ms(), 200);
queue.ack(second.token()).unwrap();
let expired = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(*expired.payload(), FixedJob(30));
assert_eq!(expired.key().visible_at_ms(), 300);
thread::sleep(Duration::from_millis(3));
let released = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(*released.payload(), FixedJob(30));
assert_eq!(released.key().visible_at_ms(), 300);
queue.ack(released.token()).unwrap();
let retried = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(*retried.payload(), FixedJob(10));
assert!(
retried.key().visible_at_ms()
> third_visible
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
);
queue.ack(retried.token()).unwrap();
assert!(queue.lease(1, None).unwrap().is_empty());
assert_eq!(queue.len(), 1, "the future row stays unleased");
}
#[test]
fn child_crash_after_first_push_never_reuses_a_published_id_block() {
let dir = tempfile::tempdir().unwrap();
let status = Command::new(std::env::current_exe().unwrap())
.arg("--exact")
.arg("queue_fixed_crash_child_after_first_push")
.arg("--nocapture")
.env("ARMDB_QUEUE_CRASH_DIR", dir.path())
.status()
.unwrap();
assert_eq!(status.code(), Some(9));
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let q = db
.open_queue::<FixedJobs>(QueueConfig::balanced().build().unwrap())
.unwrap();
assert!(
q.push(FixedJob(2), None).unwrap() >= armdb::ID_BLOCK,
"a hard crash must not permit reuse of the first published id block"
);
}
#[test]
fn queue_fixed_crash_child_after_first_push() {
let Ok(dir) = std::env::var("ARMDB_QUEUE_CRASH_DIR") else {
return;
};
let db = armdb::armour::Db::open_test(dir).unwrap();
let q = db
.open_queue::<FixedJobs>(QueueConfig::balanced().build().unwrap())
.unwrap();
q.push(FixedJob(1), None).unwrap();
unsafe { libc::_exit(9) }
}
#[test]
fn push_rejects_not_before_outside_the_queue_timestamp_domain() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let q = db
.open_queue::<FixedJobs>(QueueConfig::balanced().build().unwrap())
.unwrap();
let out_of_range = UNIX_EPOCH + Duration::from_millis(1 << 63);
assert!(matches!(
q.push(FixedJob(1), Some(out_of_range)),
Err(DbError::Client(_))
));
assert_eq!(q.len(), 0);
}
#[test]
fn an_expired_lease_does_not_consume_the_retry_budget() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let queue = db
.open_queue::<FixedJobs>(
QueueConfig::balanced()
.lease_timeout(Duration::from_millis(1))
.max_attempts(2)
.backoff_secs(&[0])
.build()
.unwrap(),
)
.unwrap();
queue.push(FixedJob(1), None).unwrap();
let first = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(
first.attempts(),
0,
"a first delivery has no failures behind it"
);
thread::sleep(Duration::from_millis(3));
let after_expiry = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(
after_expiry.attempts(),
0,
"an expired lease must not be charged to the retry budget"
);
queue.nack(after_expiry.token()).unwrap();
assert_eq!(queue.dead_len(), 0, "one nack is not the final attempt");
let retried = queue.lease(1, None).unwrap().pop().unwrap();
assert_eq!(retried.attempts(), 1, "exactly one failed delivery so far");
queue.nack(retried.token()).unwrap();
assert_eq!(queue.dead_len(), 1, "the second nack is terminal");
assert_eq!(queue.len(), 0);
}
#[test]
fn an_expired_lease_nobody_re_took_still_acks() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let queue = db
.open_queue::<FixedJobs>(
QueueConfig::balanced()
.lease_timeout(Duration::from_millis(1))
.build()
.unwrap(),
)
.unwrap();
queue.push(FixedJob(1), None).unwrap();
let leased = queue.lease(1, None).unwrap().pop().unwrap();
thread::sleep(Duration::from_millis(5));
queue.ack(leased.token()).unwrap();
assert_eq!(queue.len(), 0);
assert_eq!(queue.dead_len(), 0);
}
#[test]
fn a_re_leased_expiry_fences_the_original_token() {
let dir = tempfile::tempdir().unwrap();
let db = armdb::armour::Db::open_test(dir.path()).unwrap();
let queue = db
.open_queue::<FixedJobs>(
QueueConfig::balanced()
.lease_timeout(Duration::from_millis(1))
.build()
.unwrap(),
)
.unwrap();
queue.push(FixedJob(1), None).unwrap();
let first = queue.lease(1, None).unwrap().pop().unwrap();
thread::sleep(Duration::from_millis(5));
let second = queue.lease(1, None).unwrap().pop().unwrap();
assert!(matches!(
queue.ack(first.token()),
Err(DbError::LeaseExpired)
));
queue.ack(second.token()).unwrap();
assert_eq!(queue.len(), 0);
}