Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions crates/tracedecay-rusqlite-runtime/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,10 +569,13 @@ impl std::error::Error for VerifiedReaderError {
/// Foreign-database reader bound to the physical file actually
/// opened by SQLite rather than a later pathname observation.
pub struct VerifiedReader {
_opened_file: OpenedDatabaseFile,
// Keep the connection before the raw pin in declaration order: closing
// any raw descriptor on Unix can release another SQLite connection's
// process-wide record locks. SQLite must close its handle first.
connection: Connection,
canonical_path: PathBuf,
file_identity: u64,
_opened_file: OpenedDatabaseFile,
}

pub fn default_vfs_max_pathname() -> Option<usize> {
Expand All @@ -592,8 +595,10 @@ impl VerifiedReader {
self.file_identity
}

pub fn into_parts(self) -> (Connection, PathBuf, u64) {
(self.connection, self.canonical_path, self.file_identity)
/// The verified canonical pathname. It remains bound to this reader's
/// retained physical file until the SQLite connection is closed.
pub fn canonical_path(&self) -> &Path {
&self.canonical_path
}
}

Expand Down
89 changes: 86 additions & 3 deletions crates/tracedecay-sessions/src/runtime/host_sqlite.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
use std::sync::{Arc, Mutex, PoisonError};

/// A plain SQLite read or a verified foreign read. The verified case retains
/// its inode pin for the whole connection lifetime instead of unpacking and
/// dropping it immediately after admission.
enum SqliteReadHandle {
Plain(rusqlite::Connection),
Verified(tracedecay_rusqlite_runtime::VerifiedReader),
}

impl SqliteReadHandle {
fn connection(&self) -> &rusqlite::Connection {
match self {
Self::Plain(connection) => connection,
Self::Verified(reader) => reader.connection(),
}
}
}

#[derive(Clone)]
pub(crate) struct SqliteReadConn {
inner: Arc<Mutex<rusqlite::Connection>>,
inner: Arc<Mutex<SqliteReadHandle>>,
}

impl SqliteReadConn {
pub(crate) fn new(connection: rusqlite::Connection) -> Self {
Self {
inner: Arc::new(Mutex::new(connection)),
inner: Arc::new(Mutex::new(SqliteReadHandle::Plain(connection))),
}
}

pub(crate) fn new_verified(reader: tracedecay_rusqlite_runtime::VerifiedReader) -> Self {
Self {
inner: Arc::new(Mutex::new(SqliteReadHandle::Verified(reader))),
}
}

Expand All @@ -21,9 +44,69 @@ impl SqliteReadConn {
let inner = Arc::clone(&self.inner);
tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
let guard = inner.lock().unwrap_or_else(PoisonError::into_inner);
body(&guard)
body(guard.connection())
})
.await
.ok()
}
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
use std::os::unix::fs::MetadataExt;
use std::path::Path;

use super::SqliteReadConn;

fn descriptors_for(path: &Path) -> usize {
let identity = std::fs::metadata(path).expect("database metadata");
std::fs::read_dir("/proc/self/fd")
.expect("process file descriptors")
.filter_map(Result::ok)
.filter(|entry| {
std::fs::metadata(entry.path()).is_ok_and(|metadata| {
metadata.dev() == identity.dev() && metadata.ino() == identity.ino()
})
})
.count()
}

#[test]
fn verified_foreign_reader_retains_its_pin_until_the_last_handle_closes() {
let dir = tempfile::tempdir().expect("test directory");
let path = dir.path().join("foreign.sqlite3");
{
let conn = rusqlite::Connection::open(&path).expect("create database");
conn.execute_batch(
"CREATE TABLE sample(value INTEGER); INSERT INTO sample VALUES (42)",
)
.expect("seed database");
}
assert_eq!(descriptors_for(&path), 0);

let verified = tracedecay_rusqlite_runtime::open_verified_immutable_reader(&path)
.expect("open verified foreign database");
let pinned_descriptors = descriptors_for(&path);
assert!(
pinned_descriptors >= 2,
"SQLite and its verified identity pin each retain an inode handle"
);

let reader = SqliteReadConn::new_verified(verified);
assert_eq!(descriptors_for(&path), pinned_descriptors);
let other = reader.clone();
drop(reader);
assert_eq!(descriptors_for(&path), pinned_descriptors);
let value: i64 = other
.inner
.lock()
.expect("reader lock")
.connection()
.query_row("SELECT value FROM sample", [], |row| row.get(0))
.expect("read verified database");
assert_eq!(value, 42);

drop(other);
assert_eq!(descriptors_for(&path), 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ pub(super) async fn open_readonly_immutable(db_path: &Path) -> Result<ReadOnlyDb
tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
let opened = tracedecay_rusqlite_runtime::open_verified_immutable_reader(&path)
.map_err(|error| format!("could not open '{}' read-only: {error}", path.display()))?;
let (conn, canonical_path, file_identity) = opened.into_parts();
let generation = ObservationSourceGenerationV1::new(file_identity)
let canonical_path = opened.canonical_path().to_path_buf();
let generation = ObservationSourceGenerationV1::new(opened.file_identity())
.map_err(|error| format!("invalid SQLite generation identity: {error}"))?;
Ok(ReadOnlyDb {
conn: CursorConn::new(conn),
conn: CursorConn::new_verified(opened),
generation,
canonical_path,
})
Expand Down
Loading