118
crates/adapters/sqlite/tests/two_writers_test.rs
Normal file
118
crates/adapters/sqlite/tests/two_writers_test.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use domain::entry::MoodEntryId;
|
||||
use domain::job::{JobKind, JobSubject};
|
||||
use domain::ports::JobQueueCommandPort;
|
||||
|
||||
use sqlite::repositories::SqliteJobQueueRepository;
|
||||
|
||||
const KIND: JobKind = JobKind::BackfillRecordingIdentity;
|
||||
|
||||
fn a_file() -> String {
|
||||
let name = format!("k-mood-two-writers-{}.sqlite", uuid::Uuid::new_v4());
|
||||
|
||||
std::env::temp_dir()
|
||||
.join(name)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn two_processes_writing_one_file_both_succeed() {
|
||||
let path = a_file();
|
||||
let url = format!("sqlite://{path}");
|
||||
|
||||
let server = sqlite::create_pool(&url).await.unwrap();
|
||||
sqlite::run_migrations(&server).await.unwrap();
|
||||
|
||||
let worker = sqlite::create_pool(&url).await.unwrap();
|
||||
|
||||
let enqueuing = tokio::spawn({
|
||||
let queue = SqliteJobQueueRepository::new(server.clone());
|
||||
async move {
|
||||
for _ in 0..80 {
|
||||
queue
|
||||
.enqueue(KIND, &JobSubject::Entry(MoodEntryId::generate()))
|
||||
.await
|
||||
.expect("the server's write should not be refused");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let working = tokio::spawn({
|
||||
let queue = SqliteJobQueueRepository::new(worker.clone());
|
||||
async move {
|
||||
let mut claimed = 0;
|
||||
for _ in 0..80 {
|
||||
let batch = queue
|
||||
.claim(KIND, 5)
|
||||
.await
|
||||
.expect("the worker's write should not be refused");
|
||||
|
||||
for job in &batch {
|
||||
queue
|
||||
.finish(job.id())
|
||||
.await
|
||||
.expect("finishing should not be refused");
|
||||
}
|
||||
|
||||
claimed += batch.len();
|
||||
}
|
||||
|
||||
claimed
|
||||
}
|
||||
});
|
||||
|
||||
enqueuing.await.unwrap();
|
||||
let claimed = working.await.unwrap();
|
||||
|
||||
assert!(claimed > 0, "the worker never saw any of the server's work");
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{path}-wal"));
|
||||
let _ = std::fs::remove_file(format!("{path}-shm"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_writer_waits_for_a_held_lock_rather_than_being_refused() {
|
||||
let path = a_file();
|
||||
let url = format!("sqlite://{path}");
|
||||
|
||||
let holder = sqlite::create_pool(&url).await.unwrap();
|
||||
sqlite::run_migrations(&holder).await.unwrap();
|
||||
let other = sqlite::create_pool(&url).await.unwrap();
|
||||
|
||||
let mut held = holder.begin().await.unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO jobs (id, kind, subject, status, attempts, last_error, enqueued_at, updated_at)
|
||||
VALUES (?, 'backfillRecordingIdentity', ?, '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(&mut *held)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let waiting = tokio::spawn({
|
||||
let queue = SqliteJobQueueRepository::new(other);
|
||||
async move {
|
||||
queue
|
||||
.enqueue(KIND, &JobSubject::Entry(MoodEntryId::generate()))
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
|
||||
held.commit().await.unwrap();
|
||||
|
||||
let written = waiting.await.unwrap();
|
||||
|
||||
assert!(
|
||||
written.is_ok(),
|
||||
"the second writer was refused instead of waiting: {written:?}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{path}-wal"));
|
||||
let _ = std::fs::remove_file(format!("{path}-shm"));
|
||||
}
|
||||
Reference in New Issue
Block a user