node: Buffer gossips until fetch is completed

This change ensures that gossip messages are buffered if we try to
send them during a fetch, and sent after the fetch completes.
This commit is contained in:
Alexis Sellier 2023-02-16 09:48:05 +01:00
parent 0f81f8a7aa
commit 011923f1df
No known key found for this signature in database
4 changed files with 177 additions and 32 deletions

View File

@ -31,7 +31,6 @@ use crate::node::{Address, Features, FetchResult};
use crate::prelude::*; use crate::prelude::*;
use crate::service::message::{Announcement, AnnouncementMessage, Ping}; use crate::service::message::{Announcement, AnnouncementMessage, Ping};
use crate::service::message::{NodeAnnouncement, RefsAnnouncement}; use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
use crate::service::session::Protocol;
use crate::storage; use crate::storage;
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage}; use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage};
use crate::storage::{Namespaces, ReadStorage}; use crate::storage::{Namespaces, ReadStorage};
@ -519,7 +518,8 @@ where
session::FetchResult::Ready(fetch) => { session::FetchResult::Ready(fetch) => {
debug!(target: "service", "Fetch initiated for {rid} with {seed}.."); debug!(target: "service", "Fetch initiated for {rid} with {seed}..");
self.reactor.write(&session, fetch); self.reactor.write(session, fetch);
session.to_requesting(rid);
} }
session::FetchResult::AlreadyFetching(other) => { session::FetchResult::AlreadyFetching(other) => {
if other == rid { if other == rid {
@ -613,6 +613,9 @@ where
); );
} }
} }
// Drain any messages in the session's outbox, which might have accumulated during the
// fetch, and send them to the peer.
self.reactor.drain(session);
} else { } else {
log::debug!(target: "service", "Session not found for {remote}"); log::debug!(target: "service", "Session not found for {remote}");
} }
@ -641,6 +644,7 @@ where
let filter = self.filter(); let filter = self.filter();
if let Some(peer) = self.sessions.get_mut(&remote) { if let Some(peer) = self.sessions.get_mut(&remote) {
peer.to_connected(self.clock);
self.reactor.write_all( self.reactor.write_all(
peer, peer,
gossip::handshake( gossip::handshake(
@ -651,7 +655,6 @@ where
&self.config, &self.config,
), ),
); );
peer.to_connected(self.clock);
} }
} else { } else {
match self.sessions.entry(remote) { match self.sessions.entry(remote) {
@ -1026,7 +1029,7 @@ where
return Ok(()); return Ok(());
} }
self.reactor.write( self.reactor.write(
&peer, peer,
Message::Pong { Message::Pong {
zeroes: ZeroBytes::new(ponglen), zeroes: ZeroBytes::new(ponglen),
}, },
@ -1039,16 +1042,14 @@ where
} }
} }
} }
(session::State::Connected { protocol, .. }, Message::Fetch { rid }) => { (session::State::Connected { .. }, Message::Fetch { rid }) => {
debug!(target: "service", "Fetch requested for {rid} from {remote}.."); debug!(target: "service", "Fetch requested for {rid} from {remote}..");
// TODO: Check that we have the repo first? // TODO: Check that we have the repo first?
*protocol = Protocol::Fetch { rid };
// Accept the request and instruct the transport to handover the socket to the worker. // Accept the request and instruct the transport to handover the socket to the worker.
self.reactor.write(peer, Message::FetchOk { rid }); self.reactor.write(peer, Message::FetchOk { rid });
self.reactor self.reactor.fetch(peer, rid, Namespaces::default(), false);
.fetch(*remote, rid, Namespaces::default(), false);
} }
(session::State::Connected { protocol, .. }, Message::FetchOk { rid }) => { (session::State::Connected { protocol, .. }, Message::FetchOk { rid }) => {
if *protocol if *protocol
@ -1066,10 +1067,8 @@ where
} }
debug!(target: "service", "Fetch accepted for {rid} from {remote}.."); debug!(target: "service", "Fetch accepted for {rid} from {remote}..");
*protocol = Protocol::Fetch { rid };
// Instruct the transport to handover the socket to the worker. // Instruct the transport to handover the socket to the worker.
self.reactor self.reactor.fetch(peer, rid, Namespaces::default(), true);
.fetch(*remote, rid, Namespaces::default(), true);
} }
(session::State::Connecting { .. }, msg) => { (session::State::Connecting { .. }, msg) => {
error!("Received {:?} from connecting peer {}", msg, peer.id); error!("Received {:?} from connecting peer {}", msg, peer.id);
@ -1241,7 +1240,7 @@ where
let inactive_sessions = self let inactive_sessions = self
.sessions .sessions
.negotiated_mut() .negotiated_mut()
.filter(|(_, session)| session.last_active < *now - KEEP_ALIVE_DELTA) .filter(|(_, session)| *now - session.last_active >= KEEP_ALIVE_DELTA)
.map(|(_, session)| session); .map(|(_, session)| session);
for session in inactive_sessions { for session in inactive_sessions {
session.ping(&mut self.reactor).ok(); session.ping(&mut self.reactor).ok();

View File

@ -1,4 +1,5 @@
use std::collections::VecDeque; use std::collections::{HashMap, VecDeque};
use std::mem;
use log::*; use log::*;
@ -43,6 +44,10 @@ pub struct Fetch {
pub struct Reactor { pub struct Reactor {
/// Outgoing I/O queue. /// Outgoing I/O queue.
io: VecDeque<Io>, io: VecDeque<Io>,
/// Message outbox for each node.
/// If messages can't be sent to a node immediately, they are stored in the outbox.
/// This can happen if for eg. a fetch is ongoing with that node.
outbox: HashMap<NodeId, Vec<Message>>,
} }
impl Reactor { impl Reactor {
@ -62,35 +67,74 @@ impl Reactor {
} }
pub fn write(&mut self, remote: &Session, msg: Message) { pub fn write(&mut self, remote: &Session, msg: Message) {
debug!(target: "service", "Write {:?} to {}", &msg, remote); if remote.is_gossip_allowed() {
debug!(target: "service", "Write {:?} to {}", &msg, remote);
self.io.push_back(Io::Write(remote.id, vec![msg])); self.io.push_back(Io::Write(remote.id, vec![msg]));
} else {
debug!(target: "service", "Queue {:?} for {}", &msg, remote);
self.outbox.entry(remote.id).or_default().push(msg);
}
} }
pub fn write_all(&mut self, remote: &Session, msgs: impl IntoIterator<Item = Message>) { pub fn write_all(&mut self, remote: &Session, msgs: impl IntoIterator<Item = Message>) {
let msgs = msgs.into_iter().collect::<Vec<_>>(); let msgs = msgs.into_iter().collect::<Vec<_>>();
let is_gossip_allowed = remote.is_gossip_allowed();
for (ix, msg) in msgs.iter().enumerate() { for (ix, msg) in msgs.iter().enumerate() {
debug!( if is_gossip_allowed {
target: "service", debug!(
"Write {:?} message to {} ({}/{})", target: "service",
msg, "Write {:?} to {} ({}/{})",
remote, msg,
ix + 1, remote,
msgs.len() ix + 1,
); msgs.len()
);
} else {
debug!(
target: "service",
"Queue {:?} for {} ({}/{})",
msg,
remote,
ix + 1,
msgs.len()
);
}
}
if is_gossip_allowed {
self.io.push_back(Io::Write(remote.id, msgs));
} else {
self.outbox.entry(remote.id).or_default().extend(msgs);
}
}
pub fn drain(&mut self, remote: &Session) {
if let Some(outbox) = self.outbox.get_mut(&remote.id) {
debug!(target: "service", "Draining outbox for session {} ({} message(s))", remote.id, outbox.len());
let msgs = mem::take(outbox);
self.write_all(remote, msgs);
} }
self.io.push_back(Io::Write(remote.id, msgs));
} }
pub fn wakeup(&mut self, after: LocalDuration) { pub fn wakeup(&mut self, after: LocalDuration) {
self.io.push_back(Io::Wakeup(after)); self.io.push_back(Io::Wakeup(after));
} }
pub fn fetch(&mut self, remote: NodeId, rid: Id, namespaces: Namespaces, initiated: bool) { pub fn fetch(
&mut self,
remote: &mut Session,
rid: Id,
namespaces: Namespaces,
initiated: bool,
) {
// Transition the session state machine to "fetching".
remote.to_fetching(rid);
self.io.push_back(Io::Fetch(Fetch { self.io.push_back(Io::Fetch(Fetch {
rid, rid,
namespaces, namespaces,
remote, remote: remote.id,
initiated, initiated,
})); }));
} }

View File

@ -145,7 +145,7 @@ impl fmt::Display for Session {
} }
attrs.push(state.as_str()); attrs.push(state.as_str());
write!(f, "{}", attrs.join(" ")) write!(f, "{} [{}]", self.id, attrs.join(" "))
} }
} }
@ -203,6 +203,16 @@ impl Session {
) )
} }
pub fn is_gossip_allowed(&self) -> bool {
matches!(
self.state,
State::Connected {
protocol: Protocol::Gossip { requested: None },
..
}
)
}
pub fn attempts(&self) -> usize { pub fn attempts(&self) -> usize {
self.attempts self.attempts
} }
@ -214,9 +224,6 @@ impl Session {
if let Some(requested) = requested { if let Some(requested) = requested {
FetchResult::AlreadyFetching(*requested) FetchResult::AlreadyFetching(*requested)
} else { } else {
*protocol = Protocol::Gossip {
requested: Some(rid),
};
FetchResult::Ready(Message::Fetch { rid }) FetchResult::Ready(Message::Fetch { rid })
} }
} }
@ -227,6 +234,22 @@ impl Session {
} }
} }
pub fn to_requesting(&mut self, rid: Id) {
let State::Connected { protocol, .. } = &mut self.state else {
panic!("Session::to_requesting: cannot transition to 'requesting': session is not connected");
};
*protocol = Protocol::Gossip {
requested: Some(rid),
};
}
pub fn to_fetching(&mut self, rid: Id) {
let State::Connected { protocol, .. } = &mut self.state else {
panic!("Session::to_fetching: cannot transition to 'fetching': session is not connected");
};
*protocol = Protocol::Fetch { rid };
}
pub fn to_connecting(&mut self) { pub fn to_connecting(&mut self) {
assert!( assert!(
self.is_disconnected(), self.is_disconnected(),

View File

@ -10,6 +10,7 @@ use netservices::LinkDirection as Link;
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::identity::Id;
use crate::node;
use crate::prelude::*; use crate::prelude::*;
use crate::prelude::{LocalDuration, Timestamp}; use crate::prelude::{LocalDuration, Timestamp};
use crate::service::config::*; use crate::service::config::*;
@ -20,7 +21,7 @@ use crate::service::ServiceState as _;
use crate::service::*; use crate::service::*;
use crate::storage::git::transport::{local, remote}; use crate::storage::git::transport::{local, remote};
use crate::storage::git::Storage; use crate::storage::git::Storage;
use crate::storage::ReadStorage; use crate::storage::{Namespaces, ReadStorage};
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::assert_matches; use crate::test::assert_matches;
use crate::test::fixtures; use crate::test::fixtures;
@ -592,6 +593,84 @@ fn test_refs_announcement_no_subscribe() {
assert!(alice.messages(eve.id()).next().is_none()); assert!(alice.messages(eve.id()).next().is_none());
} }
#[test]
fn test_gossip_during_fetch() {
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 now = LocalTime::now().as_millis();
let rid = arbitrary::gen::<Id>(1);
let (send, _recv) = chan::bounded::<node::FetchResult>(1);
let inventory1 = BoundedVec::try_from(arbitrary::vec(1)).unwrap();
let inventory2 = BoundedVec::try_from(arbitrary::vec(1)).unwrap();
alice.connect_to(&bob);
alice.connect_to(&eve);
alice.command(Command::Fetch(rid, bob.id, send));
assert_matches!(alice.messages(bob.id).next(), Some(Message::Fetch { .. }));
logger::init(log::Level::Debug);
alice.receive(
eve.id(),
Message::inventory(
InventoryAnnouncement {
inventory: inventory1.clone(),
timestamp: now + 1,
},
eve.signer(),
),
);
// We shouldn't relay to Bob while we're fetching from him.
assert_matches!(alice.messages(bob.id).next(), None);
alice.receive(bob.id(), Message::FetchOk { rid });
alice.receive(
eve.id(),
Message::inventory(
InventoryAnnouncement {
inventory: inventory2.clone(),
timestamp: now + 2,
},
eve.signer(),
),
);
// We shouldn't relay to Bob while we're fetching from him.
assert_matches!(alice.messages(bob.id).next(), None);
// Have enough time pass that Alice sends a "ping" to Bob.
alice.elapse(KEEP_ALIVE_DELTA);
// Now that the fetch is done, the messages Bob missed should be relayed to him.
alice.fetched(
Fetch {
rid,
namespaces: Namespaces::All,
remote: bob.id,
initiated: true,
},
Ok(vec![]),
);
let mut messages = alice.messages(bob.id);
assert_matches!(
messages.next(),
Some(Message::Announcement(Announcement {
message: AnnouncementMessage::Inventory(InventoryAnnouncement { inventory, .. }),
..
})) if inventory == inventory1
);
assert_matches!(
messages.next(),
Some(Message::Announcement(Announcement {
message: AnnouncementMessage::Inventory(InventoryAnnouncement { inventory, .. }),
..
})) if inventory == inventory2
);
assert_matches!(messages.next(), Some(Message::Ping { .. }));
}
#[test] #[test]
fn test_inventory_relay() { fn test_inventory_relay() {
// Topology is eve <-> alice <-> bob // Topology is eve <-> alice <-> bob