changes
All checks were successful
CI / ci (push) Successful in 19m38s

This commit is contained in:
2026-08-26 20:55:30 +02:00
parent a557c183e9
commit 23d052278a
523 changed files with 24448 additions and 2005 deletions

View File

@@ -0,0 +1,213 @@
use sqlx::sqlite::SqlitePoolOptions;
use domain::entry::MoodEntryId;
use domain::job::{JobKind, JobStatus, JobSubject};
use domain::ports::{JobQueueCommandPort, JobQueueQueryPort};
use sqlite::repositories::SqliteJobQueueRepository;
const KIND: JobKind = JobKind::BackfillRecordingIdentity;
async fn a_queue() -> (sqlx::SqlitePool, SqliteJobQueueRepository) {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlite::run_migrations(&pool).await.unwrap();
(pool.clone(), SqliteJobQueueRepository::new(pool))
}
fn about(entry_id: &MoodEntryId) -> JobSubject {
JobSubject::Entry(entry_id.clone())
}
async fn status_of(pool: &sqlx::SqlitePool) -> Vec<(String, i64, Option<String>)> {
sqlx::query_as("SELECT status, attempts, last_error FROM jobs ORDER BY enqueued_at")
.fetch_all(pool)
.await
.unwrap()
}
#[tokio::test]
async fn the_same_work_cannot_be_queued_twice() {
let (pool, queue) = a_queue().await;
let subject = about(&MoodEntryId::generate());
assert!(queue.enqueue(KIND, &subject).await.unwrap());
assert!(!queue.enqueue(KIND, &subject).await.unwrap());
let rows: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM jobs")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(rows.0, 1);
}
#[tokio::test]
async fn claiming_marks_a_job_as_running_so_another_worker_leaves_it_alone() {
let (pool, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
let claimed = queue.claim(KIND, 10).await.unwrap();
let claimed_again = queue.claim(KIND, 10).await.unwrap();
assert_eq!(claimed.len(), 1);
assert!(
claimed_again.is_empty(),
"a running job is not claimed twice"
);
assert_eq!(status_of(&pool).await[0].0, JobStatus::Running.name());
}
#[tokio::test]
async fn claiming_is_bounded_and_takes_the_oldest_first() {
let (_, queue) = a_queue().await;
for _ in 0..5 {
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
}
let claimed = queue.claim(KIND, 2).await.unwrap();
assert_eq!(claimed.len(), 2);
}
#[tokio::test]
async fn finishing_a_job_removes_it() {
let (pool, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
let claimed = queue.claim(KIND, 1).await.unwrap();
queue.finish(claimed[0].id()).await.unwrap();
assert!(status_of(&pool).await.is_empty());
}
#[tokio::test]
async fn releasing_a_job_counts_the_attempt_and_keeps_the_reason() {
let (pool, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
let claimed = queue.claim(KIND, 1).await.unwrap();
queue
.release(claimed[0].id(), "musicbrainz timed out")
.await
.unwrap();
let stored = status_of(&pool).await;
assert_eq!(stored[0].0, JobStatus::Pending.name());
assert_eq!(stored[0].1, 1);
assert_eq!(stored[0].2.as_deref(), Some("musicbrainz timed out"));
assert_eq!(
queue.claim(KIND, 1).await.unwrap().len(),
1,
"and it is claimable again"
);
}
#[tokio::test]
async fn an_exhausted_job_is_never_claimed_but_can_still_be_seen() {
let (_, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
let claimed = queue.claim(KIND, 1).await.unwrap();
queue.exhaust(claimed[0].id(), "gave up").await.unwrap();
assert!(queue.claim(KIND, 10).await.unwrap().is_empty());
let visible = queue.find_exhausted(10).await.unwrap();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].last_error(), Some("gave up"));
}
#[tokio::test]
async fn a_job_left_running_by_a_dead_worker_becomes_claimable_again() {
let (_, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
queue.claim(KIND, 1).await.unwrap();
let reclaimed = queue.reclaim_stalled(0).await.unwrap();
assert_eq!(reclaimed, 1);
assert_eq!(queue.claim(KIND, 1).await.unwrap().len(), 1);
}
#[tokio::test]
async fn a_job_still_being_worked_on_is_not_reclaimed() {
let (_, queue) = a_queue().await;
queue
.enqueue(KIND, &about(&MoodEntryId::generate()))
.await
.unwrap();
queue.claim(KIND, 1).await.unwrap();
let reclaimed = queue.reclaim_stalled(300).await.unwrap();
assert_eq!(reclaimed, 0, "five minutes have not passed");
}
#[tokio::test]
async fn a_row_of_a_kind_this_build_does_not_know_is_never_claimed() {
let (pool, queue) = a_queue().await;
sqlx::query(
"INSERT INTO jobs (id, kind, subject, status, attempts, last_error, enqueued_at, updated_at)
VALUES (?, 'summonRain', ?, 'pending', 0, NULL, ?, ?)",
)
.bind(uuid::Uuid::new_v4().to_string())
.bind(uuid::Uuid::new_v4().to_string())
.bind(chrono::Utc::now().to_rfc3339())
.bind(chrono::Utc::now().to_rfc3339())
.execute(&pool)
.await
.unwrap();
assert!(queue.claim(KIND, 10).await.unwrap().is_empty());
}
#[tokio::test]
async fn a_job_this_build_cannot_read_is_given_up_on_rather_than_claimed_forever() {
let (pool, queue) = a_queue().await;
sqlx::query(
"INSERT INTO jobs (id, kind, subject, status, attempts, last_error, enqueued_at, updated_at)
VALUES (?, 'backfillRecordingIdentity', ?, 'pending', 0, NULL, 'the day before yesterday', ?)",
)
.bind(uuid::Uuid::new_v4().to_string())
.bind(uuid::Uuid::new_v4().to_string())
.bind(chrono::Utc::now().to_rfc3339())
.execute(&pool)
.await
.unwrap();
assert!(queue.claim(KIND, 10).await.unwrap().is_empty());
let stored = status_of(&pool).await;
assert_eq!(
stored[0].0,
JobStatus::Exhausted.name(),
"an unreadable job must stop churning through claim and reclaim"
);
assert!(stored[0].2.is_some(), "and must say why it was given up on");
assert_eq!(queue.reclaim_stalled(0).await.unwrap(), 0);
assert!(queue.claim(KIND, 10).await.unwrap().is_empty());
}