Skip to content

Commit ca3407d

Browse files
authored
Merge pull request silvermine#14 from 1Cor125/lifecycle_considerations
Lifecycle considerations
2 parents 4da085b + b75ae26 commit ca3407d

5 files changed

Lines changed: 112 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ serde = { version = "1.0.228", features = ["derive"] }
1919
serde_json = "1.0.145"
2020
thiserror = "2.0.17"
2121
log = "0.4.28"
22-
futures-core = "0.3.31"
2322
time = "0.3.44"
24-
tokio = { version = "1.48.0", features = ["sync"] }
23+
tokio = { version = "1.48.0", features = ["rt", "sync", "time"] }
2524
indexmap = { version = "2.12.1", features = ["serde"] }
2625
base64 = "0.22.1"
26+
tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] }
2727

2828
# SQLx for types and queries (time feature enables datetime type decoding)
2929
sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio"] }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ SQLite database interface for Tauri applications using
1515
even while writing (configurable pool size and idle timeouts)
1616
* **Write Serialization**: Exclusive write connection
1717

18-
> Wait! Why? From [SQLite docs](https://sqlite.org/whentouse.html):
18+
> From [SQLite docs](https://sqlite.org/whentouse.html):
1919
> "_SQLite ... will only allow one writer at any instant in time._"
2020
* **WAL Mode**: Enabled automatically on first write operation
2121
* **Type Safety**: Full TypeScript bindings

crates/sqlx-sqlite-conn-mgr/src/database.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,16 @@ impl SqliteDatabase {
144144
drop(conn); // Close immediately after creating the file
145145
}
146146

147+
// Enable PRAGMA optimize on close as recommended by SQLite for long-lived databases.
148+
// SQLite recommends analysis_limit values between 100-1000 for older versions;
149+
// SQLite 3.46.0+ handles limits automatically.
150+
// https://www.sqlite.org/lang_analyze.html#recommended_usage_pattern
151+
//
147152
// Create read pool with read-only connections
148-
let read_options = SqliteConnectOptions::new().filename(&path).read_only(true);
153+
let read_options = SqliteConnectOptions::new()
154+
.filename(&path)
155+
.read_only(true)
156+
.optimize_on_close(true, 400);
149157

150158
let read_pool = SqlitePoolOptions::new()
151159
.max_connections(config.max_read_connections)
@@ -157,7 +165,10 @@ impl SqliteDatabase {
157165
.await?;
158166

159167
// Create write pool with a single read-write connection
160-
let write_options = SqliteConnectOptions::new().filename(&path).read_only(false);
168+
let write_options = SqliteConnectOptions::new()
169+
.filename(&path)
170+
.read_only(false)
171+
.optimize_on_close(true, 400);
161172

162173
let write_conn = SqlitePoolOptions::new()
163174
.max_connections(1)

src/lib.rs

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::collections::HashMap;
2+
use std::sync::Arc;
23

3-
use tauri::{Manager, Runtime, plugin::Builder as PluginBuilder};
4+
use tauri::{Manager, RunEvent, Runtime, plugin::Builder as PluginBuilder};
45
use tokio::sync::RwLock;
6+
use tracing::{debug, error, info, warn};
57

68
mod commands;
79
mod decode;
@@ -15,8 +17,8 @@ pub use wrapper::{DatabaseWrapper, WriteQueryResult};
1517
///
1618
/// This struct maintains a thread-safe map of database paths to their corresponding
1719
/// connection wrappers.
18-
#[derive(Default)]
19-
pub struct DbInstances(pub RwLock<HashMap<String, DatabaseWrapper>>);
20+
#[derive(Clone, Default)]
21+
pub struct DbInstances(pub Arc<RwLock<HashMap<String, DatabaseWrapper>>>);
2022

2123
/// Builder for the SQLite plugin.
2224
///
@@ -57,10 +59,99 @@ impl Builder {
5759
])
5860
.setup(|app, _api| {
5961
app.manage(DbInstances::default());
62+
debug!("SQLite plugin initialized");
6063
// Future PR: Possibly handle migrations here
61-
// Future PR: Cleanup on app exit
6264
Ok(())
6365
})
66+
.on_event(|app, event| {
67+
match event {
68+
RunEvent::ExitRequested { api, code, .. } => {
69+
info!("App exit requested (code: {:?}) - closing databases before exit", code);
70+
71+
// Prevent immediate exit so we can close connections and checkpoint WAL
72+
api.prevent_exit();
73+
74+
let app_handle = app.clone();
75+
76+
let handle = match tokio::runtime::Handle::try_current() {
77+
Ok(h) => h,
78+
Err(_) => {
79+
warn!("No tokio runtime available for database cleanup");
80+
app_handle.exit(code.unwrap_or(0));
81+
return;
82+
}
83+
};
84+
85+
let instances = app.state::<DbInstances>().inner().clone();
86+
87+
// Spawn a blocking thread to close databases
88+
// (block_in_place panics on current_thread runtime)
89+
let cleanup_result = std::thread::spawn(move || {
90+
handle.block_on(async {
91+
let mut guard = instances.0.write().await;
92+
let wrappers: Vec<DatabaseWrapper> =
93+
guard.drain().map(|(_, v)| v).collect();
94+
95+
// Close databases in parallel with timeout
96+
let mut set = tokio::task::JoinSet::new();
97+
for wrapper in wrappers {
98+
set.spawn(async move { wrapper.close().await });
99+
}
100+
101+
let timeout_result = tokio::time::timeout(
102+
std::time::Duration::from_secs(5),
103+
async {
104+
while let Some(result) = set.join_next().await {
105+
match result {
106+
Ok(Err(e)) => warn!("Error closing database: {:?}", e),
107+
Err(e) => warn!("Database close task panicked: {:?}", e),
108+
Ok(Ok(())) => {}
109+
}
110+
}
111+
},
112+
)
113+
.await;
114+
115+
if timeout_result.is_err() {
116+
warn!("Database cleanup timed out after 5 seconds");
117+
} else {
118+
debug!("Database cleanup complete");
119+
}
120+
})
121+
})
122+
.join();
123+
124+
if let Err(e) = cleanup_result {
125+
error!("Database cleanup thread panicked: {:?}", e);
126+
}
127+
128+
app_handle.exit(code.unwrap_or(0));
129+
}
130+
RunEvent::Exit => {
131+
// ExitRequested should have already closed all databases
132+
// This is just a safety check
133+
let instances = app.state::<DbInstances>();
134+
match instances.0.try_read() {
135+
Ok(guard) => {
136+
if !guard.is_empty() {
137+
warn!(
138+
"Exit event fired with {} database(s) still open - cleanup may have been skipped",
139+
guard.len()
140+
);
141+
} else {
142+
debug!("Exit event: all databases already closed");
143+
}
144+
}
145+
Err(_) => {
146+
warn!("Exit event: could not check database state (lock held - cleanup may still be in progress)");
147+
}
148+
}
149+
}
150+
_ => {
151+
// Other events don't require action
152+
}
153+
}
154+
})
64155
.build()
65156
}
66157
}

0 commit comments

Comments
 (0)