fix(bootstrap): preserve retry cancellation across lifecycle acquisition

This commit is contained in:
Palash Debnath
2026-09-07 11:03:42 +05:30
parent 1ab03067cf
commit fb3d9b7139
2 changed files with 44 additions and 10 deletions
+16 -10
View File
@@ -619,11 +619,18 @@ fn launch_backend_and_wait<R: tauri::Runtime>(
stage_handle: &Arc<Mutex<BootstrapStage>>,
first_run_gate: bool,
) {
// Preserve cancellation arriving while waiting for ownership or preparing launch.
let wait_generation = backend_wait_generation();
let outcome = {
let state = app.state::<BackendState>();
let _lifecycle = state.lifecycle.lock().unwrap_or_else(|e| e.into_inner());
if backend_stop_requested(app) {
log::info!("App is quitting — backend launch cancelled");
#[cfg(debug_assertions)]
wait_for_tracking_test_gate(
"OMNIVOICE_TEST_LAUNCH_LOCKED_ENTERED",
"OMNIVOICE_TEST_LAUNCH_LOCKED_RELEASE",
);
if backend_stop_requested(app) || backend_wait_generation() != wait_generation {
log::info!("Backend launch cancelled before preparation");
LaunchOutcome::Done
} else {
match prepare_backend_launch(app, stage_handle) {
@@ -642,10 +649,10 @@ fn launch_backend_and_wait<R: tauri::Runtime>(
set_stage(stage_handle, BootstrapStage::AwaitingSetup);
LaunchOutcome::Done
} else {
spawn_with_supervisor_owner(app, stage_handle)
spawn_with_supervisor_owner(app, stage_handle, wait_generation)
}
} else {
spawn_with_supervisor_owner(app, stage_handle)
spawn_with_supervisor_owner(app, stage_handle, wait_generation)
}
}
}
@@ -663,9 +670,10 @@ fn launch_backend_and_wait<R: tauri::Runtime>(
fn spawn_with_supervisor_owner<R: tauri::Runtime>(
app: &tauri::AppHandle<R>,
stage_handle: &Arc<Mutex<BootstrapStage>>,
wait_generation: u64,
) -> LaunchOutcome {
let supervisor_owner = SUPERVISOR_OWNER.fetch_add(1, Ordering::SeqCst) + 1;
if spawn_backend_until_ready(app, stage_handle) {
if spawn_backend_until_ready(app, stage_handle, wait_generation) {
LaunchOutcome::SupervisedReady {
owner: supervisor_owner,
}
@@ -684,13 +692,11 @@ fn spawn_with_supervisor_owner<R: tauri::Runtime>(
fn spawn_backend_until_ready<R: tauri::Runtime>(
app: &tauri::AppHandle<R>,
stage_handle: &Arc<Mutex<BootstrapStage>>,
wait_generation: u64,
) -> bool {
let mut venv_heal_attempted = false;
// Snapshot taken AFTER our caller acquired lifecycle ownership, so a bump
// that happened before we started is not mistaken for a preemption of us.
let wait_generation = backend_wait_generation();
'bootstrap: loop {
if backend_stop_requested(app) {
if backend_stop_requested(app) || backend_wait_generation() != wait_generation {
return false;
}
spawn_and_track_backend(app, stage_handle);
@@ -3519,7 +3525,7 @@ mod tests {
// Retry and Clean & Retry both need. A waiter that cannot be asked to
// stand down turns a slow start into an app with no way out — strictly
// worse than the early kill this fix removed. The waiter snapshots the
// generation once ownership is held; a later bump means someone else is
// generation before ownership is acquired; a later bump means someone else is
// taking over.
let snapshot = backend_wait_generation();
assert_eq!(backend_wait_generation(), snapshot, "nothing changed yet");
@@ -345,6 +345,8 @@ const SCENARIO_ENV: &[&str] = &[
"OMNIVOICE_TEST_BEFORE_TRACK_RELEASE",
"OMNIVOICE_TEST_AFTER_TRACK_ENTERED",
"OMNIVOICE_TEST_AFTER_TRACK_RELEASE",
"OMNIVOICE_TEST_LAUNCH_LOCKED_ENTERED",
"OMNIVOICE_TEST_LAUNCH_LOCKED_RELEASE",
"OMNIVOICE_BACKEND_CMD",
"OMNIVOICE_LOG_DIR",
"OMNIVOICE_PORT",
@@ -1760,3 +1762,29 @@ fn deferred_startup_failure_names_the_step() {
logs.iter().map(|l| &l.line).collect::<Vec<_>>()
);
}
#[test]
fn retry_preempts_launch_before_the_readiness_wait_starts() {
let t = TestApp::new(&Scenario { serve_ms: Some(0), ..Default::default() });
std::env::set_var("OMNIVOICE_SCENARIO_PROGRESS_ONLY", "1");
let entered = t._logdir.path().join("launch-locked");
let release = t._logdir.path().join("release-launch");
std::env::set_var("OMNIVOICE_TEST_LAUNCH_LOCKED_ENTERED", &entered);
std::env::set_var("OMNIVOICE_TEST_LAUNCH_LOCKED_RELEASE", &release);
let bootstrap = t.run_bootstrap();
assert!(wait_until(Duration::from_secs(5), || entered.exists()));
app_lib::bootstrap::preempt_backend_wait();
let handle = t.handle();
let retry = std::thread::spawn(move || {
let state = handle.state::<BackendState>();
let _ownership = state.lifecycle.lock().unwrap();
});
std::fs::write(&release, b"release").unwrap();
let acquired = wait_until(Duration::from_secs(3), || retry.is_finished());
t.quit();
t.kill_tracked_child();
join_with_timeout(bootstrap, Duration::from_secs(10), "preempted launch");
join_with_timeout(retry, Duration::from_secs(10), "retry ownership");
assert!(acquired, "old launch swallowed Retry's generation and held lifecycle");
}