Skip to content

Commit 1e8be7e

Browse files
authored
Merge pull request #43 from velocitysystems/exit_bug
fix: infinite loop on user-initiated exit
2 parents 00baf2c + b0246b2 commit 1e8be7e

File tree

1 file changed

+59
-38
lines changed

1 file changed

+59
-38
lines changed

src/lib.rs

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::HashMap;
22
use std::sync::Arc;
3+
use std::sync::atomic::{AtomicU8, Ordering};
34

45
use serde::Serialize;
56
use sqlx_sqlite_conn_mgr::Migrator;
@@ -25,6 +26,23 @@ pub use sqlx_sqlite_toolkit::{
2526
/// Default maximum number of concurrently loaded databases.
2627
const DEFAULT_MAX_DATABASES: usize = 50;
2728

29+
/// Tracks cleanup progress during app exit: 0 = not started, 1 = running, 2 = complete.
30+
static CLEANUP_STATE: AtomicU8 = AtomicU8::new(0);
31+
32+
/// Guarantees `CLEANUP_STATE` reaches `2` and `app_handle.exit(0)` fires even if the
33+
/// cleanup task panics. Without this, a panic would leave the state at `1` and subsequent
34+
/// user exit attempts would call `prevent_exit()` indefinitely.
35+
struct ExitGuard<R: Runtime> {
36+
app_handle: tauri::AppHandle<R>,
37+
}
38+
39+
impl<R: Runtime> Drop for ExitGuard<R> {
40+
fn drop(&mut self) {
41+
CLEANUP_STATE.store(2, Ordering::SeqCst);
42+
self.app_handle.exit(0);
43+
}
44+
}
45+
2846
/// Database instances managed by the plugin.
2947
///
3048
/// This struct maintains a thread-safe map of database paths to their corresponding
@@ -310,51 +328,61 @@ impl Builder {
310328
return;
311329
}
312330

331+
// Claim cleanup ownership once. If another handler invocation won
332+
// the race, keep exit prevented while its cleanup finishes.
333+
if CLEANUP_STATE
334+
.compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst)
335+
.is_err()
336+
{
337+
if CLEANUP_STATE.load(Ordering::SeqCst) == 1 {
338+
api.prevent_exit();
339+
debug!("Exit requested while database cleanup is in progress");
340+
}
341+
return;
342+
}
343+
313344
info!("App exit requested - cleaning up transactions and databases");
314345

315346
// Prevent immediate exit so we can close connections and checkpoint WAL
316347
api.prevent_exit();
317348

318349
let app_handle = app.clone();
319350

320-
let handle = match tokio::runtime::Handle::try_current() {
321-
Ok(h) => h,
322-
Err(_) => {
323-
warn!("No tokio runtime available for cleanup");
324-
app_handle.exit(code.unwrap_or(0));
325-
return;
326-
}
327-
};
328-
329351
let instances_clone = app.state::<DbInstances>().inner().clone();
330352
let interruptible_txs_clone = app.state::<ActiveInterruptibleTransactions>().inner().clone();
331353
let regular_txs_clone = app.state::<ActiveRegularTransactions>().inner().clone();
332354
let active_subs_clone = app.state::<subscriptions::ActiveSubscriptions>().inner().clone();
333355

334-
// Spawn a blocking thread to abort transactions and close databases
335-
// (block_in_place panics on current_thread runtime)
336-
let cleanup_result = std::thread::spawn(move || {
337-
handle.block_on(async {
338-
// First, abort all subscriptions and transactions
339-
debug!("Aborting active subscriptions and transactions");
340-
active_subs_clone.abort_all().await;
341-
sqlx_sqlite_toolkit::cleanup_all_transactions(&interruptible_txs_clone, &regular_txs_clone).await;
342-
343-
// Close databases (each wrapper's close() disables its own
344-
// observer at the crate level, unregistering SQLite hooks)
345-
let mut guard = instances_clone.inner.write().await;
346-
let wrappers: Vec<DatabaseWrapper> =
347-
guard.drain().map(|(_, v)| v).collect();
348-
349-
// Close databases in parallel with timeout
350-
let mut set = tokio::task::JoinSet::new();
351-
for wrapper in wrappers {
352-
set.spawn(async move { wrapper.close().await });
353-
}
356+
// Run cleanup on the async runtime (without blocking the event loop),
357+
// then trigger a programmatic exit when done. ExitGuard ensures
358+
// CLEANUP_STATE reaches 2 and exit() fires even on panic.
359+
tauri::async_runtime::spawn(async move {
360+
let _guard = ExitGuard { app_handle };
354361

362+
// Scope block: drops the RwLock write guard (from instances_clone)
363+
// before _guard fires exit(), whose RunEvent::Exit handler calls
364+
// try_read() on the same lock.
365+
{
355366
let timeout_result = tokio::time::timeout(
356367
std::time::Duration::from_secs(5),
357368
async {
369+
// First, abort all subscriptions and transactions
370+
debug!("Aborting active subscriptions and transactions");
371+
active_subs_clone.abort_all().await;
372+
sqlx_sqlite_toolkit::cleanup_all_transactions(&interruptible_txs_clone, &regular_txs_clone).await;
373+
374+
// Close databases (each wrapper's close() disables its own
375+
// observer at the crate level, unregistering SQLite hooks)
376+
let mut guard = instances_clone.inner.write().await;
377+
let wrappers: Vec<DatabaseWrapper> =
378+
guard.drain().map(|(_, v)| v).collect();
379+
380+
// Close databases in parallel
381+
let mut set = tokio::task::JoinSet::new();
382+
for wrapper in wrappers {
383+
set.spawn(async move { wrapper.close().await });
384+
}
385+
358386
while let Some(result) = set.join_next().await {
359387
match result {
360388
Ok(Err(e)) => warn!("Error closing database: {:?}", e),
@@ -371,15 +399,8 @@ impl Builder {
371399
} else {
372400
debug!("Database cleanup complete");
373401
}
374-
})
375-
})
376-
.join();
377-
378-
if let Err(e) = cleanup_result {
379-
error!("Database cleanup thread panicked: {:?}", e);
380-
}
381-
382-
app_handle.exit(code.unwrap_or(0));
402+
}
403+
});
383404
}
384405
RunEvent::Exit => {
385406
// ExitRequested should have already closed all databases

0 commit comments

Comments
 (0)