node: Don't relay redundant gossip messages

This change ensures that on `Subscribe`, we only relay the latest
announcement of each node, per announcement category.

Previously, we would relay all announcement, while only the last one
was relevant.
This commit is contained in:
Alexis Sellier 2023-03-16 17:17:31 +01:00
parent 2c0cdcd1c5
commit ed1120f3eb
No known key found for this signature in database
3 changed files with 161 additions and 49 deletions

View File

@ -180,8 +180,6 @@ pub struct Service<R, A, S, G> {
gossip: Gossip, gossip: Gossip,
/// Peer sessions, currently or recently connected. /// Peer sessions, currently or recently connected.
sessions: Sessions, sessions: Sessions,
/// Keeps track of node states.
nodes: BTreeMap<NodeId, Node>,
/// Clock. Tells the time. /// Clock. Tells the time.
clock: LocalTime, clock: LocalTime,
/// Interface to the I/O reactor. /// Interface to the I/O reactor.
@ -253,8 +251,6 @@ where
clock, clock,
routing, routing,
gossip: Gossip::default(), gossip: Gossip::default(),
// FIXME: This should be loaded from the address store.
nodes: BTreeMap::new(),
reactor: Reactor::default(), reactor: Reactor::default(),
sessions, sessions,
out_of_sync: false, out_of_sync: false,
@ -797,7 +793,11 @@ where
let now = self.clock; let now = self.clock;
let timestamp = message.timestamp(); let timestamp = message.timestamp();
let relay = self.config.relay; let relay = self.config.relay;
let peer = self.nodes.entry(*announcer).or_insert_with(Node::default); let peer = self
.gossip
.nodes
.entry(*announcer)
.or_insert_with(Node::default);
// Don't allow messages from too far in the future. // Don't allow messages from too far in the future.
if timestamp.saturating_sub(now.as_millis()) > MAX_TIME_DELTA.as_millis() as u64 { if timestamp.saturating_sub(now.as_millis()) > MAX_TIME_DELTA.as_millis() as u64 {
@ -808,7 +808,7 @@ where
AnnouncementMessage::Inventory(message) => { AnnouncementMessage::Inventory(message) => {
// Discard inventory messages we've already seen, otherwise update // Discard inventory messages we've already seen, otherwise update
// out last seen time. // out last seen time.
if !peer.inventory_announced(timestamp) { if !peer.inventory_announced(announcement.clone()) {
debug!(target: "service", "Ignoring stale inventory announcement from {announcer} (t={})", self.time()); debug!(target: "service", "Ignoring stale inventory announcement from {announcer} (t={})", self.time());
return Ok(false); return Ok(false);
} }
@ -875,19 +875,19 @@ where
info!(target: "service", "Routing table updated for {} with seed {relayer}", message.rid); info!(target: "service", "Routing table updated for {} with seed {relayer}", message.rid);
} }
} }
// Discard announcement messages we've already seen, otherwise update
// our last seen time.
if !peer.refs_announced(message.rid, announcement.clone()) {
debug!(target: "service", "Ignoring stale refs announcement from {announcer} (time={timestamp})");
return Ok(false);
}
// TODO: Buffer/throttle fetches. // TODO: Buffer/throttle fetches.
if self if self
.tracking .tracking
.is_repo_tracked(&message.rid) .is_repo_tracked(&message.rid)
.expect("Service::handle_announcement: error accessing tracking configuration") .expect("Service::handle_announcement: error accessing tracking configuration")
{ {
// Discard announcement messages we've already seen, otherwise update
// our last seen time.
if !peer.refs_announced(message.rid, timestamp) {
debug!(target: "service", "Ignoring stale refs announcement from {announcer} (time={timestamp})");
return Ok(false);
}
// Refs can be relayed by peers who don't have the data in storage, // Refs can be relayed by peers who don't have the data in storage,
// therefore we only check whether we are connected to the *announcer*, // therefore we only check whether we are connected to the *announcer*,
// which is required by the protocol to only announce refs it has. // which is required by the protocol to only announce refs it has.
@ -923,7 +923,7 @@ where
) => { ) => {
// Discard node messages we've already seen, otherwise update // Discard node messages we've already seen, otherwise update
// our last seen time. // our last seen time.
if !peer.node_announced(timestamp) { if !peer.node_announced(announcement.clone()) {
debug!(target: "service", "Ignoring stale node announcement from {announcer}"); debug!(target: "service", "Ignoring stale node announcement from {announcer}");
return Ok(false); return Ok(false);
} }
@ -1004,20 +1004,20 @@ where
// Process a peer announcement. // Process a peer announcement.
(session::State::Connected { .. }, Message::Announcement(ann)) => { (session::State::Connected { .. }, Message::Announcement(ann)) => {
let relayer = peer.id; let relayer = peer.id;
let announcer = ann.node;
// Returning true here means that the message should be relayed. // Returning true here means that the message should be relayed.
if self.handle_announcement(&relayer, &ann)? { if self.handle_announcement(&relayer, &ann)? {
self.gossip.received(ann.clone(), ann.message.timestamp());
// Choose peers we should relay this message to. // Choose peers we should relay this message to.
// 1. Don't relay to the peer who sent us this message. // 1. Don't relay to the peer who sent us this message.
// 2. Don't relay to the peer who signed this announcement. // 2. Don't relay to the peer who signed this announcement.
let relay_to = self let relay_to = self
.sessions .sessions
.connected() .connected()
.filter(|(id, _)| *id != remote && *id != &ann.node); .filter(|(id, _)| *id != remote && *id != &announcer)
.map(|(_, p)| p);
self.reactor.relay(ann.clone(), relay_to.map(|(_, p)| p)); self.reactor.relay(ann, relay_to);
return Ok(()); return Ok(());
} }
@ -1621,31 +1621,31 @@ pub enum LookupError {
Identity(#[from] IdentityError), Identity(#[from] IdentityError),
} }
/// Information on a peer, that we may or may not be connected to. /// Keeps track of the most recent announcements of a node.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Node { pub struct Node {
/// Last ref announcements (per project). /// Last ref announcements (per project).
pub last_refs: HashMap<Id, Timestamp>, pub last_refs: HashMap<Id, Announcement>,
/// Last inventory announcement. /// Last inventory announcement.
pub last_inventory: Timestamp, pub last_inventory: Option<Announcement>,
/// Last node announcement. /// Last node announcement.
pub last_node: Timestamp, pub last_node: Option<Announcement>,
} }
impl Node { impl Node {
/// Process a refs announcement for the given node. /// Process a refs announcement for the given node.
/// Returns `true` if the timestamp was updated. /// Returns `true` if the timestamp was updated.
pub fn refs_announced(&mut self, id: Id, t: Timestamp) -> bool { pub fn refs_announced(&mut self, id: Id, ann: Announcement) -> bool {
match self.last_refs.entry(id) { match self.last_refs.entry(id) {
Entry::Vacant(e) => { Entry::Vacant(e) => {
e.insert(t); e.insert(ann);
return true; return true;
} }
Entry::Occupied(mut e) => { Entry::Occupied(mut e) => {
let last = e.get_mut(); let last = e.get_mut();
if t > *last { if ann.timestamp() > last.timestamp() {
*last = t; *last = ann;
return true; return true;
} }
} }
@ -1655,20 +1655,36 @@ impl Node {
/// Process an inventory announcement for the given node. /// Process an inventory announcement for the given node.
/// Returns `true` if the timestamp was updated. /// Returns `true` if the timestamp was updated.
pub fn inventory_announced(&mut self, t: Timestamp) -> bool { pub fn inventory_announced(&mut self, ann: Announcement) -> bool {
if t > self.last_inventory { match &mut self.last_inventory {
self.last_inventory = t; Some(last) => {
return true; if ann.timestamp() > last.timestamp() {
*last = ann;
return true;
}
}
None => {
self.last_inventory = Some(ann);
return true;
}
} }
false false
} }
/// Process a node announcement for the given node. /// Process a node announcement for the given node.
/// Returns `true` if the timestamp was updated. /// Returns `true` if the timestamp was updated.
pub fn node_announced(&mut self, t: Timestamp) -> bool { pub fn node_announced(&mut self, ann: Announcement) -> bool {
if t > self.last_node { match &mut self.last_node {
self.last_node = t; Some(last) => {
return true; if ann.timestamp() > last.timestamp() {
*last = ann;
return true;
}
}
None => {
self.last_node = Some(ann);
return true;
}
} }
false false
} }
@ -1738,28 +1754,30 @@ mod gossip {
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Gossip { pub struct Gossip {
received: Vec<(Timestamp, Announcement)>, // FIXME: This should be loaded from the address store.
/// Keeps track of node announcements.
pub nodes: BTreeMap<NodeId, Node>,
} }
impl Gossip { impl Gossip {
// TODO: Overwrite old messages from the same node or project.
// TODO: Should "time" be this node's time, or the time inside the message?
pub fn received(&mut self, ann: Announcement, time: Timestamp) {
self.received.push((time, ann));
}
pub fn filtered<'a>( pub fn filtered<'a>(
&'a self, &'a self,
filter: &'a Filter, filter: &'a Filter,
start: Timestamp, start: Timestamp,
end: Timestamp, end: Timestamp,
) -> impl Iterator<Item = Announcement> + '_ { ) -> impl Iterator<Item = Announcement> + '_ {
self.received self.nodes
.iter() .values()
.filter(move |(t, _)| *t >= start && *t < end) .flat_map(|n| {
.filter(move |(_, a)| a.matches(filter)) [&n.last_node, &n.last_inventory]
.cloned() .into_iter()
.map(|(_, ann)| ann) .flatten()
.chain(n.last_refs.values())
.cloned()
.collect::<Vec<_>>()
})
.filter(move |ann| ann.timestamp() >= start && ann.timestamp() < end)
.filter(move |ann| ann.matches(filter))
} }
} }

View File

@ -307,6 +307,16 @@ impl Announcement {
AnnouncementMessage::Refs(RefsAnnouncement { rid, .. }) => filter.contains(rid), AnnouncementMessage::Refs(RefsAnnouncement { rid, .. }) => filter.contains(rid),
} }
} }
/// Check whether this announcement is of the same variant as another.
pub fn variant_eq(&self, other: &Self) -> bool {
std::mem::discriminant(&self.message) == std::mem::discriminant(&other.message)
}
/// Get the announcement timestamp.
pub fn timestamp(&self) -> Timestamp {
self.message.timestamp()
}
} }
/// Message payload. /// Message payload.
@ -339,6 +349,21 @@ pub enum Message {
FetchOk { rid: Id }, FetchOk { rid: Id },
} }
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let this = wire::serialize(self);
let other = wire::serialize(other);
this.cmp(&other)
}
}
impl Message { impl Message {
pub fn announcement( pub fn announcement(
node: NodeId, node: NodeId,

View File

@ -1,5 +1,6 @@
mod e2e; mod e2e;
use std::collections::BTreeSet;
use std::default::*; use std::default::*;
use std::io; use std::io;
use std::sync::Arc; use std::sync::Arc;
@ -426,10 +427,76 @@ fn test_announcement_rebroadcast() {
}), }),
); );
let relayed = alice.messages(eve.id()).collect::<Vec<_>>(); let relayed = alice.messages(eve.id()).collect::<BTreeSet<_>>();
let received = received.into_iter().collect::<BTreeSet<_>>();
assert_eq!(relayed, received); assert_eq!(relayed, received);
} }
#[test]
fn test_announcement_rebroadcast_duplicates() {
let mut carol = Peer::new("carol", [4, 4, 4, 4]);
let mut alice = Peer::new("alice", [7, 7, 7, 7]);
let bob = Peer::new("bob", [8, 8, 8, 8]);
let eve = Peer::new("eve", [9, 9, 9, 9]);
let rids = arbitrary::set::<Id>(3..=3);
alice.connect_to(&bob);
// These are not expected to be relayed.
let stale = {
let mut anns = BTreeSet::new();
for _ in 0..5 {
carol.elapse(LocalDuration::from_mins(1));
anns.insert(carol.inventory_announcement());
anns.insert(carol.node_announcement());
}
anns
};
// These are expected to be relayed.
let expected = {
let mut anns = BTreeSet::new();
carol.elapse(LocalDuration::from_mins(1));
anns.insert(carol.inventory_announcement());
anns.insert(carol.node_announcement());
for rid in rids {
alice.track_repo(&rid, tracking::Scope::All).unwrap();
anns.insert(carol.refs_announcement(rid));
anns.insert(bob.refs_announcement(rid));
}
anns
};
let mut all = stale.iter().chain(expected.iter()).collect::<Vec<_>>();
fastrand::shuffle(&mut all);
// Alice receives all messages out of order.
for ann in all {
alice.receive(bob.id, ann.clone());
}
// Alice relays just the expected ones back to Eve.
alice.connect_from(&eve);
alice.receive(
eve.id(),
Message::Subscribe(Subscribe {
filter: Filter::default(),
since: Timestamp::MIN,
until: Timestamp::MAX,
}),
);
let relayed = alice.messages(eve.id()).collect::<BTreeSet<_>>();
assert_eq!(relayed.len(), 8);
assert_eq!(relayed, expected);
}
#[test] #[test]
fn test_announcement_rebroadcast_timestamp_filtered() { fn test_announcement_rebroadcast_timestamp_filtered() {
let mut alice = Peer::new("alice", [7, 7, 7, 7]); let mut alice = Peer::new("alice", [7, 7, 7, 7]);
@ -464,7 +531,9 @@ fn test_announcement_rebroadcast_timestamp_filtered() {
}), }),
); );
let relayed = alice.messages(eve.id()).collect::<Vec<_>>(); let relayed = alice.messages(eve.id()).collect::<BTreeSet<_>>();
let second = second.into_iter().collect::<BTreeSet<_>>();
assert_eq!(relayed.len(), second.len()); assert_eq!(relayed.len(), second.len());
assert_eq!(relayed, second); assert_eq!(relayed, second);
} }