radicle: Configure database connections on open

Configuration of database connections is not performed on `open`, which
leaves room for error (e.g. to miss specifying configuration).

Methods on `Profile` automatically supply the configuration of the
profile.

In testing code, just using the default configuration suffices.
This commit is contained in:
Lorenz Leutgeb 2026-03-02 16:45:15 +01:00 committed by Fintan Halpenny
parent f3afe7b02a
commit 5aaf978f97
10 changed files with 108 additions and 57 deletions

View File

@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
to create temporary patches. to create temporary patches.
- The `rad id` command will provide a better error message when a non-delegate - The `rad id` command will provide a better error message when a non-delegate
attempts to modify the identity document. attempts to modify the identity document.
- The `journal_mode` and `synchronous` pragmas can now be configured using the
configuration file. The default values used are `WAL` and `NORMAL`, which
generates less I/O operations. On power loss, transactions might be rolled
back, but SQLite still guarantees consistency in this mode.
## Fixed Bugs ## Fixed Bugs

View File

@ -156,7 +156,7 @@ impl Environment {
db.migrate(radicle::cob::migrate::ignore).unwrap(); db.migrate(radicle::cob::migrate::ignore).unwrap();
policy::Store::open(policies_db).unwrap(); policy::Store::open(policies_db).unwrap();
home.database_mut() home.database_mut(node::db::config::Config::default())
.unwrap() .unwrap()
.init( .init(
&keypair.pk.into(), &keypair.pk.into(),

View File

@ -175,11 +175,7 @@ impl Runtime {
.expect("Runtime::init: unable to solve proof-of-work puzzle"); .expect("Runtime::init: unable to solve proof-of-work puzzle");
log::info!(target: "node", "Opening node database.."); log::info!(target: "node", "Opening node database..");
let db = home let db = home.database_mut(config.database)?.init(
.database_mut()?
.journal_mode(config.database.sqlite.pragma.journal_mode)?
.synchronous(config.database.sqlite.pragma.synchronous)?
.init(
&id, &id,
announcement.features, announcement.features,
&announcement.alias, &announcement.alias,

View File

@ -150,7 +150,11 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
pub fn routing(&self) -> impl Iterator<Item = (RepoId, NodeId)> { pub fn routing(&self) -> impl Iterator<Item = (RepoId, NodeId)> {
use node::routing::Store as _; use node::routing::Store as _;
self.home.routing_mut().unwrap().entries().unwrap() self.home
.routing_mut(node::db::config::Config::default())
.unwrap()
.entries()
.unwrap()
} }
pub fn inventory(&self) -> impl Iterator<Item = RepoId> + '_ { pub fn inventory(&self) -> impl Iterator<Item = RepoId> + '_ {
@ -161,7 +165,11 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> {
/// Get sync status of a repo. /// Get sync status of a repo.
pub fn synced_seeds(&self, rid: &RepoId) -> Vec<node::seed::SyncedSeed> { pub fn synced_seeds(&self, rid: &RepoId) -> Vec<node::seed::SyncedSeed> {
let db = Database::reader(self.home.node().join(node::NODE_DB_FILE)).unwrap(); let db = Database::reader(
self.home.node().join(node::NODE_DB_FILE),
node::db::config::Config::default(),
)
.unwrap();
let seeds = db.seeds_for(rid).unwrap(); let seeds = db.seeds_for(rid).unwrap();
seeds.into_iter().collect::<Result<Vec<_>, _>>().unwrap() seeds.into_iter().collect::<Result<Vec<_>, _>>().unwrap()
@ -446,7 +454,9 @@ impl Node<MockSigner> {
) )
.unwrap(); .unwrap();
let policies = home.policies_mut().unwrap(); let policies = home.policies_mut().unwrap();
let db = home.database_mut().unwrap(); let db = home
.database_mut(node::db::config::Config::default())
.unwrap();
let db = service::Stores::from(db); let db = service::Stores::from(db);
log::debug!(target: "test", "Node::init {}: {}", config.alias, signer.public_key()); log::debug!(target: "test", "Node::init {}: {}", config.alias, signer.public_key());

View File

@ -173,7 +173,10 @@ where
policies.seed(&repo.rid, Scope::Followed).unwrap(); policies.seed(&repo.rid, Scope::Followed).unwrap();
} }
// Initialize database. // Initialize database.
let db = Database::open(config.tmp.path().join(node::NODE_DB_FILE)) let db = Database::open(
config.tmp.path().join(node::NODE_DB_FILE),
node::db::config::Config::default(),
)
.unwrap() .unwrap()
.init( .init(
&id, &id,

View File

@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
from `FULL` to `NORMAL`, which generates less I/O operations. On power from `FULL` to `NORMAL`, which generates less I/O operations. On power
loss, transactions might be rolled back, but SQLite still guarantees loss, transactions might be rolled back, but SQLite still guarantees
consistency in this mode. consistency in this mode.
- Opening database connections requires specification of a configuration.
`radicle::Profile` conveniently provides methods that supply the
configuration from `radicle::Profile::config`.
### Removed ### Removed

View File

@ -568,7 +568,7 @@ mod test {
fn test_empty() { fn test_empty() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("cache"); let path = tmp.path().join("cache");
let cache = Database::open(path).unwrap(); let cache = Database::open(path, crate::node::db::config::Config::default()).unwrap();
assert!(cache.is_empty().unwrap()); assert!(cache.is_empty().unwrap());
} }

View File

@ -82,19 +82,19 @@ impl Database {
const PRAGMA: &'static str = "PRAGMA foreign_keys = ON"; const PRAGMA: &'static str = "PRAGMA foreign_keys = ON";
/// Open a database at the given path. Creates a new database if it /// Open a database at the given path. Creates a new database if it
/// doesn't exist. /// does not exist.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn open<P: AsRef<Path>>(path: P, config: config::Config) -> Result<Self, Error> {
let mut db = sql::Connection::open_thread_safe(path)?; let mut db = sql::Connection::open_thread_safe(path)?;
db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?; db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?;
db.execute(Self::PRAGMA)?; db.execute(Self::PRAGMA)?;
migrate(&db)?; migrate(&db)?;
Ok(Self { db: Arc::new(db) }) Self { db: Arc::new(db) }.configure(config)
} }
/// Same as [`Self::open`], but in read-only mode. This is useful to have multiple /// Same as [`Self::open`], but in read-only mode. This is useful to have multiple
/// open databases, as no locking is required. /// open databases, as no locking is required.
pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn reader<P: AsRef<Path>>(path: P, config: config::Config) -> Result<Self, Error> {
let mut db = sql::Connection::open_thread_safe_with_flags( let mut db = sql::Connection::open_thread_safe_with_flags(
path, path,
sql::OpenFlags::new().with_read_only(), sql::OpenFlags::new().with_read_only(),
@ -102,7 +102,7 @@ impl Database {
db.set_busy_timeout(DB_READ_TIMEOUT.as_millis() as usize)?; db.set_busy_timeout(DB_READ_TIMEOUT.as_millis() as usize)?;
db.execute(Self::PRAGMA)?; db.execute(Self::PRAGMA)?;
Ok(Self { db: Arc::new(db) }) Self { db: Arc::new(db) }.configure(config)
} }
/// Set `journal_mode` pragma. /// Set `journal_mode` pragma.
@ -163,6 +163,11 @@ impl Database {
pub fn bump(&self) -> Result<usize, Error> { pub fn bump(&self) -> Result<usize, Error> {
transaction(&self.db, bump) transaction(&self.db, bump)
} }
fn configure(self, config: config::Config) -> Result<Self, Error> {
self.journal_mode(config.sqlite.pragma.journal_mode)?
.synchronous(config.sqlite.pragma.synchronous)
}
} }
/// Get the `user_version` value from the database header. /// Get the `user_version` value from the database header.

View File

@ -265,7 +265,7 @@ mod test {
use crate::test::arbitrary; use crate::test::arbitrary;
fn database(path: &str) -> Database { fn database(path: &str) -> Database {
let db = Database::open(path).unwrap(); let db = Database::open(path, crate::node::db::config::Config::default()).unwrap();
// We don't want to test foreign key constraints here. // We don't want to test foreign key constraints here.
db.db.execute("PRAGMA foreign_keys = OFF").unwrap(); db.db.execute("PRAGMA foreign_keys = OFF").unwrap();

View File

@ -252,10 +252,7 @@ impl Profile {
// Create DBs. // Create DBs.
home.policies_mut()?; home.policies_mut()?;
home.notifications_mut()?; home.notifications_mut()?;
home.database_mut()? home.database_mut(config.node.database)?.init(
.journal_mode(config.node.database.sqlite.pragma.journal_mode)?
.synchronous(config.node.database.sqlite.pragma.synchronous)?
.init(
&public_key, &public_key,
config.node.features(), config.node.features(),
&config.node.alias, &config.node.alias,
@ -364,7 +361,7 @@ impl Profile {
/// Return a multi-source store for aliases. /// Return a multi-source store for aliases.
pub fn aliases(&self) -> Aliases { pub fn aliases(&self) -> Aliases {
let policies = self.home.policies().ok(); let policies = self.home.policies().ok();
let db = self.home.database().ok(); let db = self.home.database(self.config.node.database).ok();
Aliases { policies, db } Aliases { policies, db }
} }
@ -419,6 +416,24 @@ impl Profile {
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
} }
/// Return a handle to the database of the node, with SQLite configuration
/// from [`Self::config`] applied.
pub fn database_mut(&self) -> Result<node::Database, node::db::Error> {
self.home.database_mut(self.config.node.database)
}
/// Return a handle to a read-only database of the node, with SQLite
/// configuration from [`Self::config`] applied.
pub fn database(&self) -> Result<node::Database, node::db::Error> {
self.home.database(self.config.node.database)
}
/// Returns the routing store, with SQLite
/// configuration from [`Self::config`] applied.
pub fn routing(&self) -> Result<impl node::routing::Store, node::db::Error> {
self.home.routing(self.config.node.database)
}
} }
impl std::ops::Deref for Profile { impl std::ops::Deref for Profile {
@ -630,34 +645,49 @@ impl Home {
} }
/// Return a handle to a read-only database of the node. /// Return a handle to a read-only database of the node.
pub fn database(&self) -> Result<node::Database, node::db::Error> { pub fn database(
&self,
config: node::db::config::Config,
) -> Result<node::Database, node::db::Error> {
let path = self.node().join(node::NODE_DB_FILE); let path = self.node().join(node::NODE_DB_FILE);
let db = node::Database::reader(path)?; let db = node::Database::reader(path, config)?;
Ok(db) Ok(db)
} }
/// Return a handle to the database of the node. /// Return a handle to the database of the node.
pub fn database_mut(&self) -> Result<node::Database, node::db::Error> { pub fn database_mut(
&self,
config: node::db::config::Config,
) -> Result<node::Database, node::db::Error> {
let path = self.node().join(node::NODE_DB_FILE); let path = self.node().join(node::NODE_DB_FILE);
let db = node::Database::open(path)?; let db = node::Database::open(path, config)?;
Ok(db) Ok(db)
} }
/// Returns the address store. /// Returns the address store.
pub fn addresses(&self) -> Result<impl node::address::Store, node::db::Error> { pub fn addresses(
self.database_mut() &self,
config: node::db::config::Config,
) -> Result<impl node::address::Store, node::db::Error> {
self.database_mut(config)
} }
/// Returns the routing store. /// Returns the routing store.
pub fn routing(&self) -> Result<impl node::routing::Store, node::db::Error> { pub fn routing(
self.database() &self,
config: node::db::config::Config,
) -> Result<impl node::routing::Store, node::db::Error> {
self.database(config)
} }
/// Returns the routing store, mutably. /// Returns the routing store, mutably.
pub fn routing_mut(&self) -> Result<impl node::routing::Store, node::db::Error> { pub fn routing_mut(
self.database_mut() &self,
config: node::db::config::Config,
) -> Result<impl node::routing::Store, node::db::Error> {
self.database_mut(config)
} }
/// Get read access to the COBs cache. /// Get read access to the COBs cache.