radicle: type safe tracking config

The tracking database can be opened in read-only or read-write
mode. Knowing which is being used is significant in concurrent
scenarios, e.g. it's safer to have a read-only handle on multiple
threads.

Provide static type information by signifying what kind of database
handle is being worked with, read or read-write.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-06-23 14:38:18 +01:00 committed by Alexis Sellier
parent 6b6f087c14
commit dfa7ba1df5
No known key found for this signature in database
6 changed files with 96 additions and 34 deletions

View File

@ -6,7 +6,7 @@ use term::Element;
use super::TrackingMode;
pub fn run(store: &tracking::store::Config, mode: TrackingMode) -> anyhow::Result<()> {
pub fn run(store: &tracking::store::ConfigReader, mode: TrackingMode) -> anyhow::Result<()> {
match mode {
TrackingMode::Repos => print_repos(store)?,
TrackingMode::Nodes => print_nodes(store)?,
@ -14,7 +14,7 @@ pub fn run(store: &tracking::store::Config, mode: TrackingMode) -> anyhow::Resul
Ok(())
}
fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
fn print_repos(store: &tracking::store::ConfigReader) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("RID")),
@ -39,7 +39,7 @@ fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
Ok(())
}
fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
fn print_nodes(store: &tracking::store::ConfigReader) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("DID")),

View File

@ -32,7 +32,7 @@ use crate::prelude::*;
use crate::runtime::Emitter;
use crate::service::message::{Announcement, AnnouncementMessage, Ping};
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
use crate::service::tracking::Scope;
use crate::service::tracking::{store::Write, Scope};
use crate::storage;
use crate::storage::{Namespaces, ReadStorage};
use crate::storage::{ReadRepository, RefUpdate};
@ -185,7 +185,7 @@ pub struct Service<R, A, S, G> {
/// Node address manager.
addresses: A,
/// Tracking policy configuration.
tracking: tracking::Config,
tracking: tracking::Config<Write>,
/// State relating to gossip.
gossip: Gossip,
/// Peer sessions, currently or recently connected.
@ -244,7 +244,7 @@ where
routing: R,
storage: S,
addresses: A,
tracking: tracking::Config,
tracking: tracking::Config<Write>,
signer: G,
rng: Rng,
node: NodeAnnouncement,
@ -349,7 +349,7 @@ where
}
/// Get the tracking policy.
pub fn tracking(&self) -> &tracking::Config {
pub fn tracking(&self) -> &tracking::Config<Write> {
&self.tracking
}

View File

@ -1,3 +1,4 @@
use core::fmt;
use std::collections::HashSet;
use std::ops;
@ -11,6 +12,7 @@ use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage};
use crate::prelude::Id;
use crate::service::NodeId;
pub use crate::node::tracking::store;
pub use crate::node::tracking::store::Config as Store;
pub use crate::node::tracking::store::Error;
pub use crate::node::tracking::{Alias, Node, Policy, Repo, Scope};
@ -42,19 +44,30 @@ pub enum NamespacesError {
}
/// Tracking configuration.
#[derive(Debug)]
pub struct Config {
pub struct Config<T> {
/// Default policy, if a policy for a specific node or repository was not found.
policy: Policy,
/// Default scope, if a scope for a specific repository was not found.
scope: Scope,
/// Underlying configuration store.
store: Store,
store: Store<T>,
}
impl Config {
// N.b. deriving `Debug` will require `T: Debug` so we manually
// implement it here.
impl<T> fmt::Debug for Config<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("policy", &self.policy)
.field("scope", &self.scope)
.field("store", &self.store)
.finish()
}
}
impl<T> Config<T> {
/// Create a new tracking configuration.
pub fn new(policy: Policy, scope: Scope, store: Store) -> Self {
pub fn new(policy: Policy, scope: Scope, store: Store<T>) -> Self {
Self {
policy,
scope,
@ -139,15 +152,15 @@ impl Config {
}
}
impl ops::Deref for Config {
type Target = Store;
impl<T> ops::Deref for Config<T> {
type Target = Store<T>;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl ops::DerefMut for Config {
impl<T> ops::DerefMut for Config<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.store
}

View File

@ -153,7 +153,7 @@ where
mut config: Config<G>,
) -> Self {
let routing = routing::Table::memory().unwrap();
let tracking = tracking::Store::memory().unwrap();
let tracking = tracking::Store::<tracking::store::Write>::memory().unwrap();
let tracking = tracking::Config::new(config.policy, config.scope, tracking);
let tempdir = tempfile::tempdir().unwrap();
let id = *config.signer.public_key();

View File

@ -1,4 +1,5 @@
#![allow(clippy::type_complexity)]
use std::marker::PhantomData;
use std::path::Path;
use std::{fmt, io, ops::Not as _, str::FromStr, time};
@ -25,30 +26,31 @@ pub enum Error {
Internal(#[from] sql::Error),
}
/// Read-only type witness.
pub struct Read;
/// Read-write type witness.
pub struct Write;
/// Read only config.
pub type ConfigReader = Config<Read>;
/// Read-write config.
pub type ConfigWriter = Config<Write>;
/// Tracking configuration.
pub struct Config {
pub struct Config<T> {
db: sql::Connection,
_marker: PhantomData<T>,
}
impl fmt::Debug for Config {
impl<T> fmt::Debug for Config<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Config(..)")
}
}
impl Config {
impl Config<Read> {
const SCHEMA: &str = include_str!("schema.sql");
/// Open a policy store at the given path. Creates a new store if it
/// doesn't exist.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut db = sql::Connection::open(path)?;
db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?;
db.execute(Self::SCHEMA)?;
Ok(Self { db })
}
/// Same as [`Self::open`], but in read-only mode. This is useful to have multiple
/// open databases, as no locking is required.
pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
@ -57,7 +59,39 @@ impl Config {
db.set_busy_timeout(DB_READ_TIMEOUT.as_millis() as usize)?;
db.execute(Self::SCHEMA)?;
Ok(Self { db })
Ok(Self {
db,
_marker: PhantomData,
})
}
/// Create a new in-memory address book.
pub fn memory() -> Result<Self, Error> {
let db =
sql::Connection::open_with_flags(":memory:", sqlite::OpenFlags::new().set_read_only())?;
db.execute(Self::SCHEMA)?;
Ok(Self {
db,
_marker: PhantomData,
})
}
}
impl Config<Write> {
const SCHEMA: &str = include_str!("schema.sql");
/// Open a policy store at the given path. Creates a new store if it
/// doesn't exist.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut db = sql::Connection::open(path)?;
db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?;
db.execute(Self::SCHEMA)?;
Ok(Self {
db,
_marker: PhantomData,
})
}
/// Create a new in-memory address book.
@ -65,7 +99,18 @@ impl Config {
let db = sql::Connection::open(":memory:")?;
db.execute(Self::SCHEMA)?;
Ok(Self { db })
Ok(Self {
db,
_marker: PhantomData,
})
}
/// Get a read-only version of this store.
pub fn read_only(self) -> ConfigReader {
Config {
db: self.db,
_marker: PhantomData,
}
}
/// Track a node.
@ -155,7 +200,11 @@ impl Config {
Ok(self.db.change_count() > 0)
}
}
/// `Read` methods for `Config`. This implies that a
/// `Config<Write>` can access these functions as well.
impl<T> Config<T> {
/// Check if a node is tracked.
pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
Ok(matches!(
@ -265,7 +314,7 @@ impl Config {
}
}
impl AliasStore for Config {
impl<T> AliasStore for Config<T> {
/// Retrieve `alias` of given node.
/// Calls `Self::node_policy` under the hood.
fn alias(&self, nid: &NodeId) -> Option<Alias> {

View File

@ -208,7 +208,7 @@ impl Profile {
}
/// Return a read-only handle to the tracking configuration of the node.
pub fn tracking(&self) -> Result<tracking::store::Config, tracking::store::Error> {
pub fn tracking(&self) -> Result<tracking::store::ConfigReader, tracking::store::Error> {
let path = self.home.node().join(node::TRACKING_DB_FILE);
let config = tracking::store::Config::reader(path)?;
@ -261,7 +261,7 @@ impl Profile {
/// Holds multiple alias stores, and will try
/// them one by one when asking for an alias.
pub struct Aliases {
tracking: Option<tracking::store::Config>,
tracking: Option<tracking::store::ConfigReader>,
addresses: Option<address::Book>,
}