Line references are pinned to main at bac2f765.
Pre-existing on main. Not introduced by #372 — that PR relocated the function into remote::startup unchanged, so do not bisect to it.
Where
src-tauri/src/remote/startup.rs L380-400, started from L61.
What goes wrong
The executor is a detached thread with an unconditional loop and no way to stop it:
// L380-399
fn spawn_durable_operation_executor<R: Runtime>(state: AppState, app_handle: AppHandle<R>) {
std::thread::spawn(move || {
let publish_changes = crate::remote::PublishChanges::new(&state, &app_handle);
if let Err(error) = publish_changes.recover_pending() { /* warn */ }
loop {
std::thread::sleep(DURABLE_OPERATION_POLL_INTERVAL);
if let Err(error) = publish_changes.recover_pending() { /* warn */ }
}
});
}
No stop flag, no shutdown channel, no JoinHandle retained. The thread lives until the process exits, and it holds AppState and an AppHandle for that whole time.
Effect
- Shutdown. The poller can wake mid-teardown and run
recover_pendingagainst the control-plane database and anAppHandlewhose window is going away, producing spurious warnings or work committed during teardown. - Tests. Every runtime bootstrapped in a test leaks a poller that keeps hitting a database under a
TempDirthat the test is about to delete. Failures show up as unrelated noise in later tests rather than at the source. - Exclusivity. There is no way to pause the executor for operations that need exclusive access to the control DB; callers can only race it.
Suggested resolution (not applied)
Give the executor a shutdown signal — a cancellation flag checked around the sleep, or a channel with a timeout-based recv so it wakes promptly — and retain the handle so app teardown (and test harness teardown) can join it.
Not fixing here
Filed for tracking only.