use super::{stop, tests::make_task};
use crate::{store::Store, types::TaskStatus};
use std::cell::RefCell;
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
thread_local! {
static WRITE_BARRIER: RefCell<Option<(Sender<()>, Receiver<()>)>> = const { RefCell::new(None) };
}
fn busy_after_read(_: i32) -> bool {
WRITE_BARRIER.with(|barrier| {
if let Some((ready, release)) = barrier.borrow_mut().take() {
ready.send(()).expect("write blocked");
release
.recv_timeout(Duration::from_secs(10))
.expect("release writer");
}
});
true
}
fn race_stopped_write(replace: bool) {
let (home, _guard, writer, dispatch) = setup_race(replace);
let (ready_tx, ready_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let dispatch_home = home.path().to_path_buf();
let handle = std::thread::spawn(move || {
let _guard = crate::support::paths::AidHomeGuard::set(&dispatch_home);
WRITE_BARRIER.with(|barrier| *barrier.borrow_mut() = Some((ready_tx, release_rx)));
dispatch
.db()
.busy_handler(Some(busy_after_read))
.expect("busy callback");
if replace {
dispatch
.replace_waiting_task(&make_task("t-stop-dispatch", TaskStatus::Pending))
.is_ok()
} else {
dispatch
.update_task_status("t-stop-dispatch", TaskStatus::Running)
.expect("launch")
}
});
ready_rx
.recv_timeout(Duration::from_secs(10))
.expect("dispatch read before stop");
writer
.db()
.execute_batch("ROLLBACK")
.expect("release blocker lock");
stop(&writer, "t-stop-dispatch").expect("settle stop before dispatch resumes");
release_tx.send(()).expect("release dispatch");
assert!(
!handle.join().expect("dispatch"),
"stale dispatch must be refused"
);
assert_eq!(
writer
.get_task("t-stop-dispatch")
.expect("get")
.expect("task")
.status,
TaskStatus::Stopped
);
let events = writer.get_events("t-stop-dispatch").expect("events");
assert_eq!(events.len(), 1);
assert_eq!(events[0].detail, "Task stopped by user");
}
fn setup_race(replace: bool) -> (tempfile::TempDir, crate::support::paths::AidHomeGuard, Arc<Store>, Store) {
let home = tempfile::tempdir().expect("home");
let guard = crate::support::paths::AidHomeGuard::set(home.path());
let path = home.path().join("race.db");
let writer = Arc::new(Store::open(&path).expect("writer"));
let dispatch = Store::open(&path).expect("dispatch");
let status = if replace {
TaskStatus::Waiting
} else {
TaskStatus::Pending
};
writer
.insert_task(&make_task("t-stop-dispatch", status))
.expect("insert");
writer
.db()
.execute_batch("BEGIN IMMEDIATE")
.expect("hold writer lock");
(home, guard, writer, dispatch)
}
#[test]
fn settle_first_waiting_replacement_cannot_revive_concurrent_stop() {
race_stopped_write(true);
}
#[test]
fn settle_first_launch_cannot_revive_concurrent_stop() {
race_stopped_write(false);
}
#[test]
fn settle_first_stopped_launch_claim_is_silent() {
let home = tempfile::tempdir().expect("home");
let _guard = crate::support::paths::AidHomeGuard::set(home.path());
let store = Arc::new(Store::open_memory().expect("store"));
let task = make_task("t-stopped-claim", TaskStatus::Pending);
store.insert_task(&task).expect("task");
stop(&store, task.id.as_str()).expect("stop");
assert!(!crate::task::lifecycle::mark_running(&store, &task.id).expect("launch claim"));
let saved = store.get_task(task.id.as_str()).expect("get").expect("task");
assert_eq!(saved.status, TaskStatus::Stopped);
assert_eq!(store.get_events(task.id.as_str()).expect("events").len(), 1);
}