node: Implement inventory sync command

This commit is contained in:
Alexis Sellier 2023-02-12 17:19:37 +01:00
parent 8c7c2242cc
commit 7087288cfb
No known key found for this signature in database
7 changed files with 88 additions and 33 deletions

View File

@ -62,8 +62,6 @@ enum CommandError {
InvalidCommandArg(String, Box<dyn std::error::Error>), InvalidCommandArg(String, Box<dyn std::error::Error>),
#[error("invalid command arguments `{0:?}`")] #[error("invalid command arguments `{0:?}`")]
InvalidCommandArgs(Vec<String>), InvalidCommandArgs(Vec<String>),
#[error("unknown command `{0}`")]
UnknownCommand(String),
#[error("serialization failed: {0}")] #[error("serialization failed: {0}")]
Serialization(#[from] json::Error), Serialization(#[from] json::Error),
#[error("runtime error: {0}")] #[error("runtime error: {0}")]
@ -89,6 +87,9 @@ fn command<H: Handle<Error = runtime::HandleError, FetchResult = FetchResult>>(
let cmd: Command = json::from_str(input)?; let cmd: Command = json::from_str(input)?;
match cmd.name { match cmd.name {
CommandName::Connect => {
todo!();
}
CommandName::Fetch => { CommandName::Fetch => {
let (rid, nid): (Id, NodeId) = parse::args(cmd)?; let (rid, nid): (Id, NodeId) = parse::args(cmd)?;
fetch(rid, nid, LineWriter::new(stream), handle)?; fetch(rid, nid, LineWriter::new(stream), handle)?;
@ -162,6 +163,14 @@ fn command<H: Handle<Error = runtime::HandleError, FetchResult = FetchResult>>(
} }
CommandResult::ok().to_writer(writer).ok(); CommandResult::ok().to_writer(writer).ok();
} }
CommandName::SyncInventory => match handle.sync_inventory() {
Ok(updated) => {
CommandResult::Okay { updated }.to_writer(writer)?;
}
Err(e) => {
return Err(CommandError::Runtime(e));
}
},
CommandName::Status => { CommandName::Status => {
CommandResult::ok().to_writer(writer).ok(); CommandResult::ok().to_writer(writer).ok();
} }
@ -184,9 +193,6 @@ fn command<H: Handle<Error = runtime::HandleError, FetchResult = FetchResult>>(
CommandName::Shutdown => { CommandName::Shutdown => {
return Err(CommandError::Shutdown); return Err(CommandError::Shutdown);
} }
_ => {
return Err(CommandError::UnknownCommand(line));
}
} }
Ok(()) Ok(())
} }

View File

@ -159,6 +159,12 @@ impl<G: Signer + EcSign + 'static> radicle::node::Handle for Handle<G> {
self.command(service::Command::AnnounceRefs(id)) self.command(service::Command::AnnounceRefs(id))
} }
fn sync_inventory(&mut self) -> Result<bool, Error> {
let (sender, receiver) = chan::bounded(1);
self.command(service::Command::SyncInventory(sender))?;
receiver.recv().map_err(Error::from)
}
fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Error> { fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Error> {
let (sender, receiver) = chan::unbounded(); let (sender, receiver) = chan::unbounded();
let query: Arc<QueryState> = Arc::new(move |state| { let query: Arc<QueryState> = Arc::new(move |state| {

View File

@ -1,5 +1,6 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
#![allow(clippy::collapsible_match)] #![allow(clippy::collapsible_match)]
#![allow(clippy::collapsible_if)]
pub mod config; pub mod config;
pub mod filter; pub mod filter;
pub mod message; pub mod message;
@ -103,6 +104,8 @@ pub type QueryState = dyn Fn(&dyn ServiceState) -> Result<(), CommandError> + Se
pub enum Command { pub enum Command {
/// Announce repository references for given repository to peers. /// Announce repository references for given repository to peers.
AnnounceRefs(Id), AnnounceRefs(Id),
/// Announce local inventory to peers.
SyncInventory(chan::Sender<bool>),
/// Connect to node with the given address. /// Connect to node with the given address.
Connect(NodeId, Address), Connect(NodeId, Address),
/// Lookup seeds for the given repository in the routing table. /// Lookup seeds for the given repository in the routing table.
@ -125,6 +128,7 @@ impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::AnnounceRefs(id) => write!(f, "AnnounceRefs({id})"), Self::AnnounceRefs(id) => write!(f, "AnnounceRefs({id})"),
Self::SyncInventory(_) => write!(f, "SyncInventory(..)"),
Self::Connect(id, addr) => write!(f, "Connect({id}, {addr})"), Self::Connect(id, addr) => write!(f, "Connect({id}, {addr})"),
Self::Seeds(id, _) => write!(f, "Seeds({id})"), Self::Seeds(id, _) => write!(f, "Seeds({id})"),
Self::Fetch(id, node, _) => write!(f, "Fetch({id}, {node})"), Self::Fetch(id, node, _) => write!(f, "Fetch({id}, {node})"),
@ -396,7 +400,11 @@ where
} }
if now - self.last_announce >= ANNOUNCE_INTERVAL { if now - self.last_announce >= ANNOUNCE_INTERVAL {
if self.out_of_sync { if self.out_of_sync {
if let Err(err) = self.announce_inventory() { if let Err(err) = self
.storage
.inventory()
.and_then(|i| self.announce_inventory(i))
{
error!("Error announcing inventory: {}", err); error!("Error announcing inventory: {}", err);
} }
} }
@ -480,6 +488,12 @@ where
error!("Error announcing refs: {}", err); error!("Error announcing refs: {}", err);
} }
} }
Command::SyncInventory(resp) => {
let updated = self
.sync_and_announce_inventory()
.expect("Service::command: error syncing and announcing inventory");
resp.send(updated).ok();
}
Command::QueryState(query, sender) => { Command::QueryState(query, sender) => {
sender.send(query(self)).ok(); sender.send(query(self)).ok();
} }
@ -735,17 +749,17 @@ where
return Ok(false); return Ok(false);
} }
if let Err(err) = self.sync_inventory( match self.sync_routing(&message.inventory, *announcer, message.timestamp) {
message.inventory.as_slice(), Ok(updated) => {
*announcer, if !updated {
&message.timestamp,
) {
error!("Error processing inventory from {}: {}", announcer, err);
// There's not much we can do if the peer sending us this message isn't the
// origin of it.
return Ok(false); return Ok(false);
} }
}
Err(e) => {
error!("Error processing inventory from {}: {}", announcer, e);
return Ok(false);
}
}
return Ok(relay); return Ok(relay);
} }
// Process a peer inventory update announcement by (maybe) fetching. // Process a peer inventory update announcement by (maybe) fetching.
@ -995,19 +1009,32 @@ where
Ok(()) Ok(())
} }
/// Sync, and if needed, announce our local inventory.
fn sync_and_announce_inventory(&mut self) -> Result<bool, Error> {
let inventory = self.storage.inventory()?;
let updated = self.sync_routing(&inventory, self.node_id(), self.clock.as_secs())?;
if updated {
self.announce_inventory(inventory)?;
}
Ok(updated)
}
/// Process a peer inventory announcement by updating our routing table. /// Process a peer inventory announcement by updating our routing table.
/// This function expects the peer's full inventory, and prunes entries that are not in the /// This function expects the peer's full inventory, and prunes entries that are not in the
/// given inventory. /// given inventory.
fn sync_inventory( fn sync_routing(
&mut self, &mut self,
inventory: &[Id], inventory: &[Id],
from: NodeId, from: NodeId,
timestamp: &Timestamp, timestamp: Timestamp,
) -> Result<(), Error> { ) -> Result<bool, Error> {
let mut updated = false;
let mut included = HashSet::new(); let mut included = HashSet::new();
for proj_id in inventory { for proj_id in inventory {
included.insert(proj_id); included.insert(proj_id);
if self.routing.insert(*proj_id, from, *timestamp)? { if self.routing.insert(*proj_id, from, timestamp)? {
info!(target: "service", "Routing table updated for {proj_id} with seed {from}"); info!(target: "service", "Routing table updated for {proj_id} with seed {from}");
if self if self
@ -1018,14 +1045,17 @@ where
// TODO: We should fetch here if we're already connected, case this seed has // TODO: We should fetch here if we're already connected, case this seed has
// refs we don't have. // refs we don't have.
} }
updated = true;
} }
} }
for id in self.routing.get_resources(&from)?.into_iter() { for id in self.routing.get_resources(&from)?.into_iter() {
if !included.contains(&id) { if !included.contains(&id) {
self.routing.remove(&id, &from)?; if self.routing.remove(&id, &from)? {
updated = true;
} }
} }
Ok(()) }
Ok(updated)
} }
/// Announce local refs for given id. /// Announce local refs for given id.
@ -1088,13 +1118,9 @@ where
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
/// Announce our inventory to all connected peers. /// Announce our inventory to all connected peers.
fn announce_inventory(&mut self) -> Result<(), storage::Error> { fn announce_inventory(&mut self, inventory: Vec<Id>) -> Result<(), storage::Error> {
let inventory = self.storage().inventory()?; let time = self.clock.as_secs();
let inv = Message::inventory( let inv = Message::inventory(gossip::inventory(time, inventory), &self.signer);
gossip::inventory(self.clock.as_secs(), inventory),
&self.signer,
);
for id in self.sessions.negotiated().map(|(id, _)| id) { for id in self.sessions.negotiated().map(|(id, _)| id) {
self.reactor.write(*id, inv.clone()); self.reactor.write(*id, inv.clone());
} }

View File

@ -26,7 +26,7 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
msgs.push(Message::inventory( msgs.push(Message::inventory(
InventoryAnnouncement { InventoryAnnouncement {
inventory: arbitrary::gen(3), inventory: arbitrary::vec(3).try_into().unwrap(),
timestamp: time.as_secs(), timestamp: time.as_secs(),
}, },
&signer, &signer,

View File

@ -60,6 +60,10 @@ impl radicle::node::Handle for Handle {
Ok(()) Ok(())
} }
fn sync_inventory(&mut self) -> Result<bool, Self::Error> {
unimplemented!()
}
fn routing(&self) -> Result<chan::Receiver<(Id, service::NodeId)>, Self::Error> { fn routing(&self) -> Result<chan::Receiver<(Id, service::NodeId)>, Self::Error> {
unimplemented!(); unimplemented!();
} }

View File

@ -598,7 +598,7 @@ fn test_inventory_relay() {
let mut alice = Peer::new("alice", [7, 7, 7, 7]); let mut alice = Peer::new("alice", [7, 7, 7, 7]);
let bob = Peer::new("bob", [8, 8, 8, 8]); let bob = Peer::new("bob", [8, 8, 8, 8]);
let eve = Peer::new("eve", [9, 9, 9, 9]); let eve = Peer::new("eve", [9, 9, 9, 9]);
let inv = BoundedVec::new(); let inv = BoundedVec::try_from(arbitrary::vec(1)).unwrap();
let now = LocalTime::now().as_secs(); let now = LocalTime::now().as_secs();
// Inventory from Bob relayed to Eve. // Inventory from Bob relayed to Eve.

View File

@ -108,6 +108,8 @@ impl From<net::SocketAddr> for Address {
pub enum CommandName { pub enum CommandName {
/// Announce repository references for given repository to peers. /// Announce repository references for given repository to peers.
AnnounceRefs, AnnounceRefs,
/// Sync local inventory with node.
SyncInventory,
/// Connect to node with the given address. /// Connect to node with the given address.
Connect, Connect,
/// Lookup seeds for the given repository in the routing table. /// Lookup seeds for the given repository in the routing table.
@ -261,9 +263,11 @@ pub trait Handle {
fn untrack_repo(&mut self, id: Id) -> Result<bool, Self::Error>; fn untrack_repo(&mut self, id: Id) -> Result<bool, Self::Error>;
/// Untrack the given node. /// Untrack the given node.
fn untrack_node(&mut self, id: NodeId) -> Result<bool, Self::Error>; fn untrack_node(&mut self, id: NodeId) -> Result<bool, Self::Error>;
/// Notify the client that a project has been updated. /// Notify the service that a project has been updated.
fn announce_refs(&mut self, id: Id) -> Result<(), Self::Error>; fn announce_refs(&mut self, id: Id) -> Result<(), Self::Error>;
/// Ask the client to shutdown. /// Notify the service that our inventory was updated.
fn sync_inventory(&mut self) -> Result<bool, Self::Error>;
/// Ask the service to shutdown.
fn shutdown(self) -> Result<(), Self::Error>; fn shutdown(self) -> Result<(), Self::Error>;
/// Query the routing table entries. /// Query the routing table entries.
fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Self::Error>; fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Self::Error>;
@ -403,6 +407,15 @@ impl Handle for Node {
Ok(()) Ok(())
} }
fn sync_inventory(&mut self) -> Result<bool, Error> {
let mut line = self.call::<&str, _>(CommandName::SyncInventory, [])?;
let response: CommandResult = line.next().ok_or(Error::EmptyResponse {
cmd: CommandName::SyncInventory,
})??;
response.into()
}
fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Error> { fn routing(&self) -> Result<chan::Receiver<(Id, NodeId)>, Error> {
todo!(); todo!();
} }