use std::time::Duration;
use tokio::sync::{Mutex, MutexGuard};
use uuid::Uuid;
use super::dialect::JobPool;
use super::{JobModel, JobRequest, Jobs};
static TABLE: Mutex<()> = Mutex::const_new(());
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct Marker {
pub(crate) n: i64,
}
pub(crate) const MARKER: JobModel<Marker> = JobModel::new("arcature.test.marker", 1, 3);
pub(crate) struct Queue {
pool: JobPool,
_exclusive: MutexGuard<'static, ()>,
}
impl Queue {
pub(crate) fn pool(&self) -> &JobPool {
&self.pool
}
}
pub(crate) const WORKERS: usize = 8;
pub(crate) const JOBS: usize = 40;
const CONNECTIONS: u32 = WORKERS as u32 + 2;
pub(crate) async fn queue() -> Option<Queue> {
use crate::test_kit::database::{
REQUIRE_TEST_DB_VAR, TEST_DB_URL_VAR, TestDatabaseError, test_database_required,
test_database_url,
};
let url = match test_database_url() {
Ok(url) => url,
Err(TestDatabaseError::NotConfigured) => {
assert!(
!test_database_required(),
"{REQUIRE_TEST_DB_VAR} is set, so {TEST_DB_URL_VAR} has to be too"
);
return None;
}
Err(error) => panic!("{error}"),
};
let exclusive = TABLE.lock().await;
let pool = sqlx::pool::PoolOptions::<crate::database::Driver>::new()
.max_connections(CONNECTIONS)
.acquire_timeout(Duration::from_secs(30))
.connect(&url)
.await
.unwrap_or_else(|error| panic!("connect to the test database: {error}"));
super::migrate::apply(&pool)
.await
.expect("apply the jobs migrations");
sqlx::query("DELETE FROM arcature_jobs")
.execute(&pool)
.await
.expect("empty arcature_jobs");
Some(Queue {
pool,
_exclusive: exclusive,
})
}
pub(crate) async fn enqueue(pool: &JobPool, count: usize) -> Vec<Uuid> {
let jobs = Jobs::new(pool.clone());
let mut ids = Vec::with_capacity(count);
for n in 0..count {
let request = JobRequest::new(&MARKER, &Marker { n: n as i64 }).expect("build the request");
ids.push(jobs.enqueue(&request).await.expect("enqueue").id);
}
ids
}
pub(crate) async fn rows(pool: &JobPool) -> Vec<(Uuid, String, i32)> {
use sqlx::Row;
sqlx::query("SELECT id, status, attempts FROM arcature_jobs")
.fetch_all(pool)
.await
.expect("read arcature_jobs")
.iter()
.map(|row| {
(
row.try_get("id").expect("id"),
row.try_get("status").expect("status"),
row.try_get("attempts").expect("attempts"),
)
})
.collect()
}
pub(crate) async fn row(pool: &JobPool, id: Uuid) -> (String, i32, Option<Uuid>) {
use sqlx::Row;
let sql = format!(
"SELECT status, attempts, claim_token FROM arcature_jobs WHERE id = {}",
crate::database::dialect::placeholder(1)
);
let row = sqlx::query(sqlx::AssertSqlSafe(sql))
.bind(id)
.fetch_one(pool)
.await
.expect("read one job row");
(
row.try_get("status").expect("status"),
row.try_get("attempts").expect("attempts"),
row.try_get("claim_token").expect("claim_token"),
)
}
pub(crate) async fn drain_concurrently(pool: &JobPool, batch: i64) -> Vec<Vec<Uuid>> {
let mut claimers = Vec::with_capacity(WORKERS);
for worker in 0..WORKERS {
let pool = pool.clone();
claimers.push(tokio::spawn(async move {
let id = format!("worker-{worker}");
let mut mine = Vec::new();
loop {
let claimed = super::claim::claim_jobs(&pool, &id, Duration::from_secs(60), batch)
.await
.expect("claim a batch");
if claimed.is_empty() {
break mine;
}
mine.extend(claimed.iter().map(|job| job.id));
}
}));
}
let mut claimed = Vec::with_capacity(WORKERS);
for claimer in claimers {
claimed.push(claimer.await.expect("a claimer panicked"));
}
claimed
}
pub(crate) async fn assert_claimed_exactly_once(
pool: &JobPool,
enqueued: &[Uuid],
claimed: &[Vec<Uuid>],
) {
let mut seen: std::collections::HashMap<Uuid, Vec<usize>> = std::collections::HashMap::new();
for (worker, batch) in claimed.iter().enumerate() {
for &id in batch {
seen.entry(id).or_default().push(worker);
}
}
let contested: Vec<_> = seen.iter().filter(|(_, by)| by.len() > 1).collect();
assert!(
contested.is_empty(),
"a job was handed to more than one worker: {contested:?}"
);
for id in enqueued {
assert!(seen.contains_key(id), "job {id} was never claimed");
}
assert_eq!(
seen.len(),
enqueued.len(),
"claimed {} distinct jobs, enqueued {}",
seen.len(),
enqueued.len()
);
for (id, status, attempts) in rows(pool).await {
assert_eq!(status, "running", "job {id} is {status}, not running");
assert_eq!(attempts, 1, "job {id} was claimed {attempts} times");
}
}