feat: add WAL checkpoint task and improve Landlock fallback logging
Add periodic PRAGMA wal_checkpoint(TRUNCATE) every 5 minutes for both sessions.db and btmsg.db to prevent unbounded WAL growth under sustained multi-agent load. Improve Landlock fallback log message with kernel version requirement. Add WAL checkpoint tests.
This commit is contained in:
parent
83c6711cd6
commit
e46b9e06d1
3 changed files with 83 additions and 2 deletions
|
|
@ -152,11 +152,18 @@ impl SandboxConfig {
|
|||
.restrict_self()
|
||||
.map_err(|e| format!("Landlock: restrict_self failed: {e}"))?;
|
||||
|
||||
// Landlock enforcement states:
|
||||
// - Enforced: kernel 6.2+ with ABI V3 (full filesystem restriction)
|
||||
// - NotEnforced: kernel 5.13–6.1 (Landlock exists but ABI too old for V3)
|
||||
// - Error (caught above): kernel <5.13 (no Landlock LSM available)
|
||||
let enforced = status.ruleset != RulesetStatus::NotEnforced;
|
||||
if enforced {
|
||||
log::info!("Landlock sandbox applied ({} rw, {} ro paths)", self.rw_paths.len(), self.ro_paths.len());
|
||||
} else {
|
||||
log::warn!("Landlock sandbox was not enforced (kernel may lack support)");
|
||||
log::warn!(
|
||||
"Landlock not enforced — sidecar runs without filesystem restrictions. \
|
||||
Kernel 6.2+ required for enforcement."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(enforced)
|
||||
|
|
|
|||
|
|
@ -1724,6 +1724,41 @@ mod tests {
|
|||
assert!(json.get("agent_id").is_none(), "should not have snake_case");
|
||||
}
|
||||
|
||||
// ---- WAL checkpoint tests ----
|
||||
|
||||
#[test]
|
||||
fn test_checkpoint_wal_on_temp_database() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db_path = dir.path().join("test_wal.db");
|
||||
|
||||
// Create a WAL-mode database with some data
|
||||
{
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())).unwrap();
|
||||
conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);").unwrap();
|
||||
for i in 0..100 {
|
||||
conn.execute("INSERT INTO t (val) VALUES (?)", params![format!("row-{i}")]).unwrap();
|
||||
}
|
||||
// Keep connection open while we checkpoint from another connection
|
||||
// to simulate the real scenario (app holds a conn, background task checkpoints)
|
||||
|
||||
// Run checkpoint from a separate connection (like the background task does)
|
||||
let result = crate::checkpoint_wal(&db_path);
|
||||
assert!(result.is_ok(), "checkpoint_wal should succeed: {:?}", result);
|
||||
}
|
||||
|
||||
// Verify data is still intact after checkpoint
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0)).unwrap();
|
||||
assert_eq!(count, 100, "all rows should survive checkpoint");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checkpoint_wal_nonexistent_db_is_ok() {
|
||||
let result = crate::checkpoint_wal(std::path::Path::new("/tmp/nonexistent_bterminal_test.db"));
|
||||
assert!(result.is_ok(), "checkpoint_wal should return Ok for missing DB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_permissions_bidirectional() {
|
||||
let conn = test_db();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use session::SessionDb;
|
|||
use sidecar::{SidecarConfig, SidecarManager};
|
||||
use fs_watcher::ProjectFsWatcher;
|
||||
use watcher::FileWatcherManager;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
|
||||
|
|
@ -104,6 +104,40 @@ fn install_cli_tools(resource_dir: &Path, dev_root: &Path) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Run `PRAGMA wal_checkpoint(TRUNCATE)` on a SQLite database to reclaim WAL file space.
|
||||
/// Returns Ok(()) on success or Err with a diagnostic message.
|
||||
pub(crate) fn checkpoint_wal(path: &Path) -> Result<(), String> {
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(()); // DB doesn't exist yet — nothing to checkpoint
|
||||
}
|
||||
|
||||
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE)
|
||||
.map_err(|e| format!("WAL checkpoint: failed to open {}: {e}", path.display()))?;
|
||||
conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(()))
|
||||
.map_err(|e| format!("WAL checkpoint: failed to set busy_timeout: {e}"))?;
|
||||
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
|
||||
.map_err(|e| format!("WAL checkpoint failed on {}: {e}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn a background task that checkpoints WAL on both databases every 5 minutes.
|
||||
fn spawn_wal_checkpoint_task(sessions_db_path: PathBuf, btmsg_db_path: PathBuf) {
|
||||
tokio::spawn(async move {
|
||||
let interval = std::time::Duration::from_secs(300);
|
||||
loop {
|
||||
tokio::time::sleep(interval).await;
|
||||
for (label, path) in [("sessions.db", &sessions_db_path), ("btmsg.db", &btmsg_db_path)] {
|
||||
match checkpoint_wal(path) {
|
||||
Ok(()) => tracing::info!("WAL checkpoint completed for {label}"),
|
||||
Err(e) => tracing::warn!("WAL checkpoint error for {label}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
||||
|
|
@ -347,6 +381,11 @@ pub fn run() {
|
|||
Err(e) => log::warn!("Sidecar startup failed (agent features unavailable): {e}"),
|
||||
}
|
||||
|
||||
// Start periodic WAL checkpoint task (every 5 minutes)
|
||||
let sessions_db_path = config.data_dir.join("sessions.db");
|
||||
let btmsg_db_path = config.btmsg_db_path();
|
||||
spawn_wal_checkpoint_task(sessions_db_path, btmsg_db_path);
|
||||
|
||||
app.manage(AppState {
|
||||
pty_manager,
|
||||
sidecar_manager,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue