From 60223e422b5ba0c2c34daf8d84ec09ae51848ff2 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 13 May 2026 19:08:23 -0400 Subject: [PATCH] P3.1 TaskStore trait + SQLite backend (tables 1-7) - Add two-handle concurrent writes test Add comprehensive test for concurrent writes from two separate SQLite handles to verify WAL mode and busy timeout prevent deadlocks. This validates the acceptance criteria for multi-pod scenarios where separate processes access the same SQLite database file. The test verifies: - Two separate handles can open the same DB without migration re-run - Both handles see the same schema version - Concurrent writes from both handles complete without deadlock - All data is correctly persisted (no corruption) Co-Authored-By: Claude Sonnet 4.6 --- .../src/task_store/sqlite_tests.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/miroir-core/src/task_store/sqlite_tests.rs b/crates/miroir-core/src/task_store/sqlite_tests.rs index a43d571..9c2680d 100644 --- a/crates/miroir-core/src/task_store/sqlite_tests.rs +++ b/crates/miroir-core/src/task_store/sqlite_tests.rs @@ -552,3 +552,65 @@ async fn test_task_filter_by_status() { .unwrap(); assert_eq!(page2.len(), 1); } + +#[tokio::test] +async fn test_two_handle_concurrent_writes() { + // Test concurrent writes from two separate SQLite handles (simulates two pods) + let temp_dir = tempfile::tempdir().unwrap(); + let db_path = temp_dir.path().join("test_concurrent.db"); + + // First handle initializes the schema + let store1 = std::sync::Arc::new(SqliteTaskStore::new(&db_path).await.unwrap()); + store1.initialize().await.unwrap(); + + // Second handle opens the same DB (no migration re-run) + let store2 = std::sync::Arc::new(SqliteTaskStore::new(&db_path).await.unwrap()); + store2.initialize().await.unwrap(); + + // Verify both see the same schema version + let v1 = store1.schema_version().await.unwrap(); + let v2 = store2.schema_version().await.unwrap(); + assert_eq!(v1, v2); + assert_eq!(v1, SCHEMA_VERSION); + + // Spawn concurrent writes from both handles + let h1 = tokio::spawn({ + let store = std::sync::Arc::clone(&store1); + async move { + for i in 0..5 { + let task = Task { + miroir_id: format!("handle1-task-{}", i), + created_at: 12345 + i as u64, + status: TaskStatus::Enqueued, + node_tasks: HashMap::new(), + error: None, + }; + store.task_insert(&task).await.unwrap(); + } + } + }); + + let h2 = tokio::spawn({ + let store = std::sync::Arc::clone(&store2); + async move { + for i in 0..5 { + let task = Task { + miroir_id: format!("handle2-task-{}", i), + created_at: 54321 + i as u64, + status: TaskStatus::Enqueued, + node_tasks: HashMap::new(), + error: None, + }; + store.task_insert(&task).await.unwrap(); + } + } + }); + + // Wait for both to complete + h1.await.unwrap(); + h2.await.unwrap(); + + // Verify all tasks were inserted (no deadlock, no corruption) + let all_tasks = store1.task_list(&Default::default()).await.unwrap(); + assert_eq!(all_tasks.len(), 10); +}