Files
alkstore/alkstore-postgres/src/store/queue_tests.rs
T

1653 lines
58 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The queue mechanism's acceptance tests (`pg-engine-queues`): the
//! enqueue→claim→ack lifecycle with stamps visible via `get_job`
//! matching the resolution rules, the extent guard (ADR-023 §2), the
//! exactly-once handout under concurrent claims (two workers, one row;
//! 6×40 drain, no double handouts), the backoff curve's range pin
//! (statistical, tolerance-bounded) with the 1-hour cap and the
//! `Some(d)` override, the uniform validity predicate refusing lapsed
//! ops with `Ok(false)` and a reclaim consuming an attempt, the
//! dead-letter defaults and dead-row `get_job` visibility, the
//! pre-claim exhausted-reclaimable sweep, claim ordering, cancel +
//! `ack_batch`'s per-id predicate without a worker filter, `sweep_
//! expired`'s both-states move + retention (incl. forever-none), the
//! wake-driven claim loop pinned end-to-end (enqueue → notify → wake →
//! claim, cross-connection; the LISTEN-driven posture with the re-poll
//! safety net — the contract claim ops stay pull-shaped), queue-name
//! scoping, the validation posture (empty/reserved queue names, empty
//! `worker_id`), and the closed-store fail-closed shape on all queue
//! ops (ADR-010; ADR-019 §1–§3; ADR-020; ADR-023 §2).
//!
//! Harness convention (the schema/open tasks'): connection settings
//! ride the environment (`ALKSTORE_PG_HOST/PORT/USER/PASSWORD/DB`),
//! never hardcoded; tests without a reachable server skip cleanly so
//! the workspace gates stay green server-less. Isolation is a fresh
//! unique schema per test.
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use alkstore::{EnqueueOpts, JobHandle, JobState, QueueOpts, Store};
use serde_json::{Value, json};
use crate::queue::{EXHAUSTED_ERROR, EXPIRED_ERROR, FAILED_ERROR, backoff_delay_s};
use crate::store::{PgStore, open_store};
const ENV_HOST: &str = "ALKSTORE_PG_HOST";
const ENV_PORT: &str = "ALKSTORE_PG_PORT";
const ENV_USER: &str = "ALKSTORE_PG_USER";
const ENV_PASSWORD: &str = "ALKSTORE_PG_PASSWORD";
const ENV_DB: &str = "ALKSTORE_PG_DB";
fn harness_dsn() -> Option<String> {
let host = std::env::var(ENV_HOST).ok()?;
let port: u16 = std::env::var(ENV_PORT).ok()?.parse().ok()?;
let user = std::env::var(ENV_USER).ok()?;
let password = std::env::var(ENV_PASSWORD).ok()?;
let db = std::env::var(ENV_DB).unwrap_or_else(|_| "postgres".to_string());
Some(format!(
"host={host} port={port} user={user} password={password} dbname={db}"
))
}
fn instance_namer(tag: &str) -> impl Fn() -> String + use<'_> {
let counter = AtomicU64::new(0);
move || {
format!(
"{tag}_{}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos(),
counter.fetch_add(1, Ordering::SeqCst),
)
}
}
/// A raw admin client for cross-connection probes (NOTIFY senders,
/// schema teardown, row backdating) — separate from the store's pool.
async fn harness_client() -> Option<tokio_postgres::Client> {
let (client, connection) = tokio_postgres::connect(&harness_dsn()?, tokio_postgres::NoTls)
.await
.ok()?;
tokio::spawn(async move {
let _ = connection.await;
});
Some(client)
}
async fn drop_schema(client: &tokio_postgres::Client, schema: &str) {
if schema == crate::schema::DEFAULT_SCHEMA {
return;
}
let sql = format!(
"DROP SCHEMA IF EXISTS {} CASCADE",
crate::schema::quote_identifier(schema)
);
let _ = client.batch_execute(&sql).await;
}
fn test_opts(schema: &str) -> crate::opts::PgOpts {
crate::opts::PgOpts {
schema: schema.to_string(),
..crate::opts::PgOpts::default()
}
}
/// Open a store on the harness server with a fresh unique schema —
/// `None` = no harness server (the skip posture). The returned store
/// closes on `Fixture` drop (the schema teardown included).
struct Fixture {
store: PgStore,
admin: tokio_postgres::Client,
schema: String,
}
impl Fixture {
async fn open(namer: &impl Fn() -> String) -> Option<Fixture> {
let dsn = harness_dsn()?;
let schema = namer();
let store = open_store(&dsn, test_opts(&schema)).await.unwrap();
let admin = harness_client().await?;
Some(Fixture {
store,
admin,
schema,
})
}
}
impl Drop for Fixture {
fn drop(&mut self) {
self.store.close();
}
}
fn opts() -> QueueOpts {
QueueOpts {
visibility_timeout_s: 300,
max_attempts: 3,
backoff_base_s: 5,
dead_letter_retention_s: None,
}
}
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
/// Backdate a live row's `expires_at` past the wall clock (the honest
/// cross-connection probe for the sweep's both-states move — a fresh
/// enqueue would carry a future expiry, so the admin UPDATE backdates
/// it: schema-qualified, typed-binded).
async fn backdate_expires(admin: &tokio_postgres::Client, schema: &str, id: i64) {
admin
.execute(
&format!(
"UPDATE {} SET expires_at = $2 WHERE id = $1",
crate::schema::QualifiedTable {
schema,
name: crate::schema::tables::JOB,
}
),
&[&id, &(now_unix() - 1)],
)
.await
.unwrap();
}
/// Acceptance: the full lifecycle — enqueue stamps the resolved values
/// (resolved `run_at` after delay-over-`run_at`, absolute `expires_at`
/// after relative-`expires`, the handle's `QueueOpts` stamps with the
/// per-job `max_attempts` override), claim returns a boxed handle
/// carrying the row, ack deletes, `get_job` sees the states (ADR-010
/// §1/§3a; ADR-020 §1–§2; ADR-019 §1).
#[tokio::test(flavor = "multi_thread")]
async fn enqueue_claim_ack_lifecycle_with_resolved_stamps() {
let Some(fx) = Fixture::open(&instance_namer("ql")).await else {
eprintln!("skip: no harness server");
return;
};
let qopts = QueueOpts {
visibility_timeout_s: 120,
max_attempts: 4,
backoff_base_s: 9,
dead_letter_retention_s: Some(7200),
};
let queue = fx.store.queue("emails", qopts.clone()).await.unwrap();
assert_eq!(queue.name(), "emails");
let before = now_unix();
let id = queue
.enqueue(
json!({"to": "a@example.com"}),
EnqueueOpts {
delay: Some(60),
run_at: Some(before + 10_000),
priority: 3,
max_attempts: Some(7),
expires: Some(1200),
},
)
.await
.unwrap();
let job = queue.get_job(id).await.unwrap().expect("row exists");
assert_eq!(job.id, id);
assert_eq!(job.queue, "emails");
assert_eq!(job.state, JobState::Pending);
assert_eq!(job.priority, 3);
assert!(
(before + 60..=before + 62).contains(&job.run_at),
"ready time is now + delay, delay wins over run_at: {}",
job.run_at
);
assert_eq!(
job.expires_at,
Some(before + 1200),
"relative expires resolves to the absolute row expiry"
);
assert_eq!(job.max_attempts, 7, "the per-job override stamps the row");
assert_eq!(job.visibility_timeout_s, 120, "the handle's stamp");
assert_eq!(job.backoff_base_s, 9, "the handle's stamp");
assert_eq!(job.dead_letter_retention_s, Some(7200));
assert_eq!(job.attempts, 0);
assert_eq!(job.worker_id, None);
assert_eq!(job.claimed_at, None);
assert_eq!(job.claim_expires_at, None);
assert_eq!(job.last_error, None);
assert_eq!(job.died_at, None);
let decoded: Value = job.payload_as().unwrap();
assert_eq!(decoded, json!({"to": "a@example.com"}));
// Not yet claimable — ready time is a minute out.
assert!(queue.claim_one("w1").await.unwrap().is_none());
// A second enqueue with run_at in the past is claimable now.
let id2 = queue
.enqueue(
json!({"n": 2}),
EnqueueOpts {
run_at: Some(before - 10),
..Default::default()
},
)
.await
.unwrap();
let job2 = queue.get_job(id2).await.unwrap().unwrap();
assert!(
(before - 10..=before - 9).contains(&job2.run_at),
"run_at is used literally when delay is unset"
);
// Claim: the handle carries the row, the claim counts an attempt,
// the deadline comes from the job's own visibility stamp.
let handle = queue.claim_one("w1").await.unwrap().expect("claimable");
let job = handle.job();
assert_eq!(job.id, id2);
assert_eq!(job.state, JobState::Processing);
assert_eq!(job.attempts, 1);
assert_eq!(job.worker_id.as_deref(), Some("w1"));
assert!(job.claimed_at.is_some());
assert_eq!(
job.claim_expires_at,
Some(job.claimed_at.unwrap() + 120),
"the deadline comes from the job's own visibility stamp"
);
// get_job reflects the claim.
let seen = queue.get_job(id2).await.unwrap().unwrap();
assert_eq!(seen.state, JobState::Processing);
assert_eq!(seen.worker_id.as_deref(), Some("w1"));
// A second worker cannot claim it (exactly-once handout).
assert!(queue.claim_one("w2").await.unwrap().is_none());
// Ack deletes the row (ADR-010 §1).
assert!(handle.ack().await.unwrap());
assert!(queue.get_job(id2).await.unwrap().is_none());
assert!(queue.claim_one("w2").await.unwrap().is_none());
assert!(
queue.get_job(id).await.unwrap().is_some(),
"the delayed row survives untouched"
);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: the extent guard (ADR-023 §2) — `claim_batch(n <= 0)`
/// claims nothing (the empty `Vec`, no error; pg's `LIMIT` with a
/// negative would be a server error, which the guard makes
/// unreachable) and `claim_one` on an empty queue is `None` (no-work
/// is a value).
#[tokio::test(flavor = "multi_thread")]
async fn extent_guard_claims_nothing_on_non_positive_n() {
let Some(fx) = Fixture::open(&instance_namer("qe")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("work", opts()).await.unwrap();
for i in 0..5 {
queue
.enqueue(json!({"i": i}), EnqueueOpts::default())
.await
.unwrap();
}
for n in [-1i64, 0] {
let got = queue.claim_batch("w1", n).await.unwrap();
assert!(
got.is_empty(),
"claim_batch({n}) must claim nothing (empty Vec, no error)"
);
}
// Nothing was destroyed by the guarded call.
let seen = queue.get_job(1).await.unwrap().unwrap();
assert_eq!(seen.state, JobState::Pending);
// A positive claim on the same state still works and hands out
// exactly the requested extent.
let got = queue.claim_batch("w1", 2).await.unwrap();
assert_eq!(got.len(), 2);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: exactly-once handout under concurrent claims — two
/// workers racing claim on one row (exactly one wins; the POC's 4×4
/// shape at 2×1) and a 6×40 concurrent drain with zero double-claims
/// (the POC-pinned property, engine-tested here; the drain tasks share
/// the store through `Arc<ArcSwapLike>`-free sibling stores — every
/// store instance is a stateless client of the same schema'd tables,
/// so concurrent stores over one schema are the honest multi-worker
/// shape).
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_claims_hand_each_row_out_exactly_once() {
let Some(dsn) = harness_dsn() else {
eprintln!("skip: no harness server");
return;
};
let schema = instance_namer("qx")();
let admin = harness_client().await.unwrap();
// Two workers, one row: exactly one claim wins.
{
let store = open_store(&dsn, test_opts(&schema)).await.unwrap();
let queue = store.queue("solo", opts()).await.unwrap();
let id = queue
.enqueue(json!({"only": true}), EnqueueOpts::default())
.await
.unwrap();
let q1 = store.queue("solo", opts()).await.unwrap();
let q2 = store.queue("solo", opts()).await.unwrap();
let (a, b) = tokio::join!(q1.claim_one("w1"), q2.claim_one("w2"));
let winners = [a.unwrap(), b.unwrap()]
.into_iter()
.flatten()
.collect::<Vec<_>>();
assert_eq!(winners.len(), 1, "exactly one worker got the row");
assert_eq!(winners[0].job().id, id);
store.close();
}
// Many concurrent stores drain a whole queue with no double
// handout (cross-instance claims — each drain task opens its own
// pool connection; the SKIP LOCKED handout is the exactly-once
// property under connection diversity too).
const JOBS: i64 = 40;
const WORKERS: usize = 6;
{
let store = open_store(&dsn, test_opts(&schema)).await.unwrap();
let queue = store.queue("shared_arc", opts()).await.unwrap();
for i in 0..JOBS {
queue
.enqueue(json!({"i": i}), EnqueueOpts::default())
.await
.unwrap();
}
store.close();
let factory = Arc::new(TestFactory {
dsn: dsn.clone(),
schema: schema.clone(),
});
let claimed = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for w in 0..WORKERS {
let factory = factory.clone();
let claimed = claimed.clone();
handles.push(tokio::spawn(async move {
let store = factory.open().await;
let queue = store.queue("shared_arc", opts()).await.unwrap();
let mut got = 0usize;
loop {
let batch = queue
.claim_batch(&format!("w{w}"), 4)
.await
.expect("claim must not error under contention");
if batch.is_empty() {
break;
}
got += batch.len();
// Hold the rows (no ack) — the drain semantics are
// what is under test.
}
claimed.fetch_add(got, Ordering::SeqCst);
got
}));
}
let mut shares = Vec::new();
for h in handles {
shares.push(h.await.unwrap());
}
assert_eq!(claimed.load(Ordering::SeqCst), JOBS as usize);
assert!(
shares.iter().any(|s| *s > 0) && shares.iter().any(|s| *s < JOBS as usize),
"work was shared across workers, not drained by one"
);
// Every job is processing now; no id claimed twice (probed via
// a raw admin count — the bigserial ids are not the loop's
// 1..=n range, other tables' rows interleave).
let raw = open_store(&dsn, test_opts(&schema)).await.unwrap();
let job_table = crate::schema::QualifiedTable {
schema: &schema,
name: crate::schema::tables::JOB,
}
.to_string();
let conn = raw.pool().get().await.unwrap();
let processing: i64 = conn
.query_one(
&format!(
"SELECT count(*) FROM {job_table} WHERE state = 'processing' AND queue = 'shared_arc'"
),
&[],
)
.await
.unwrap()
.get(0);
let distinct: i64 = conn
.query_one(
&format!(
"SELECT count(DISTINCT id) FROM {job_table} WHERE state = 'processing' AND queue = 'shared_arc'"
),
&[],
)
.await
.unwrap()
.get(0);
assert_eq!(processing, JOBS, "every job handed out exactly once");
assert_eq!(distinct, JOBS, "no id claimed twice");
store.close();
raw.close();
}
drop_schema(&admin, &schema).await;
}
/// A per-test store factory for the drain tasks (a fresh store
/// instance over the same schema per call — the honest cross-instance
/// worker shape).
struct TestFactory {
dsn: String,
schema: String,
}
impl TestFactory {
async fn open(&self) -> PgStore {
open_store(&self.dsn, test_opts(&self.schema))
.await
.unwrap()
}
}
/// Acceptance: the uniform validity predicate — once the claim
/// deadline lapsed, all handle ops refuse with `Ok(false)` (heartbeat,
/// ack, retry, fail — never errors), the row untouched, and a reclaim
/// consumes an attempt (ADR-010 §2). Healthy renewal is pinned first:
/// absolute reset, repeatable, `claimed_at` untouched.
#[tokio::test(flavor = "multi_thread")]
async fn lapsed_deadline_refuses_all_handle_ops_and_reclaim_consumes_an_attempt() {
let Some(fx) = Fixture::open(&instance_namer("qv")).await else {
eprintln!("skip: no harness server");
return;
};
// Healthy phase (visibility 300): renewal lands and is repeatable.
let queue = fx.store.queue("healthy", opts()).await.unwrap();
let id = queue
.enqueue(json!({"work": true}), EnqueueOpts::default())
.await
.unwrap();
let handle = queue.claim_one("w1").await.unwrap().unwrap();
let first_deadline = handle.job().claim_expires_at.unwrap();
assert!(handle.heartbeat(5000).await.unwrap(), "renewal lands");
assert!(handle.heartbeat(5000).await.unwrap(), "repeatable");
let renewed = queue.get_job(id).await.unwrap().unwrap();
assert!(
renewed.claim_expires_at.unwrap() >= first_deadline + 4500,
"renewal is an absolute reset from now: {} vs {first_deadline}",
renewed.claim_expires_at.unwrap()
);
assert!(
renewed.claimed_at == handle.job().claimed_at,
"heartbeat never touches claimed_at"
);
assert!(handle.ack().await.unwrap(), "healthy ack lands");
// Lapsed phase (visibility 1): every op refuses with Ok(false).
let queue = fx
.store
.queue(
"lapsed",
QueueOpts {
visibility_timeout_s: 1,
max_attempts: 5,
backoff_base_s: 5,
dead_letter_retention_s: None,
},
)
.await
.unwrap();
let mut ids = Vec::new();
for _ in 0..4 {
ids.push(
queue
.enqueue(json!({"work": true}), EnqueueOpts::default())
.await
.unwrap(),
);
}
let mut claims: Vec<Box<dyn JobHandle>> = Vec::new();
for id in &ids {
let h = queue
.claim_batch("w1", 1)
.await
.unwrap()
.into_iter()
.next()
.expect("claimable");
assert_eq!(h.job().id, *id);
claims.push(h);
}
for id in &ids {
until_deadline_lapsed(queue.as_ref(), *id).await;
}
assert!(
!claims[0].heartbeat(5000).await.unwrap(),
"late heartbeat refuses"
);
assert!(!claims[0].heartbeat(5000).await.unwrap(), "still refuses");
assert!(!claims.remove(1).ack().await.unwrap(), "lapsed ack refuses");
assert!(
!claims
.remove(1)
.retry(Some("boom".to_string()), Some(10))
.await
.unwrap(),
"lapsed retry refuses"
);
assert!(
!claims
.remove(1)
.fail(Some("boom".to_string()))
.await
.unwrap(),
"lapsed fail refuses"
);
// The rows are untouched by the refusals.
for id in &ids {
let job = queue.get_job(*id).await.unwrap().unwrap();
assert_eq!(job.state, JobState::Processing);
assert_eq!(job.attempts, 1);
}
// A reclaim wins and consumes an attempt.
let reclaimed = queue.claim_one("w2").await.unwrap().expect("reclaimable");
assert_eq!(reclaimed.job().attempts, 2, "the reclaim counts an attempt");
assert_eq!(reclaimed.job().worker_id.as_deref(), Some("w2"));
// Post-reclaim, the original claim's ops refuse (the reclaim won
// atomically).
assert!(
!claims[0].heartbeat(5000).await.unwrap(),
"the original claim's ops refuse post-reclaim"
);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Wait until the claimed row's deadline has lapsed — the bounded-wait
/// posture (no tight-race assertions).
async fn until_deadline_lapsed(queue: &dyn alkstore::Queue, id: i64) {
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
loop {
let lapsed = match queue.get_job(id).await {
Ok(Some(job)) => {
job.claim_expires_at.is_some() && job.claim_expires_at.unwrap() < now_unix()
}
_ => false,
};
if lapsed {
return;
}
assert!(tokio::time::Instant::now() < deadline, "never lapsed");
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
/// Acceptance: the backoff curve — `retry(err, None)` computes the
/// equal-jitter exponential `delay ∈ [base·2^(a−1)/2, base·2^(a−1)]`,
/// capped at 1 hour (ADR-010 §3): statistical pin over many draws,
/// tolerance-bounded; the row's `run_at` after an end-to-end retry
/// lands in the range; `Some(d)` overrides.
#[tokio::test(flavor = "multi_thread")]
async fn backoff_curve_lands_in_the_pinned_range() {
// Statistical pin of the pure formula (a = attempts index): base
// 5, a=1 → [3, 5]; draws must stay in the inclusive integer range
// and spread across most of it.
for (base, attempts, lo, hi) in [(5, 1, 3, 5), (5, 2, 5, 10), (5, 3, 10, 20), (1, 1, 1, 1)] {
let mut lo_seen = i64::MAX;
let mut hi_seen = i64::MIN;
for id in 0..400 {
let d = backoff_delay_s(base, attempts, id);
assert!(
(lo..=hi).contains(&d),
"delay {d} outside [{lo}, {hi}] for base={base} a={attempts}"
);
lo_seen = lo_seen.min(d);
hi_seen = hi_seen.max(d);
}
if lo == hi {
assert_eq!(lo_seen, hi_seen, "degenerate range pins to the value");
} else {
assert!(
lo_seen <= lo + (hi - lo) / 2 && hi_seen >= hi - (hi - lo) / 2,
"draws must spread over the range ([{lo_seen}, {hi_seen}] vs [{lo}, {hi}])"
);
}
}
// Cap at 1 hour: a huge doubling exponent still lands in [1800, 3600].
for id in 0..200 {
let d = backoff_delay_s(5, 40, id);
assert!((1800..=3600).contains(&d), "capped delay {d} out of range");
}
// End-to-end: the row's run_at after retry(err, None) is now plus
// a curve draw from the job's stamped base.
let Some(fx) = Fixture::open(&instance_namer("qb")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx
.store
.queue(
"retrying",
QueueOpts {
visibility_timeout_s: 300,
max_attempts: 5,
backoff_base_s: 8,
dead_letter_retention_s: None,
},
)
.await
.unwrap();
let id = queue
.enqueue(json!({"task": 1}), EnqueueOpts::default())
.await
.unwrap();
let handle = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(handle.job().attempts, 1, "a = the post-claim count");
let before = now_unix();
assert!(handle.retry(None, None).await.unwrap());
let job = queue.get_job(id).await.unwrap().unwrap();
assert_eq!(job.state, JobState::Pending);
let span_lo = before - 1 + 8 / 2;
let span_hi = before + 1 + 8;
assert!(
(span_lo..=span_hi).contains(&job.run_at),
"run_at {} must be now + a draw from [4, 8] (base 8, a=1)",
job.run_at
);
// Some(d) overrides: wait out the curve draw, claim again, retry
// with the explicit delay.
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
let override_landed = loop {
match queue.claim_one("w1").await {
Ok(Some(h)) => {
let ok = h
.retry(Some("still broken".to_string()), Some(123))
.await
.unwrap();
assert!(ok, "the override retry lands");
break true;
}
Ok(None) => {}
Err(e) => panic!("claim errored: {e}"),
}
assert!(
tokio::time::Instant::now() < deadline,
"curve never elapsed"
);
tokio::time::sleep(Duration::from_millis(200)).await;
};
assert!(override_landed);
let job = queue.get_job(id).await.unwrap().unwrap();
let now = now_unix();
assert!(
(now + 123 - 2..=now + 123 + 2).contains(&job.run_at) && job.run_at >= before + 120,
"the explicit delay passes through: {} (now {now})",
job.run_at
);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: dead-letter — `retry` at exhausted budget moves the row
/// to dead with the pinned default string (`"max attempts exceeded"` —
/// or the caller's `Some(err)` verbatim), `fail(None)` records
/// `"failed"`, `get_job` sees dead rows with `last_error`/`died_at`
/// carrying the full stamp list (ADR-010 §1/§4; ADR-019 §3).
#[tokio::test(flavor = "multi_thread")]
async fn exhaustion_and_fail_dead_letter_with_the_pinned_strings() {
let Some(fx) = Fixture::open(&instance_namer("qd")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx
.store
.queue(
"dying",
QueueOpts {
visibility_timeout_s: 300,
max_attempts: 2,
backoff_base_s: 5,
dead_letter_retention_s: None,
},
)
.await
.unwrap();
// Exhaustion via retry: claim (attempts=1), retry under budget →
// pending, reclaim (attempts=2 == max) → the retry dead-letters.
let id1 = queue
.enqueue(json!({"n": 1}), EnqueueOpts::default())
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert!(h.retry(None, Some(0)).await.unwrap(), "under budget");
let job = queue.get_job(id1).await.unwrap().unwrap();
assert_eq!(job.state, JobState::Pending);
assert_eq!(job.attempts, 1);
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().attempts, 2, "a reclaim counts too");
assert!(
h.retry(Some("caller string".to_string()), None)
.await
.unwrap(),
"the exhausted-budget retry lands as the dead-letter move"
);
let dead = queue
.get_job(id1)
.await
.unwrap()
.expect("dead rows visible");
assert_eq!(dead.state, JobState::Dead);
assert_eq!(
dead.last_error.as_deref(),
Some("caller string"),
"retry(Some(err)) carries the caller's string on the dead row"
);
assert!(dead.died_at.is_some());
assert_eq!(dead.max_attempts, 2, "the dead row carries its stamps");
assert_eq!(dead.visibility_timeout_s, 300);
assert_eq!(dead.backoff_base_s, 5);
assert_eq!(dead.attempts, 2);
assert!(
dead.claimed_at.is_some(),
"the dead row keeps claim history"
);
assert_eq!(dead.worker_id.as_deref(), Some("w1"));
// retry(None) exhaustion pins the default string.
let id2 = queue
.enqueue(json!({"n": 2}), EnqueueOpts::default())
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().attempts, 1, "a fresh claim is one attempt");
assert!(h.retry(None, Some(0)).await.unwrap(), "attempts 1 < 2");
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().attempts, 2);
assert!(h.retry(None, None).await.unwrap(), "at-budget dead-letters");
let dead = queue.get_job(id2).await.unwrap().unwrap();
assert_eq!(dead.state, JobState::Dead);
assert_eq!(
dead.last_error.as_deref(),
Some(EXHAUSTED_ERROR),
"retry(None) exhaustion records the pinned default string"
);
assert!(dead.died_at.is_some());
drop_schema(&fx.admin, &fx.schema).await;
// fail(None) pins "failed"; fail(Some(e)) carries the caller's.
let Some(fx) = Fixture::open(&instance_namer("qf")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("dying", opts()).await.unwrap();
let id = queue
.enqueue(json!({"n": 3}), EnqueueOpts::default())
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(None).await.unwrap(), "fail lands");
let dead = queue.get_job(id).await.unwrap().unwrap();
assert_eq!(dead.state, JobState::Dead);
assert_eq!(
dead.last_error.as_deref(),
Some(FAILED_ERROR),
"fail(None) records the pinned default string"
);
assert!(dead.died_at.is_some());
let id = queue
.enqueue(json!({"n": 4}), EnqueueOpts::default())
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(Some("explicit reason".to_string())).await.unwrap());
let dead = queue.get_job(id).await.unwrap().unwrap();
assert_eq!(dead.last_error.as_deref(), Some("explicit reason"));
// The pinning constants are the contract's defaults.
assert_eq!(EXHAUSTED_ERROR, "max attempts exceeded");
assert_eq!(FAILED_ERROR, "failed");
assert_eq!(EXPIRED_ERROR, "expired");
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: the pre-claim dead-letter sweep — a reclaimable row
/// at-or-past its budget is dead-lettered by the next claim (the
/// substrate's D-12 shape: a worker dying right after its last allowed
/// claim dead-letters, not strands — ADR-010 §2/§3).
#[tokio::test(flavor = "multi_thread")]
async fn pre_claim_sweep_dead_letters_exhausted_reclaimables() {
let Some(fx) = Fixture::open(&instance_namer("q12")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx
.store
.queue(
"d12",
QueueOpts {
visibility_timeout_s: 1,
max_attempts: 1,
backoff_base_s: 5,
dead_letter_retention_s: None,
},
)
.await
.unwrap();
let spent = queue
.enqueue(json!({"k": 1}), EnqueueOpts::default())
.await
.unwrap();
let fresh = queue
.enqueue(json!({"k": 2}), EnqueueOpts::default())
.await
.unwrap();
// Claim the budget-spent row (attempts 1 == max 1) and let its
// deadline lapse: it is now a reclaimable-past-budget row.
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().id, spent);
until_deadline_lapsed(queue.as_ref(), spent).await;
// The next claim dead-letters the spent row (the pre-claim sweep)
// and hands out the fresh one.
let h = queue
.claim_one("w2")
.await
.unwrap()
.expect("fresh claimable");
assert_eq!(h.job().id, fresh);
let dead = queue.get_job(spent).await.unwrap().expect("dead visible");
assert_eq!(dead.state, JobState::Dead);
assert_eq!(
dead.last_error.as_deref(),
Some(EXHAUSTED_ERROR),
"the exhausted reclaimable dead-letters (not strands)"
);
assert!(dead.died_at.is_some());
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: claim ordering — `priority DESC`, ready-time ASC,
/// enqueue order (ADR-010 §1).
#[tokio::test(flavor = "multi_thread")]
async fn claim_ordering_is_priority_desc_run_at_then_enqueue_order() {
let Some(fx) = Fixture::open(&instance_namer("qo")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("ordered", opts()).await.unwrap();
let now = now_unix();
let a = queue
.enqueue(
json!("a"),
EnqueueOpts {
run_at: Some(now),
..Default::default()
},
)
.await
.unwrap();
let b = queue
.enqueue(
json!("b"),
EnqueueOpts {
run_at: Some(now),
priority: 5,
..Default::default()
},
)
.await
.unwrap();
let c = queue
.enqueue(
json!("c"),
EnqueueOpts {
run_at: Some(now),
priority: 5,
..Default::default()
},
)
.await
.unwrap();
let d = queue
.enqueue(
json!("d"),
EnqueueOpts {
run_at: Some(now - 10),
priority: 5,
..Default::default()
},
)
.await
.unwrap();
for _ in 0..3 {
queue.claim_one("w").await.unwrap().unwrap();
}
// After 3 claims: d, b, c claimed (priority DESC; among equal
// priority, run_at ASC → d's earlier ready time, then enqueue
// order b then c); a (priority 0) is last, unclaimed.
let ja = queue.get_job(a).await.unwrap().unwrap();
assert_eq!(ja.state, JobState::Pending, "the lower-priority job last");
assert_eq!(ja.worker_id, None);
for id in [b, c, d] {
let j = queue.get_job(id).await.unwrap().unwrap();
assert_eq!(j.state, JobState::Processing, "claimed in order");
assert_eq!(j.worker_id.as_deref(), Some("w"));
}
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: `cancel` is unconditional (either state, any holder),
/// `ack_batch` applies the per-id validity predicate without a worker
/// filter (partial success ordinary), missing ids silent (ADR-010 §1;
/// ADR-019 §1).
#[tokio::test(flavor = "multi_thread")]
async fn cancel_and_ack_batch_behaviors() {
let Some(fx) = Fixture::open(&instance_namer("qc")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("ops", opts()).await.unwrap();
let pending = queue
.enqueue(json!({"p": 1}), EnqueueOpts::default())
.await
.unwrap();
let processing = queue
.enqueue(json!({"p": 2}), EnqueueOpts::default())
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
let claimed_first = h.job().id;
assert!(claimed_first == pending || claimed_first == processing);
let still_live = if claimed_first == pending {
processing
} else {
pending
};
assert!(queue.cancel(claimed_first).await.unwrap());
assert!(queue.cancel(still_live).await.unwrap(), "unconditional");
assert!(!queue.cancel(still_live).await.unwrap(), "gone = false");
assert!(queue.get_job(still_live).await.unwrap().is_none());
assert!(!h.ack().await.unwrap(), "the holder's next op refuses");
// ack_batch: per-id predicate, no worker filter, count returned.
let a = queue
.enqueue(json!({"a": 1}), EnqueueOpts::default())
.await
.unwrap();
let b = queue
.enqueue(json!({"b": 1}), EnqueueOpts::default())
.await
.unwrap();
let c = queue
.enqueue(json!({"c": 1}), EnqueueOpts::default())
.await
.unwrap();
let ha = queue.claim_one("w1").await.unwrap().unwrap();
let hb = queue.claim_one("w2").await.unwrap().unwrap();
assert_eq!(ha.job().id, a, "FIFO: the first-enqueued row claims first");
assert_eq!(hb.job().id, b);
assert!(queue.cancel(c).await.unwrap());
// a is w1's claim, b is w2's — both count (no worker filter); the
// cancelled c and the missing id do not.
let count = queue.ack_batch(&[a, b, c, 999_999]).await.unwrap();
assert_eq!(count, 2, "only the valid ids count; partial is ordinary");
assert!(queue.get_job(a).await.unwrap().is_none());
assert!(queue.get_job(b).await.unwrap().is_none());
// An empty batch is vacuously zero.
assert_eq!(queue.ack_batch(&[]).await.unwrap(), 0);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: `sweep_expired` moves every past-`expires_at` row (both
/// states — the no-stranded-rows property, ADR-010 §5) to dead with
/// `"expired"`, and returns the moved count; unexpired rows are
/// untouched; no leader lock required (the bare call just runs).
#[tokio::test(flavor = "multi_thread")]
async fn sweep_expired_moves_both_states_and_applies_retention() {
let Some(fx) = Fixture::open(&instance_namer("qs")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("sweeping", opts()).await.unwrap();
let now = now_unix();
// Expired pending + expired processing + unexpired: only the two
// expired rows move; the count is 2. The claim takes the head (the
// expired-pending row, FIFO) then the honest cross-connection probe
// back-dates both rows' expiry so the both-states move is
// exercised deterministically.
let expired_pending = queue
.enqueue(
json!({"k": 1}),
EnqueueOpts {
expires: Some(3600),
..Default::default()
},
)
.await
.unwrap();
let expired_processing = queue
.enqueue(
json!({"k": 2}),
EnqueueOpts {
expires: Some(3600),
..Default::default()
},
)
.await
.unwrap();
let unexpired = queue
.enqueue(
json!({"k": 3}),
EnqueueOpts {
expires: Some(3600),
..Default::default()
},
)
.await
.unwrap();
let h = queue.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().id, expired_pending);
backdate_expires(&fx.admin, &fx.schema, expired_pending).await;
backdate_expires(&fx.admin, &fx.schema, expired_processing).await;
let moved = queue.sweep_expired().await.unwrap();
assert_eq!(moved, 2, "both expired rows moved (pending + processing)");
for id in [expired_pending, expired_processing] {
let dead = queue.get_job(id).await.unwrap().expect("moved to dead");
assert_eq!(dead.state, JobState::Dead);
assert_eq!(dead.last_error.as_deref(), Some(EXPIRED_ERROR));
assert!(dead.died_at.is_some() && dead.died_at.unwrap() >= now);
}
let live = queue.get_job(unexpired).await.unwrap().unwrap();
assert_eq!(live.state, JobState::Pending, "unexpired rows untouched");
// A bare sweep on an empty-expiry queue is 0 and not an error.
assert_eq!(queue.sweep_expired().await.unwrap(), 0);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: retention — dead rows past their
/// `dead_letter_retention_s` stamp are deleted by the sweep (the only
/// sweeper dead rows ever have), rows with `None` retention live
/// forever; the deleted count reports through (ADR-010 §4/§5).
#[tokio::test(flavor = "multi_thread")]
async fn sweep_enforces_dead_letter_retention_incl_forever_none() {
let Some(fx) = Fixture::open(&instance_namer("qr")).await else {
eprintln!("skip: no harness server");
return;
};
let keeping = fx
.store
.queue(
"keeping",
QueueOpts {
dead_letter_retention_s: None,
..opts()
},
)
.await
.unwrap();
let expiring = fx
.store
.queue(
"expiring",
QueueOpts {
dead_letter_retention_s: Some(60),
..opts()
},
)
.await
.unwrap();
let keep_id = keeping
.enqueue(json!({"k": 1}), EnqueueOpts::default())
.await
.unwrap();
let expire_id = expiring
.enqueue(json!({"k": 2}), EnqueueOpts::default())
.await
.unwrap();
let h = keeping.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(None).await.unwrap());
let h = expiring.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(None).await.unwrap());
// Move the expired-retention row's death back past its TTL (the
// honest probe: retention reads died_at vs now).
fx.admin
.execute(
&format!(
"UPDATE {} SET died_at = $2 WHERE id = $1",
crate::schema::QualifiedTable {
schema: &fx.schema,
name: crate::schema::tables::DEAD,
}
),
&[&expire_id, &(now_unix() - 3600)],
)
.await
.unwrap();
// The sweep on the expiring queue deletes the TTL'd row; the
// keeping queue's row (retention None) survives.
assert_eq!(expiring.sweep_expired().await.unwrap(), 1);
assert!(expiring.get_job(expire_id).await.unwrap().is_none());
assert_eq!(keeping.sweep_expired().await.unwrap(), 0);
assert!(
keeping.get_job(keep_id).await.unwrap().is_some(),
"retention None = dead rows live forever"
);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: the wake-driven claim loop pinned end-to-end — enqueue
/// → pg_notify on the queue's wake channel → the LISTEN forwarder fan
/// → cross-connection wake → the claim (the LISTEN-driven default
/// consumption posture, ADR-004; the contract claim ops are
/// pull-shaped — a consumer's loop listens and claims on wake; the
/// re-poll safety net is the loop's fallback, pinned here only as the
/// pull-shaped surface's honest shape).
#[tokio::test(flavor = "multi_thread")]
async fn wake_driven_claim_loop_end_to_end() {
let Some(fx) = Fixture::open(&instance_namer("qw")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("wakeup", opts()).await.unwrap();
let mut receiver = fx.store.listen("wakeup").await.unwrap();
// Enqueue on the handle (a pooled connection, not the listener's):
// the row lands, the wake notifies on the queue's wake channel.
let id = queue
.enqueue(json!({"work": true}), EnqueueOpts::default())
.await
.unwrap();
// The wake arrives cross-connection (the enqueue's NOTIFY fired at
// its implicit transaction's commit and the forwarder forwarded
// it).
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
loop {
match receiver.recv_timeout(Duration::from_millis(300)).await {
Ok(Some(wake)) => {
assert_eq!(wake.channel, "wakeup");
break;
}
Ok(None) => {}
Err(e) => panic!("receiver errored: {e}"),
}
assert!(
tokio::time::Instant::now() < deadline,
"the enqueue's wake never arrived"
);
}
// The claim lands on the consumer's own pooled connection.
let handle = queue.claim_one("w1").await.unwrap().expect("claimable");
assert_eq!(handle.job().id, id);
assert!(handle.ack().await.unwrap());
assert!(queue.get_job(id).await.unwrap().is_none());
// A poll-only consumer (the safety net's shape — no LISTEN wake at
// all, just a claim loop) still lands the same work: a second
// enqueue is claimed by an unreactive loop alone.
let second = queue
.enqueue(json!({"work": 2}), EnqueueOpts::default())
.await
.unwrap();
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
let claimed = loop {
if let Some(h) = queue.claim_one("w2").await.unwrap() {
break h;
}
assert!(
tokio::time::Instant::now() < deadline,
"the poll-only loop never landed the row"
);
tokio::time::sleep(Duration::from_millis(50)).await;
};
assert_eq!(claimed.job().id, second);
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: entry-point validation — the constructor rejects empty
/// and reserved names (`InvalidName`/`ReservedName`), claims reject
/// empty `worker_id` (ADR-008 §4; ADR-019 §2) — nothing round-trips.
#[tokio::test(flavor = "multi_thread")]
async fn validation_rejects_bad_names() {
let Some(fx) = Fixture::open(&instance_namer("qval")).await else {
eprintln!("skip: no harness server");
return;
};
match fx.store.queue("", opts()).await {
Err(alkstore::Error::InvalidName { .. }) => {}
_other => panic!("empty queue name must be InvalidName"),
}
match fx.store.queue(alkstore::RESERVED_PREFIX, opts()).await {
Err(alkstore::Error::ReservedName { .. }) => {}
_other => panic!("reserved queue name must be ReservedName"),
}
let queue = fx.store.queue("valid", opts()).await.unwrap();
match queue.claim_batch("", 1).await {
Err(alkstore::Error::InvalidName { .. }) => {}
_other => panic!("empty worker_id must be InvalidName"),
}
// The non-positive extent guards to the empty value even under a
// valid worker (ADR-023 §2).
let got = queue.claim_batch("w1", 0).await.unwrap();
assert!(got.is_empty());
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: closed-store fail-closed on all queue ops (the wave-3
/// shape) — the constructor, enqueue, claims, ack_batch, cancel,
/// get_job, sweep, heartbeat.
#[tokio::test(flavor = "multi_thread")]
async fn closed_store_fails_closed_on_all_queue_ops() {
let Some(fx) = Fixture::open(&instance_namer("qcl")).await else {
eprintln!("skip: no harness server");
return;
};
let queue = fx.store.queue("valid", opts()).await.unwrap();
let id = queue
.enqueue(json!({}), EnqueueOpts::default())
.await
.unwrap();
let handle = queue.claim_one("w1").await.unwrap().unwrap();
fx.store.close();
let err = fx
.store
.queue("valid", opts())
.await
.err()
.expect("closed store refuses construction");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.enqueue(json!({}), EnqueueOpts::default())
.await
.expect_err("closed store refuses enqueue");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.claim_batch("w1", 1)
.await
.err()
.expect("closed store refuses claim");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.ack_batch(&[id])
.await
.expect_err("closed store refuses ack_batch");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.cancel(id)
.await
.expect_err("closed store refuses cancel");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.get_job(id)
.await
.expect_err("closed store refuses get_job");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = queue
.sweep_expired()
.await
.expect_err("closed store refuses sweep");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
let err = handle
.heartbeat(100)
.await
.expect_err("closed store refuses heartbeat");
assert!(matches!(err, alkstore::Error::Database(_)), "got {err:?}");
drop_schema(&fx.admin, &fx.schema).await;
}
/// Acceptance: queue scoping — two handles on two queues are isolated:
/// a claim on one queue never hands out another queue's row (the
/// queue-scoped claim predicate), `get_job` is queue-scoped, and
/// cancel/ack_batch/sweep are queue-scoped (the seam's scoping rule at
/// the auto-commit shape).
#[tokio::test(flavor = "multi_thread")]
async fn queue_handles_are_name_scoped() {
let Some(fx) = Fixture::open(&instance_namer("qsc")).await else {
eprintln!("skip: no harness server");
return;
};
let q1 = fx.store.queue("one", opts()).await.unwrap();
let q2 = fx.store.queue("two", opts()).await.unwrap();
let id1 = q1
.enqueue(json!({"q": 1}), EnqueueOpts::default())
.await
.unwrap();
let id2 = q2
.enqueue(json!({"q": 2}), EnqueueOpts::default())
.await
.unwrap();
// q1's claim only ever sees q1's rows; the second claim is
// no-work.
let h = q1.claim_one("w1").await.unwrap().unwrap();
assert_eq!(h.job().id, id1);
assert!(q1.claim_one("w1").await.unwrap().is_none());
// q2 still hands out its own.
let h2 = q2.claim_one("w2").await.unwrap().unwrap();
assert_eq!(h2.job().id, id2);
// get_job is queue-scoped: q1 cannot see q2's row.
assert!(q1.get_job(id2).await.unwrap().is_none());
assert!(q2.get_job(id1).await.unwrap().is_none());
assert!(q1.get_job(id1).await.unwrap().is_some());
// Cancel/ack_batch are queue-scoped too.
assert!(!q1.cancel(id2).await.unwrap(), "cancel is q-scoped");
assert!(q2.cancel(id2).await.unwrap());
assert_eq!(q1.ack_batch(&[id2]).await.unwrap(), 0);
drop_schema(&fx.admin, &fx.schema).await;
}
/// The dead-letter string constants are the contract's pinned values
/// (the engine-default strings land identically on both engines — the
/// backlog row's wording; server-less pin, the behavioral rows above
/// exercise them through the harness).
#[test]
fn dead_letter_strings_match_the_pinned_defaults() {
assert_eq!(EXHAUSTED_ERROR, "max attempts exceeded");
assert_eq!(FAILED_ERROR, "failed");
assert_eq!(EXPIRED_ERROR, "expired");
}
/// The backoff formula's unit pins (server-less): the curve's
/// attempt-index clamping and the pure formula's range behavior — the
/// statistical rows run harness-side.
#[test]
fn backoff_formula_unit_pins() {
// attempts 1 == the first retry draws from [ceil(base/2), base].
for base in [1, 2, 5, 100] {
for id in 0..200 {
let d = backoff_delay_s(base, 1, id);
assert!(((base + 1) / 2..=base).contains(&d), "base {base} d {d}");
}
}
// A zero attempts index clamps to the first-retry attempt (the
// formula's `max(1)` floor — the draw's subsecond input varies, so
// pin the *range* the clamp produces, not call equality).
for id in 0..200 {
let d = backoff_delay_s(5, 0, id);
assert!(
(3..=5).contains(&d),
"clamped-attempts draw {d} out of range"
);
}
// The cap saturates the doubling arithmetic (no overflow at any
// attempt count).
for attempts in [30, 31, 60, 10_000] {
for id in 0..100 {
let d = backoff_delay_s(1, attempts, id);
assert!((1..=3600).contains(&d));
}
}
}
/// Acceptance: the server-side stamp-arithmetic guard (ADR-023 §2's
/// 2026-10-11 annotation) — extreme i64 stamps and per-call arguments
/// are values whose server-side sums saturate to the i64 domain, never
/// "bigint out of range" failures, and the sticky-poison property (one
/// extreme-stamped row failing every later claim that touches it) is
/// retired. Pinned per site against the live server: an
/// `visibility_timeout_s = i64::MAX` row claims to the saturated
/// `i64::MAX` deadline and neighbors claim unaffected; extreme
/// heartbeat extends renew to the saturated edge and the representable
/// `now + i64::MIN` sum rides exact (Rust `saturating_add` parity),
/// the past deadline voiding through the validity predicate on the
/// next op; the retry override reschedules `Some(i64::MAX)` to
/// `i64::MAX` (and the negative-extreme delay rides exact to
/// ready-now); the retention `i64::MIN` arm sweeps every dead row
/// (`sat(now − MIN) = i64::MAX`) while the `Some(i64::MAX)` arm
/// deletes nothing.
#[tokio::test(flavor = "multi_thread")]
async fn extreme_stamps_saturate_and_no_longer_poison_queue_ops() {
let Some(fx) = Fixture::open(&instance_namer("qext")).await else {
eprintln!("skip: no harness server");
return;
};
let now = now_unix();
// Claim + heartbeat over an i64::MAX-stamped row: the claim UPDATE
// sums now + visibility_timeout_s server-side (pre-guard this
// failed sticky per row) — it must land, deadline at the saturated
// edge.
let queue = fx
.store
.queue(
"neverlaps",
QueueOpts {
visibility_timeout_s: i64::MAX,
..opts()
},
)
.await
.unwrap();
let id = queue
.enqueue(json!({"k": 1}), EnqueueOpts::default())
.await
.unwrap();
let h = queue
.claim_one("w1")
.await
.unwrap()
.expect("the extreme-stamped row claims (no overflow failure)");
assert_eq!(h.job().id, id);
assert_eq!(
h.job().claim_expires_at,
Some(i64::MAX),
"the claim deadline saturates to the i64::MAX edge"
);
// A neighboring normal queue's row claims unaffected (the poison
// was per-row, not per-store; the other-rows-unaffected arm).
let normal = fx.store.queue("normal", opts()).await.unwrap();
let normal_id = normal
.enqueue(json!({"k": 2}), EnqueueOpts::default())
.await
.unwrap();
let nh = normal
.claim_one("w1")
.await
.unwrap()
.expect("normal rows still claim");
assert_eq!(nh.job().id, normal_id);
assert!(
nh.job().claim_expires_at.unwrap() >= now + 300,
"normal rows still claim (unaffected by the extreme row)"
);
assert!(nh.ack().await.unwrap(), "the normal row acks");
// Heartbeats on the extreme row land: a normal renewal, then the
// extreme extend renewing to the saturated edge.
assert!(h.heartbeat(120).await.unwrap(), "heartbeat renews");
let renewed = queue.get_job(id).await.unwrap().unwrap();
assert!(
renewed.claim_expires_at.unwrap() > now,
"renewal is an absolute reset from now: {:?}",
renewed.claim_expires_at
);
assert!(
h.heartbeat(i64::MAX).await.unwrap(),
"the i64::MAX extend renews (no overflow failure)"
);
let renewed = queue.get_job(id).await.unwrap().unwrap();
assert_eq!(
renewed.claim_expires_at,
Some(i64::MAX),
"sat(now + i64::MAX) = i64::MAX"
);
// The negative extreme: now + i64::MIN is representable, so the
// guard rides it exact (matching Rust `saturating_add` — no
// gratuitous clamp) and the past deadline it stamps voids the
// claim through the validity predicate on the next op.
let before_min = now_unix();
assert!(
h.heartbeat(i64::MIN).await.unwrap(),
"the i64::MIN extend renews"
);
let renewed = queue.get_job(id).await.unwrap().unwrap();
let offset = renewed.claim_expires_at.unwrap().wrapping_sub(i64::MIN);
assert!(
(before_min..=before_min + 2).contains(&offset),
"sat(now + i64::MIN) rides exact: deadline {:?} vs before {before_min}",
renewed.claim_expires_at
);
assert!(
!h.heartbeat(60).await.unwrap(),
"the past saturated deadline voids the claim on the next op"
);
// Retry override extremes: `Some(i64::MIN)` rides exact to
// ready-now (the row reclaims immediately), `Some(i64::MAX)`
// reschedules to the ready-~never saturated edge.
let id2 = normal
.enqueue(json!({"k": 5}), EnqueueOpts::default())
.await
.unwrap();
let h = normal.claim_one("w1").await.unwrap().unwrap();
let before = now_unix();
assert!(
h.retry(Some("boom".to_string()), Some(i64::MIN))
.await
.unwrap(),
"the negative-extreme delay retry lands"
);
let job = normal.get_job(id2).await.unwrap().unwrap();
assert_eq!(job.state, JobState::Pending);
let offset = job.run_at.wrapping_sub(i64::MIN);
assert!(
(before..=before + 2).contains(&offset),
"sat(now + i64::MIN) rides exact: run_at {} vs now {before}",
job.run_at
);
let h = normal
.claim_one("w1")
.await
.unwrap()
.expect("the ready-now row reclaims");
assert_eq!(h.job().id, id2);
assert!(
h.retry(Some("boom".to_string()), Some(i64::MAX))
.await
.unwrap(),
"the positive-extreme delay retry lands"
);
let job = normal.get_job(id2).await.unwrap().unwrap();
assert_eq!(job.run_at, i64::MAX, "sat(now + i64::MAX) = i64::MAX");
// Retention arms: `Some(i64::MIN)` sweeps every dead row
// (sat(now − MIN) = i64::MAX, the sweep-all edge — pre-guard this
// sweep failed sticky with the overflow error);
// `Some(i64::MAX)` deletes nothing (sat(now − MAX) = i64::MIN).
let sweepall = fx
.store
.queue(
"sweepall",
QueueOpts {
dead_letter_retention_s: Some(i64::MIN),
..opts()
},
)
.await
.unwrap();
let swept_id = sweepall
.enqueue(json!({"k": 3}), EnqueueOpts::default())
.await
.unwrap();
let h = sweepall.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(None).await.unwrap(), "the dead row exists");
assert!(
sweepall.sweep_expired().await.unwrap() >= 1,
"the retention i64::MIN sweep lands (no overflow failure)"
);
assert!(
sweepall.get_job(swept_id).await.unwrap().is_none(),
"sat(now − i64::MIN) = i64::MAX sweeps the dead row"
);
let sweepnone = fx
.store
.queue(
"sweepnone",
QueueOpts {
dead_letter_retention_s: Some(i64::MAX),
..opts()
},
)
.await
.unwrap();
let kept_id = sweepnone
.enqueue(json!({"k": 4}), EnqueueOpts::default())
.await
.unwrap();
let h = sweepnone.claim_one("w1").await.unwrap().unwrap();
assert!(h.fail(None).await.unwrap());
assert_eq!(
sweepnone.sweep_expired().await.unwrap(),
0,
"sat(now − i64::MAX) = i64::MIN deletes nothing"
);
assert!(
sweepnone.get_job(kept_id).await.unwrap().is_some(),
"the retention i64::MAX dead row survives"
);
drop_schema(&fx.admin, &fx.schema).await;
}