node: prune project routing table
Avoid wasting persistent memory by cleaning up stale routing entries when the routing table is considered constrained. Limit pruning to improve resiliency in the face of long intervals between service downtimes. Signed-off-by: Slack Coder <slackcoder@server.ky>
This commit is contained in:
parent
a74cb416c1
commit
5472931ff0
|
|
@ -2,6 +2,8 @@ use std::{env, net, process, thread};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
use nakamoto_net::LocalDuration;
|
||||||
|
|
||||||
use radicle::profile;
|
use radicle::profile;
|
||||||
use radicle_node::crypto::ssh::keystore::MemorySigner;
|
use radicle_node::crypto::ssh::keystore::MemorySigner;
|
||||||
use radicle_node::logger;
|
use radicle_node::logger;
|
||||||
|
|
@ -13,8 +15,9 @@ type Reactor = nakamoto_net_poll::Reactor<net::TcpStream>;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Options {
|
struct Options {
|
||||||
connect: Vec<Address>,
|
connect: Vec<Address>,
|
||||||
listen: Vec<net::SocketAddr>,
|
|
||||||
external_addresses: Vec<Address>,
|
external_addresses: Vec<Address>,
|
||||||
|
limits: service::config::Limits,
|
||||||
|
listen: Vec<net::SocketAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
|
|
@ -24,6 +27,7 @@ impl Options {
|
||||||
let mut parser = lexopt::Parser::from_env();
|
let mut parser = lexopt::Parser::from_env();
|
||||||
let mut connect = Vec::new();
|
let mut connect = Vec::new();
|
||||||
let mut external_addresses = Vec::new();
|
let mut external_addresses = Vec::new();
|
||||||
|
let mut limits = service::config::Limits::default();
|
||||||
let mut listen = Vec::new();
|
let mut listen = Vec::new();
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
while let Some(arg) = parser.next()? {
|
||||||
|
|
@ -32,14 +36,21 @@ impl Options {
|
||||||
let addr = parser.value()?.parse()?;
|
let addr = parser.value()?.parse()?;
|
||||||
connect.push(addr);
|
connect.push(addr);
|
||||||
}
|
}
|
||||||
Long("listen") => {
|
|
||||||
let addr = parser.value()?.parse()?;
|
|
||||||
listen.push(addr);
|
|
||||||
}
|
|
||||||
Long("external-address") => {
|
Long("external-address") => {
|
||||||
let addr = parser.value()?.parse()?;
|
let addr = parser.value()?.parse()?;
|
||||||
external_addresses.push(addr);
|
external_addresses.push(addr);
|
||||||
}
|
}
|
||||||
|
Long("limit-routing-max-age") => {
|
||||||
|
let secs: u64 = parser.value()?.parse()?;
|
||||||
|
limits.routing_max_age = LocalDuration::from_secs(secs);
|
||||||
|
}
|
||||||
|
Long("limit-routing-max-size") => {
|
||||||
|
limits.routing_max_size = parser.value()?.parse()?;
|
||||||
|
}
|
||||||
|
Long("listen") => {
|
||||||
|
let addr = parser.value()?.parse()?;
|
||||||
|
listen.push(addr);
|
||||||
|
}
|
||||||
Long("help") => {
|
Long("help") => {
|
||||||
println!("usage: radicle-node [--connect <addr>]..");
|
println!("usage: radicle-node [--connect <addr>]..");
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
|
|
@ -49,8 +60,9 @@ impl Options {
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
connect,
|
connect,
|
||||||
listen,
|
|
||||||
external_addresses,
|
external_addresses,
|
||||||
|
limits,
|
||||||
|
listen,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +89,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
service: service::Config {
|
service: service::Config {
|
||||||
connect: options.connect,
|
connect: options.connect,
|
||||||
external_addresses: options.external_addresses,
|
external_addresses: options.external_addresses,
|
||||||
|
limits: options.limits,
|
||||||
..service::Config::default()
|
..service::Config::default()
|
||||||
},
|
},
|
||||||
listen: options.listen,
|
listen: options.listen,
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,7 @@ where
|
||||||
if now - self.last_prune >= PRUNE_INTERVAL {
|
if now - self.last_prune >= PRUNE_INTERVAL {
|
||||||
debug!("Running 'prune' task...");
|
debug!("Running 'prune' task...");
|
||||||
|
|
||||||
if let Err(err) = self.prune_routing_entries() {
|
if let Err(err) = self.prune_routing_entries(&now) {
|
||||||
error!("Error pruning routing entries: {}", err);
|
error!("Error pruning routing entries: {}", err);
|
||||||
}
|
}
|
||||||
self.reactor.wakeup(PRUNE_INTERVAL);
|
self.reactor.wakeup(PRUNE_INTERVAL);
|
||||||
|
|
@ -934,8 +934,17 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_routing_entries(&mut self) -> Result<(), storage::Error> {
|
fn prune_routing_entries(&mut self, now: &LocalTime) -> Result<(), routing::Error> {
|
||||||
// TODO
|
let count = self.routing.len()?;
|
||||||
|
if count <= self.config.limits.routing_max_size {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta = count - self.config.limits.routing_max_size;
|
||||||
|
self.routing.prune(
|
||||||
|
(*now - self.config.limits.routing_max_age).as_secs(),
|
||||||
|
Some(delta),
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use super::nakamoto::LocalDuration;
|
||||||
|
|
||||||
use crate::collections::HashSet;
|
use crate::collections::HashSet;
|
||||||
use crate::identity::{Id, PublicKey};
|
use crate::identity::{Id, PublicKey};
|
||||||
use crate::service::filter::Filter;
|
use crate::service::filter::Filter;
|
||||||
|
|
@ -40,6 +42,24 @@ pub enum RemoteTracking {
|
||||||
Allowed(HashSet<PublicKey>),
|
Allowed(HashSet<PublicKey>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration parameters defining attributes of minima and maxima.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Limits {
|
||||||
|
/// Number of routing table entries before we start pruning.
|
||||||
|
pub routing_max_size: usize,
|
||||||
|
/// How long to keep a routing table entry before being pruned.
|
||||||
|
pub routing_max_age: LocalDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Limits {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
routing_max_size: 1000,
|
||||||
|
routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Service configuration.
|
/// Service configuration.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -58,6 +78,7 @@ pub struct Config {
|
||||||
pub relay: bool,
|
pub relay: bool,
|
||||||
/// List of addresses to listen on for protocol connections.
|
/// List of addresses to listen on for protocol connections.
|
||||||
pub listen: Vec<Address>,
|
pub listen: Vec<Address>,
|
||||||
|
pub limits: Limits,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -70,6 +91,7 @@ impl Default for Config {
|
||||||
remote_tracking: RemoteTracking::default(),
|
remote_tracking: RemoteTracking::default(),
|
||||||
relay: true,
|
relay: true,
|
||||||
listen: vec![],
|
listen: vec![],
|
||||||
|
limits: Limits::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ pub enum Error {
|
||||||
/// An Internal error.
|
/// An Internal error.
|
||||||
#[error("internal error: {0}")]
|
#[error("internal error: {0}")]
|
||||||
Internal(#[from] sql::Error),
|
Internal(#[from] sql::Error),
|
||||||
/// Internal clock time overflow.
|
/// Internal unit overflow.
|
||||||
#[error("the time overflowed")]
|
#[error("the unit overflowed")]
|
||||||
TimeOverflow,
|
UnitOverflow,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persistent file storage for a routing table.
|
/// Persistent file storage for a routing table.
|
||||||
|
|
@ -61,14 +61,20 @@ pub trait Store {
|
||||||
fn get_resources(&self, node_id: &NodeId) -> Result<HashSet<Id>, Error>;
|
fn get_resources(&self, node_id: &NodeId) -> Result<HashSet<Id>, Error>;
|
||||||
/// Get a specific entry.
|
/// Get a specific entry.
|
||||||
fn entry(&self, id: &Id, node: &NodeId) -> Result<Option<Timestamp>, Error>;
|
fn entry(&self, id: &Id, node: &NodeId) -> Result<Option<Timestamp>, Error>;
|
||||||
|
/// Checks if any entries are available.
|
||||||
|
fn is_empty(&self) -> Result<bool, Error> {
|
||||||
|
Ok(self.len()? == 0)
|
||||||
|
}
|
||||||
/// Add a new node seeding the given id.
|
/// Add a new node seeding the given id.
|
||||||
fn insert(&mut self, id: Id, node: NodeId, time: Timestamp) -> Result<bool, Error>;
|
fn insert(&mut self, id: Id, node: NodeId, time: Timestamp) -> Result<bool, Error>;
|
||||||
/// Remove a node for the given id.
|
/// Remove a node for the given id.
|
||||||
fn remove(&mut self, id: &Id, node: &NodeId) -> Result<bool, Error>;
|
fn remove(&mut self, id: &Id, node: &NodeId) -> Result<bool, Error>;
|
||||||
/// Iterate over all entries in the routing table.
|
/// Iterate over all entries in the routing table.
|
||||||
fn entries(&self) -> Result<Box<dyn Iterator<Item = (Id, NodeId)>>, Error>;
|
fn entries(&self) -> Result<Box<dyn Iterator<Item = (Id, NodeId)>>, Error>;
|
||||||
|
/// Get the total number of routing entries.
|
||||||
|
fn len(&self) -> Result<usize, Error>;
|
||||||
/// Prune entries older than the given timestamp.
|
/// Prune entries older than the given timestamp.
|
||||||
fn prune(&mut self, oldest: Timestamp) -> Result<usize, Error>;
|
fn prune(&mut self, oldest: Timestamp, limit: Option<usize>) -> Result<usize, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store for Table {
|
impl Store for Table {
|
||||||
|
|
@ -113,11 +119,13 @@ impl Store for Table {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, id: Id, node: NodeId, time: Timestamp) -> Result<bool, Error> {
|
fn insert(&mut self, id: Id, node: NodeId, time: Timestamp) -> Result<bool, Error> {
|
||||||
let time: i64 = time.try_into().map_err(|_| Error::TimeOverflow)?;
|
let time: i64 = time.try_into().map_err(|_| Error::UnitOverflow)?;
|
||||||
let mut stmt = self.db.prepare(
|
let mut stmt = self.db.prepare(
|
||||||
"INSERT INTO routing (resource, node, time)
|
"INSERT INTO routing (resource, node, time)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT DO NOTHING",
|
ON CONFLICT DO UPDATE
|
||||||
|
SET time = ?3
|
||||||
|
WHERE time < ?3",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
stmt.bind(1, &id)?;
|
stmt.bind(1, &id)?;
|
||||||
|
|
@ -156,11 +164,30 @@ impl Store for Table {
|
||||||
Ok(self.db.change_count() > 0)
|
Ok(self.db.change_count() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune(&mut self, oldest: Timestamp) -> Result<usize, Error> {
|
fn len(&self) -> Result<usize, Error> {
|
||||||
let oldest: i64 = oldest.try_into().map_err(|_| Error::TimeOverflow)?;
|
let stmt = self.db.prepare("SELECT COUNT(1) FROM routing")?;
|
||||||
let mut stmt = self.db.prepare("DELETE FROM routing WHERE time < ?")?;
|
let count: i64 = stmt
|
||||||
|
.into_cursor()
|
||||||
|
.next()
|
||||||
|
.expect("COUNT will always return a single row")?
|
||||||
|
.get(0);
|
||||||
|
let count: usize = count.try_into().map_err(|_| Error::UnitOverflow)?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune(&mut self, oldest: Timestamp, limit: Option<usize>) -> Result<usize, Error> {
|
||||||
|
let oldest: i64 = oldest.try_into().map_err(|_| Error::UnitOverflow)?;
|
||||||
|
let limit: i64 = limit
|
||||||
|
.unwrap_or(i64::MAX as usize)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::UnitOverflow)?;
|
||||||
|
|
||||||
|
let mut stmt = self.db.prepare(
|
||||||
|
"DELETE FROM routing WHERE rowid IN
|
||||||
|
(SELECT rowid FROM routing WHERE time < ? LIMIT ?)",
|
||||||
|
)?;
|
||||||
stmt.bind(1, oldest)?;
|
stmt.bind(1, oldest)?;
|
||||||
|
stmt.bind(2, limit)?;
|
||||||
stmt.next()?;
|
stmt.next()?;
|
||||||
|
|
||||||
Ok(self.db.change_count())
|
Ok(self.db.change_count())
|
||||||
|
|
@ -278,6 +305,19 @@ mod test {
|
||||||
assert!(!db.remove(&id, &node).unwrap());
|
assert!(!db.remove(&id, &node).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_len() {
|
||||||
|
let mut db = Table::open(":memory:").unwrap();
|
||||||
|
let ids = arbitrary::vec::<Id>(10);
|
||||||
|
let node = arbitrary::gen(1);
|
||||||
|
|
||||||
|
for id in ids {
|
||||||
|
db.insert(id, node, LocalTime::now().as_secs()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(10, db.len().unwrap(), "correct number of rows in table");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_prune() {
|
fn test_prune() {
|
||||||
let rng = fastrand::Rng::new();
|
let rng = fastrand::Rng::new();
|
||||||
|
|
@ -303,7 +343,7 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pruned = db.prune(now.as_secs()).unwrap();
|
let pruned = db.prune(now.as_secs(), None).unwrap();
|
||||||
assert_eq!(pruned, ids.len() * nodes.len());
|
assert_eq!(pruned, ids.len() * nodes.len());
|
||||||
|
|
||||||
for id in &ids {
|
for id in &ids {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use nakamoto_net as nakamoto;
|
||||||
use crate::address;
|
use crate::address;
|
||||||
use crate::collections::{HashMap, HashSet};
|
use crate::collections::{HashMap, HashSet};
|
||||||
use crate::crypto::test::signer::MockSigner;
|
use crate::crypto::test::signer::MockSigner;
|
||||||
|
use crate::identity::Id;
|
||||||
use crate::prelude::{LocalDuration, Timestamp};
|
use crate::prelude::{LocalDuration, Timestamp};
|
||||||
use crate::service::config::*;
|
use crate::service::config::*;
|
||||||
use crate::service::filter::Filter;
|
use crate::service::filter::Filter;
|
||||||
|
|
@ -227,6 +228,100 @@ fn test_inventory_sync() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inventory_pruning() {
|
||||||
|
struct Test {
|
||||||
|
limits: Limits,
|
||||||
|
/// Number of projects by peer
|
||||||
|
peer_projects: Vec<usize>,
|
||||||
|
wait_time: LocalDuration,
|
||||||
|
expected_routing_table_size: usize,
|
||||||
|
}
|
||||||
|
let tests = [
|
||||||
|
// All zero
|
||||||
|
Test {
|
||||||
|
limits: Limits {
|
||||||
|
routing_max_size: 0,
|
||||||
|
routing_max_age: LocalDuration::from_secs(0),
|
||||||
|
},
|
||||||
|
peer_projects: vec![10; 5],
|
||||||
|
wait_time: LocalDuration::from_mins(7 * 24 * 60) + LocalDuration::from_secs(1),
|
||||||
|
expected_routing_table_size: 0,
|
||||||
|
},
|
||||||
|
// All entries are too young to expire.
|
||||||
|
Test {
|
||||||
|
limits: Limits {
|
||||||
|
routing_max_size: 0,
|
||||||
|
routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
|
||||||
|
},
|
||||||
|
peer_projects: vec![10; 5],
|
||||||
|
wait_time: LocalDuration::from_mins(7 * 24 * 60) + LocalDuration::from_secs(1),
|
||||||
|
expected_routing_table_size: 0,
|
||||||
|
},
|
||||||
|
// All entries remain because the table is unconstrained.
|
||||||
|
Test {
|
||||||
|
limits: Limits {
|
||||||
|
routing_max_size: 50,
|
||||||
|
routing_max_age: LocalDuration::from_mins(0),
|
||||||
|
},
|
||||||
|
peer_projects: vec![10; 5],
|
||||||
|
wait_time: LocalDuration::from_mins(7 * 24 * 60) + LocalDuration::from_secs(1),
|
||||||
|
expected_routing_table_size: 50,
|
||||||
|
},
|
||||||
|
// Some entries are pruned because the table is constrained.
|
||||||
|
Test {
|
||||||
|
limits: Limits {
|
||||||
|
routing_max_size: 25,
|
||||||
|
routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
|
||||||
|
},
|
||||||
|
peer_projects: vec![10; 5],
|
||||||
|
wait_time: LocalDuration::from_mins(7 * 24 * 60) + LocalDuration::from_secs(1),
|
||||||
|
expected_routing_table_size: 25,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
let mut alice = Peer::config(
|
||||||
|
"alice",
|
||||||
|
Config {
|
||||||
|
limits: test.limits,
|
||||||
|
..Config::default()
|
||||||
|
},
|
||||||
|
[7, 7, 7, 7],
|
||||||
|
MockStorage::empty(),
|
||||||
|
address::Book::memory().unwrap(),
|
||||||
|
MockSigner::default(),
|
||||||
|
fastrand::Rng::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
|
||||||
|
|
||||||
|
// Tell Alice about the amazing projects available
|
||||||
|
alice.connect_to(&bob);
|
||||||
|
for num_projs in test.peer_projects {
|
||||||
|
alice.receive(
|
||||||
|
&bob.addr(),
|
||||||
|
Message::inventory(
|
||||||
|
InventoryAnnouncement {
|
||||||
|
inventory: test::arbitrary::vec::<Id>(num_projs),
|
||||||
|
timestamp: bob.clock().timestamp(),
|
||||||
|
},
|
||||||
|
&MockSigner::default(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for things to happen
|
||||||
|
assert!(test.wait_time > PRUNE_INTERVAL, "pruning must be triggered");
|
||||||
|
alice.elapse(test.wait_time);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
test.expected_routing_table_size,
|
||||||
|
alice.routing().len().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tracking() {
|
fn test_tracking() {
|
||||||
let mut alice = Peer::config(
|
let mut alice = Peer::config(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue