radicle/config/node: Use newtypes

This commit is contained in:
Lorenz Leutgeb 2025-08-15 22:10:00 +02:00
parent c4ff0d8eae
commit 9a7c22536b
8 changed files with 179 additions and 192 deletions

View File

@ -36,11 +36,13 @@ pub(crate) mod config {
inbound: RateLimit { inbound: RateLimit {
fill_rate: 1.0, fill_rate: 1.0,
capacity: usize::MAX, capacity: usize::MAX,
}, }
.into(),
outbound: RateLimit { outbound: RateLimit {
fill_rate: 1.0, fill_rate: 1.0,
capacity: usize::MAX, capacity: usize::MAX,
}, }
.into(),
}, },
..Limits::default() ..Limits::default()
}, },

View File

@ -90,7 +90,7 @@ fn execute() -> anyhow::Result<()> {
let config = options.config.unwrap_or_else(|| home.config()); let config = options.config.unwrap_or_else(|| home.config());
let mut config = profile::Config::load(&config)?; let mut config = profile::Config::load(&config)?;
let level = options.log.unwrap_or(config.node.log); let level = options.log.unwrap_or_else(|| config.node.log.into());
let logger = { let logger = {
let journal = { let journal = {
@ -135,7 +135,7 @@ fn execute() -> anyhow::Result<()> {
config.node.listen.clone() config.node.listen.clone()
}; };
if let Err(e) = radicle::io::set_file_limit(config.node.limits.max_open_files) { if let Err(e) = radicle::io::set_file_limit::<usize>(config.node.limits.max_open_files.into()) {
log::warn!(target: "node", "Unable to set process open file limit: {e}"); log::warn!(target: "node", "Unable to set process open file limit: {e}");
} }

View File

@ -245,7 +245,7 @@ impl Runtime {
cobs_cache, cobs_cache,
db, db,
worker::Config { worker::Config {
capacity: config.workers, capacity: config.workers.into(),
storage: storage.clone(), storage: storage.clone(),
fetch, fetch,
policy, policy,

View File

@ -308,8 +308,8 @@ fn test_inventory_pruning() {
// All zero // All zero
Test { Test {
limits: Limits { limits: Limits {
routing_max_size: 0, routing_max_size: 0.into(),
routing_max_age: LocalDuration::from_secs(0), routing_max_age: LocalDuration::from_secs(0).into(),
..Limits::default() ..Limits::default()
}, },
peer_projects: vec![10; 5], peer_projects: vec![10; 5],
@ -319,8 +319,8 @@ fn test_inventory_pruning() {
// All entries are too young to expire. // All entries are too young to expire.
Test { Test {
limits: Limits { limits: Limits {
routing_max_size: 0, routing_max_size: 0.into(),
routing_max_age: LocalDuration::from_mins(7 * 24 * 60), routing_max_age: LimitRoutingMaxAge::from(LocalDuration::from_mins(7 * 24 * 60)),
..Limits::default() ..Limits::default()
}, },
peer_projects: vec![10; 5], peer_projects: vec![10; 5],
@ -330,8 +330,8 @@ fn test_inventory_pruning() {
// All entries remain because the table is unconstrained. // All entries remain because the table is unconstrained.
Test { Test {
limits: Limits { limits: Limits {
routing_max_size: 50, routing_max_size: 50.into(),
routing_max_age: LocalDuration::from_mins(0), routing_max_age: LocalDuration::from_mins(0).into(),
..Limits::default() ..Limits::default()
}, },
peer_projects: vec![10; 5], peer_projects: vec![10; 5],
@ -341,8 +341,8 @@ fn test_inventory_pruning() {
// Some entries are pruned because the table is constrained. // Some entries are pruned because the table is constrained.
Test { Test {
limits: Limits { limits: Limits {
routing_max_size: 25, routing_max_size: 25.into(),
routing_max_age: LocalDuration::from_mins(7 * 24 * 60), routing_max_age: LocalDuration::from_mins(7 * 24 * 60).into(),
..Limits::default() ..Limits::default()
}, },
peer_projects: vec![10; 5], peer_projects: vec![10; 5],

View File

@ -679,7 +679,7 @@ fn test_concurrent_fetches() {
let repos = scale.max(4); let repos = scale.max(4);
let limits = Limits { let limits = Limits {
// Have one fetch be queued. // Have one fetch be queued.
fetch_concurrency: repos - 1, fetch_concurrency: (repos - 1).into(),
..Limits::default() ..Limits::default()
}; };
let mut bob_repos = HashSet::new(); let mut bob_repos = HashSet::new();

View File

@ -27,7 +27,7 @@ use radicle::node;
use radicle::node::address; use radicle::node::address;
use radicle::node::address::Store as _; use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, AddressType, KnownAddress}; use radicle::node::address::{AddressBook, AddressType, KnownAddress};
use radicle::node::config::PeerConfig; use radicle::node::config::{PeerConfig, RateLimit};
use radicle::node::device::Device; use radicle::node::device::Device;
use radicle::node::refs::Store as _; use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _; use radicle::node::routing::Store as _;
@ -845,7 +845,7 @@ where
if let Err(err) = self if let Err(err) = self
.db .db
.gossip_mut() .gossip_mut()
.prune((now - self.config.limits.gossip_max_age).into()) .prune((now - LocalDuration::from(self.config.limits.gossip_max_age)).into())
{ {
error!(target: "service", "Error pruning gossip entries: {err}"); error!(target: "service", "Error pruning gossip entries: {err}");
} }
@ -1302,7 +1302,7 @@ where
return true; return true;
} }
// Check for inbound connection limit. // Check for inbound connection limit.
if self.sessions.inbound().count() >= self.config.limits.connection.inbound { if self.sessions.inbound().count() >= self.config.limits.connection.inbound.into() {
return false; return false;
} }
match self.db.addresses().is_ip_banned(ip) { match self.db.addresses().is_ip_banned(ip) {
@ -1315,13 +1315,9 @@ where
Err(e) => error!(target: "service", "Error querying ban status for {ip}: {e}"), Err(e) => error!(target: "service", "Error querying ban status for {ip}: {e}"),
} }
let host: HostName = ip.into(); let host: HostName = ip.into();
let tokens = RateLimit::from(self.config.limits.rate.inbound.clone());
if self.limiter.limit( if self.limiter.limit(host.clone(), None, &tokens, self.clock) {
host.clone(),
None,
&self.config.limits.rate.inbound,
self.clock,
) {
trace!(target: "service", "Rate limiting inbound connection from {host}.."); trace!(target: "service", "Rate limiting inbound connection from {host}..");
return false; return false;
} }
@ -1824,13 +1820,13 @@ where
}; };
peer.last_active = self.clock; peer.last_active = self.clock;
let limit = match peer.link { let limit: RateLimit = match peer.link {
Link::Outbound => &self.config.limits.rate.outbound, Link::Outbound => self.config.limits.rate.outbound.clone().into(),
Link::Inbound => &self.config.limits.rate.inbound, Link::Inbound => self.config.limits.rate.inbound.clone().into(),
}; };
if self if self
.limiter .limiter
.limit(peer.addr.clone().into(), Some(remote), limit, self.clock) .limit(peer.addr.clone().into(), Some(remote), &limit, self.clock)
{ {
debug!(target: "service", "Rate limiting message from {remote} ({})", peer.addr); debug!(target: "service", "Rate limiting message from {remote} ({})", peer.addr);
return Ok(()); return Ok(());
@ -2260,7 +2256,7 @@ where
if self.sessions.contains_key(&nid) { if self.sessions.contains_key(&nid) {
return Err(ConnectError::SessionExists { nid }); return Err(ConnectError::SessionExists { nid });
} }
if self.sessions.outbound().count() >= self.config.limits.connection.outbound { if self.sessions.outbound().count() >= self.config.limits.connection.outbound.into() {
return Err(ConnectError::LimitReached { nid, addr }); return Err(ConnectError::LimitReached { nid, addr });
} }
let persistent = self.config.is_persistent(&nid); let persistent = self.config.is_persistent(&nid);
@ -2432,14 +2428,14 @@ where
fn prune_routing_entries(&mut self, now: &LocalTime) -> Result<(), routing::Error> { fn prune_routing_entries(&mut self, now: &LocalTime) -> Result<(), routing::Error> {
let count = self.db.routing().len()?; let count = self.db.routing().len()?;
if count <= self.config.limits.routing_max_size { if count <= self.config.limits.routing_max_size.into() {
return Ok(()); return Ok(());
} }
let delta = count - self.config.limits.routing_max_size; let delta = count - usize::from(self.config.limits.routing_max_size);
let nid = self.node_id(); let nid = self.node_id();
self.db.routing_mut().prune( self.db.routing_mut().prune(
(*now - self.config.limits.routing_max_age).into(), (*now - LocalDuration::from(self.config.limits.routing_max_age)).into(),
Some(delta), Some(delta),
&nid, &nid,
)?; )?;

View File

@ -226,7 +226,7 @@ impl Session {
pub fn is_at_capacity(&self) -> bool { pub fn is_at_capacity(&self) -> bool {
if let State::Connected { fetching, .. } = &self.state { if let State::Connected { fetching, .. } = &self.state {
if fetching.len() >= self.limits.fetch_concurrency { if fetching.len() >= self.limits.fetch_concurrency.into() {
return true; return true;
} }
} }

View File

@ -120,72 +120,35 @@ impl Network {
} }
/// Configuration parameters defining attributes of minima and maxima. /// Configuration parameters defining attributes of minima and maxima.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Limits { pub struct Limits {
/// Number of routing table entries before we start pruning. /// Number of routing table entries before we start pruning.
#[serde(default = "defaults::limit_routing_max_size")] pub routing_max_size: LimitRoutingMaxSize,
pub routing_max_size: usize,
/// How long to keep a routing table entry before being pruned. /// How long to keep a routing table entry before being pruned.
#[serde( pub routing_max_age: LimitRoutingMaxAge,
default = "defaults::limit_routing_max_age",
with = "crate::serde_ext::localtime::duration"
)]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
pub routing_max_age: LocalDuration,
/// How long to keep a gossip message entry before pruning it. /// How long to keep a gossip message entry before pruning it.
#[serde( pub gossip_max_age: LimitGossipMaxAge,
default = "defaults::limit_gossip_max_age",
with = "crate::serde_ext::localtime::duration"
)]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
pub gossip_max_age: LocalDuration,
/// Maximum number of concurrent fetches per peer connection. /// Maximum number of concurrent fetches per peer connection.
#[serde(default = "defaults::limit_fetch_concurrency")] pub fetch_concurrency: LimitFetchConcurrency,
pub fetch_concurrency: usize,
/// Maximum number of open files. /// Maximum number of open files.
#[serde(default = "defaults::limit_max_open_files")] pub max_open_files: LimitMaxOpenFiles,
pub max_open_files: usize,
/// Rate limitter settings. /// Rate limitter settings.
#[serde(default)]
pub rate: RateLimits, pub rate: RateLimits,
/// Connection limits. /// Connection limits.
#[serde(default)]
pub connection: ConnectionLimits, pub connection: ConnectionLimits,
/// Channel limits. /// Channel limits.
#[serde(default)]
pub fetch_pack_receive: FetchPackSizeLimit, pub fetch_pack_receive: FetchPackSizeLimit,
} }
impl Default for Limits {
fn default() -> Self {
Self {
routing_max_size: defaults::limit_routing_max_size(),
routing_max_age: defaults::limit_routing_max_age(),
gossip_max_age: defaults::limit_gossip_max_age(),
fetch_concurrency: defaults::limit_fetch_concurrency(),
max_open_files: defaults::limit_max_open_files(),
rate: RateLimits::default(),
connection: ConnectionLimits::default(),
fetch_pack_receive: FetchPackSizeLimit::default(),
}
}
}
/// Limiter for byte streams. /// Limiter for byte streams.
/// ///
/// Default: 500MiB /// Default: 500MiB
@ -277,30 +240,20 @@ impl Default for FetchPackSizeLimit {
} }
/// Connection limits. /// Connection limits.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConnectionLimits { pub struct ConnectionLimits {
/// Max inbound connections. /// Max inbound connections.
#[serde(default = "defaults::limit_connections_inbound")] pub inbound: LimitConnectionsInbound,
pub inbound: usize,
/// Max outbound connections. Note that this can be higher than the *target* number. /// Max outbound connections. Note that this can be higher than the *target* number.
#[serde(default = "defaults::limit_connections_outbound")] pub outbound: LimitConnectionsOutbound,
pub outbound: usize,
}
impl Default for ConnectionLimits {
fn default() -> Self {
Self {
inbound: defaults::limit_connections_inbound(),
outbound: defaults::limit_connections_outbound(),
}
}
} }
/// Rate limts for a single connection. /// Rate limts for a single connection.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Display)]
#[display("RateLimit(fill_rate={fill_rate}, capacity={capacity})")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RateLimit { pub struct RateLimit {
@ -309,24 +262,13 @@ pub struct RateLimit {
} }
/// Rate limits for inbound and outbound connections. /// Rate limits for inbound and outbound connections.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RateLimits { pub struct RateLimits {
#[serde(default = "defaults::limit_rate_inbound")] pub inbound: LimitRateInbound,
pub inbound: RateLimit,
#[serde(default = "defaults::limit_rate_outbound")] pub outbound: LimitRateOutbound,
pub outbound: RateLimit,
}
impl Default for RateLimits {
fn default() -> Self {
Self {
inbound: defaults::limit_rate_inbound(),
outbound: defaults::limit_rate_outbound(),
}
}
} }
/// Full address used to connect to a remote node. /// Full address used to connect to a remote node.
@ -388,22 +330,17 @@ impl Deref for ConnectAddress {
} }
/// Peer configuration. /// Peer configuration.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PeerConfig { pub enum PeerConfig {
/// Static peer set. Connect to the configured peers and maintain the connections. /// Static peer set. Connect to the configured peers and maintain the connections.
Static, Static,
/// Dynamic peer set. /// Dynamic peer set.
#[default]
Dynamic, Dynamic,
} }
impl Default for PeerConfig {
fn default() -> Self {
Self::Dynamic
}
}
/// Relay configuration. /// Relay configuration.
#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -505,13 +442,8 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub network: Network, pub network: Network,
/// Log level. /// Log level.
#[serde(default = "defaults::log")] #[serde(default)]
#[serde(with = "crate::serde_ext::string")] pub log: LogLevel,
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::log::Level")
)]
pub log: log::Level,
/// Whether or not our node should relay messages. /// Whether or not our node should relay messages.
#[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")] #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
pub relay: Relay, pub relay: Relay,
@ -519,8 +451,8 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub limits: Limits, pub limits: Limits,
/// Number of worker threads to spawn. /// Number of worker threads to spawn.
#[serde(default = "defaults::workers")] #[serde(default)]
pub workers: usize, pub workers: Workers,
/// Default seeding policy. /// Default seeding policy.
#[serde(default)] #[serde(default)]
pub seeding_policy: DefaultSeedingPolicy, pub seeding_policy: DefaultSeedingPolicy,
@ -549,8 +481,8 @@ impl Config {
onion: None, onion: None,
relay: Relay::default(), relay: Relay::default(),
limits: Limits::default(), limits: Limits::default(),
workers: defaults::workers(), workers: Workers::default(),
log: defaults::log(), log: LogLevel::default(),
seeding_policy: DefaultSeedingPolicy::default(), seeding_policy: DefaultSeedingPolicy::default(),
extra: json::Map::default(), extra: json::Map::default(),
} }
@ -587,71 +519,128 @@ impl Config {
} }
} }
/// Defaults as functions, for serde. #[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, From)]
mod defaults { #[serde(transparent)]
/// Default number of workers to spawn. #[display("{0}")]
#[inline] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub const fn workers() -> usize { pub struct LogLevel(
8 #[serde(with = "crate::serde_ext::string")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::log::Level")
)]
log::Level,
);
impl Default for LogLevel {
fn default() -> Self {
Self(log::Level::Info)
}
} }
/// Log level. impl From<LogLevel> for log::Level {
#[inline] fn from(value: LogLevel) -> Self {
pub const fn log() -> log::Level { value.0
log::Level::Info }
} }
#[inline] #[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub const fn limit_connections_inbound() -> usize { #[serde(transparent)]
128 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LimitRoutingMaxAge(
#[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
localtime::LocalDuration,
);
impl Default for LimitRoutingMaxAge {
fn default() -> Self {
Self(localtime::LocalDuration::from_mins(7 * 24 * 60)) // One week
}
} }
#[inline] impl From<LimitRoutingMaxAge> for LocalDuration {
pub const fn limit_connections_outbound() -> usize { fn from(value: LimitRoutingMaxAge) -> Self {
16 value.0
}
} }
#[inline] impl From<LocalDuration> for LimitRoutingMaxAge {
pub const fn limit_routing_max_size() -> usize { fn from(value: LocalDuration) -> Self {
1000 Self(value)
}
} }
#[inline] #[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub const fn limit_routing_max_age() -> localtime::LocalDuration { #[serde(transparent)]
localtime::LocalDuration::from_mins(7 * 24 * 60) // One week #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LimitGossipMaxAge(
#[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
localtime::LocalDuration,
);
impl Default for LimitGossipMaxAge {
fn default() -> Self {
Self(localtime::LocalDuration::from_mins(2 * 7 * 24 * 60)) // Two weeks
}
} }
#[inline] impl From<LimitGossipMaxAge> for LocalDuration {
pub const fn limit_gossip_max_age() -> localtime::LocalDuration { fn from(value: LimitGossipMaxAge) -> Self {
localtime::LocalDuration::from_mins(2 * 7 * 24 * 60) // Two weeks value.0
}
} }
#[inline] macro_rules! wrapper {
pub const fn limit_fetch_concurrency() -> usize { ($name:ident, $type:ty, $default:expr $(, $derive:ty)*) => {
1 #[derive(Clone, Debug, Deserialize, Display, Serialize, From $(, $derive)*)]
#[display("{0}")]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct $name($type);
impl Default for $name {
fn default() -> Self {
Self($default)
}
} }
#[inline] impl From<$name> for $type {
pub const fn limit_max_open_files() -> usize { fn from(value: $name) -> Self {
4096 value.0
} }
}
#[inline] };
pub const fn limit_rate_inbound() -> super::RateLimit { }
super::RateLimit { wrapper!(Workers, usize, 8, Copy);
wrapper!(LimitConnectionsInbound, usize, 128, Copy);
wrapper!(LimitConnectionsOutbound, usize, 16, Copy);
wrapper!(LimitRoutingMaxSize, usize, 1000, Copy);
wrapper!(LimitFetchConcurrency, usize, 1, Copy);
wrapper!(
LimitRateInbound,
RateLimit,
RateLimit {
fill_rate: 5.0, fill_rate: 5.0,
capacity: 1024, capacity: 1024,
} }
} );
wrapper!(LimitMaxOpenFiles, usize, 4096, Copy);
#[inline] wrapper!(
pub const fn limit_rate_outbound() -> super::RateLimit { LimitRateOutbound,
super::RateLimit { RateLimit,
RateLimit {
fill_rate: 10.0, fill_rate: 10.0,
capacity: 2048, capacity: 2048,
} }
} );
}
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
@ -671,10 +660,10 @@ mod test {
} }
)) ))
.unwrap(); .unwrap();
assert_eq!(config.limits.connection.inbound, 1337); assert_eq!(config.limits.connection.inbound.0, 1337);
assert_eq!( assert_eq!(
config.limits.connection.outbound, config.limits.connection.outbound.0,
super::defaults::limit_connections_outbound() super::LimitConnectionsOutbound::default().0,
); );
let config: Config = serde_json::from_value(json!({ let config: Config = serde_json::from_value(json!({
@ -688,9 +677,9 @@ mod test {
)) ))
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
config.limits.connection.inbound, config.limits.connection.inbound.0,
super::defaults::limit_connections_inbound() super::LimitConnectionsInbound::default().0,
); );
assert_eq!(config.limits.connection.outbound, 1337); assert_eq!(config.limits.connection.outbound.0, 1337);
} }
} }