fix(bootstrap): preserve retry cancellation across lifecycle acquisition
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user