node: Prevent redundant [re-]connections

It was possible to trigger multiple connection attempts to the same
peer, since we didn't track connection attempts that hadn't yet
returned to the service via `Service::attempted`.

With this change, we add an extra `Initial` state to sessions, so
that redundant connections are not attempted when one is already under
way.
This commit is contained in:
Alexis Sellier 2023-03-13 21:58:24 +01:00
parent 413238a1e0
commit cd12741bf2
No known key found for this signature in database
4 changed files with 126 additions and 65 deletions

View File

@ -637,14 +637,15 @@ where
// Inbound connection attempt. // Inbound connection attempt.
} }
pub fn attempted(&mut self, id: NodeId, addr: &Address) { pub fn attempted(&mut self, nid: NodeId, addr: &Address) {
debug!(target: "service", "Attempted connection to {id} ({addr})"); debug!(target: "service", "Attempted connection to {nid} ({addr})");
let persistent = self.config.is_persistent(&id); if let Some(sess) = self.sessions.get_mut(&nid) {
self.sessions sess.to_attempted();
.entry(id) } else {
.and_modify(|sess| sess.to_connecting()) #[cfg(debug_assertions)]
.or_insert_with(|| Session::connecting(id, persistent, self.rng.clone())); panic!("Service::attempted: unknown session {nid}@{addr}");
}
} }
pub fn connected(&mut self, remote: NodeId, link: Link) { pub fn connected(&mut self, remote: NodeId, link: Link) {
@ -666,9 +667,8 @@ where
); );
} }
Entry::Vacant(e) => { Entry::Vacant(e) => {
let peer = e.insert(Session::connected( let peer = e.insert(Session::inbound(
remote, remote,
Link::Inbound,
self.config.is_persistent(&remote), self.config.is_persistent(&remote),
self.rng.clone(), self.rng.clone(),
self.clock, self.clock,
@ -684,40 +684,46 @@ where
debug!(target: "service", "Disconnected from {} ({})", remote, reason); debug!(target: "service", "Disconnected from {} ({})", remote, reason);
if let Some(session) = self.sessions.get_mut(&remote) { let Some(session) = self.sessions.get_mut(&remote) else {
// If the peer disconnected while we were waiting for a [`Message::FetchOk`], if cfg!(debug_assertions) {
// return a failure to any potential fetcher. panic!("Service::disconnected: unknown session {remote}");
if let Some(requested) = session.requesting() {
if let Some(resp) = self.fetch_reqs.remove(&requested) {
resp.send(FetchResult::Failed {
reason: format!("disconnected: {reason}"),
})
.ok();
}
}
// Attempt to re-connect to persistent peers.
if self.config.peer(&remote).is_some() {
if reason.is_transient() {
let delay =
LocalDuration::from_secs(2u64.saturating_pow(session.attempts() as u32))
.clamp(MIN_RECONNECTION_DELTA, MAX_RECONNECTION_DELTA);
session.to_disconnected(since, since + delay);
debug!(target: "service", "Reconnecting to {remote} in {delay}..");
self.reactor.wakeup(delay);
} else {
// TODO: Only handle error transience for non-persistent peers.
warn!(target: "service", "Permanently dropping persistent peer {remote} session due to non-transient error: {reason}");
self.sessions.remove(&remote);
}
} else { } else {
self.sessions.remove(&remote); return;
self.maintain_connections();
} }
};
// If the peer disconnected while we were waiting for a [`Message::FetchOk`],
// return a failure to any potential fetcher.
if let Some(requested) = session.requesting() {
if let Some(resp) = self.fetch_reqs.remove(&requested) {
resp.send(FetchResult::Failed {
reason: format!("disconnected: {reason}"),
})
.ok();
}
}
// Attempt to re-connect to persistent peers.
if self.config.peer(&remote).is_some() {
if reason.is_transient() {
let delay =
LocalDuration::from_secs(2u64.saturating_pow(session.attempts() as u32))
.clamp(MIN_RECONNECTION_DELTA, MAX_RECONNECTION_DELTA);
session.to_disconnected(since, since + delay);
debug!(target: "service", "Reconnecting to {remote} in {delay}..");
self.reactor.wakeup(delay);
} else {
// TODO: Only handle error transience for non-persistent peers.
warn!(target: "service", "Permanently dropping persistent peer {remote} session due to non-transient error: {reason}");
self.sessions.remove(&remote);
}
} else {
self.sessions.remove(&remote);
self.maintain_connections();
} }
} }
@ -1090,7 +1096,7 @@ where
self.reactor self.reactor
.fetch(peer, rid, FetchDirection::Initiator { namespaces }); .fetch(peer, rid, FetchDirection::Initiator { namespaces });
} }
(session::State::Connecting { .. }, msg) => { (session::State::Attempted { .. } | session::State::Initial, msg) => {
error!(target: "service", "Received {:?} from connecting peer {}", msg, peer.id); error!(target: "service", "Received {:?} from connecting peer {}", msg, peer.id);
} }
(session::State::Disconnected { .. }, msg) => { (session::State::Disconnected { .. }, msg) => {
@ -1248,16 +1254,30 @@ where
} }
} }
fn connect(&mut self, node: NodeId, addr: Address) -> bool { fn reconnect(&mut self, nid: NodeId, addr: Address) -> bool {
if self.sessions.is_disconnected(&node) { if let Some(sess) = self.sessions.get_mut(&nid) {
self.reactor.connect(node, addr); sess.to_initial();
self.reactor.connect(nid, addr);
return true; return true;
} }
log::warn!(target: "service", "Attempted connection to peer {node} which already has a session");
false false
} }
fn connect(&mut self, nid: NodeId, addr: Address) -> bool {
if self.sessions.contains_key(&nid) {
log::warn!(target: "service", "Attempted connection to peer {nid} which already has a session");
return false;
}
let persistent = self.config.is_persistent(&nid);
self.sessions
.insert(nid, Session::outbound(nid, persistent, self.rng.clone()));
self.reactor.connect(nid, addr);
true
}
fn seeds(&self, rid: &Id) -> Result<Seeds, Error> { fn seeds(&self, rid: &Id) -> Result<Seeds, Error> {
#[derive(Default)] #[derive(Default)]
pub struct Stats { pub struct Stats {
@ -1439,6 +1459,8 @@ where
/// Maintain persistent peer connections. /// Maintain persistent peer connections.
fn maintain_persistent(&mut self) { fn maintain_persistent(&mut self) {
debug!(target: "service", "Maintaining persistent peers..");
let now = self.local_time(); let now = self.local_time();
let mut reconnect = Vec::new(); let mut reconnect = Vec::new();
@ -1449,8 +1471,6 @@ where
// even a successful attempt means that we're unlikely to be able to reconnect. // even a successful attempt means that we're unlikely to be able to reconnect.
if now >= *retry_at { if now >= *retry_at {
// FIXME: Make sure we don't attempt two concurrent outgoing connections
// to the same peer.
reconnect.push((*nid, addr.clone(), session.attempts())); reconnect.push((*nid, addr.clone(), session.attempts()));
} }
} }
@ -1458,7 +1478,7 @@ where
} }
for (nid, addr, attempts) in reconnect { for (nid, addr, attempts) in reconnect {
if self.connect(nid, addr) { if self.reconnect(nid, addr) {
debug!(target: "service", "Reconnecting to {nid} (attempts={attempts})..."); debug!(target: "service", "Reconnecting to {nid} (attempts={attempts})...");
} }
} }

View File

@ -38,7 +38,9 @@ impl Default for Protocol {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum State { pub enum State {
/// Initial state for outgoing connections. /// Initial state for outgoing connections.
Connecting, Initial,
/// Connection attempted successfully.
Attempted,
/// Initial state after handshake protocol hand-off. /// Initial state after handshake protocol hand-off.
Connected { Connected {
/// Connected since this time. /// Connected since this time.
@ -60,8 +62,11 @@ pub enum State {
impl fmt::Display for State { impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Connecting => { Self::Initial => {
write!(f, "connecting") write!(f, "initial")
}
Self::Attempted => {
write!(f, "attempted")
} }
Self::Connected { protocol, .. } => match protocol { Self::Connected { protocol, .. } => match protocol {
Protocol::Gossip { Protocol::Gossip {
@ -163,10 +168,10 @@ impl fmt::Display for Session {
} }
impl Session { impl Session {
pub fn connecting(id: NodeId, persistent: bool, rng: Rng) -> Self { pub fn outbound(id: NodeId, persistent: bool, rng: Rng) -> Self {
Self { Self {
id, id,
state: State::Connecting, state: State::Initial,
link: Link::Outbound, link: Link::Outbound,
subscribe: None, subscribe: None,
persistent, persistent,
@ -176,7 +181,7 @@ impl Session {
} }
} }
pub fn connected(id: NodeId, link: Link, persistent: bool, rng: Rng, time: LocalTime) -> Self { pub fn inbound(id: NodeId, persistent: bool, rng: Rng, time: LocalTime) -> Self {
Self { Self {
id, id,
state: State::Connected { state: State::Connected {
@ -184,7 +189,7 @@ impl Session {
ping: PingState::default(), ping: PingState::default(),
protocol: Protocol::default(), protocol: Protocol::default(),
}, },
link, link: Link::Inbound,
subscribe: None, subscribe: None,
persistent, persistent,
last_active: LocalTime::default(), last_active: LocalTime::default(),
@ -194,7 +199,7 @@ impl Session {
} }
pub fn is_connecting(&self) -> bool { pub fn is_connecting(&self) -> bool {
matches!(self.state, State::Connecting { .. }) matches!(self.state, State::Attempted { .. })
} }
pub fn is_connected(&self) -> bool { pub fn is_connected(&self) -> bool {
@ -215,6 +220,10 @@ impl Session {
matches!(self.state, State::Disconnected { .. }) matches!(self.state, State::Disconnected { .. })
} }
pub fn is_initial(&self) -> bool {
matches!(self.state, State::Initial)
}
pub fn is_requesting(&self) -> bool { pub fn is_requesting(&self) -> bool {
matches!( matches!(
self.state, self.state,
@ -275,12 +284,12 @@ impl Session {
} }
} }
pub fn to_connecting(&mut self) { pub fn to_attempted(&mut self) {
assert!( assert!(
self.is_disconnected(), self.is_initial(),
"Can only transition to 'connecting' state from 'disconnected' state" "Can only transition to 'attempted' state from 'initial' state"
); );
self.state = State::Connecting; self.state = State::Attempted;
self.attempts += 1; self.attempts += 1;
} }
@ -303,6 +312,16 @@ impl Session {
self.state = State::Disconnected { since, retry_at }; self.state = State::Disconnected { since, retry_at };
} }
/// Return to initial state from disconnected state. This state transition
/// happens when we attempt to re-connect to a disconnected peer.
pub fn to_initial(&mut self) {
assert!(
self.is_disconnected(),
"Can only transition to 'initial' state from 'disconnected' state"
);
self.state = State::Initial;
}
pub fn requesting(&self) -> Option<Id> { pub fn requesting(&self) -> Option<Id> {
if let State::Connected { if let State::Connected {
protocol: Protocol::Gossip { requested }, protocol: Protocol::Gossip { requested },

View File

@ -21,6 +21,7 @@ use crate::service::*;
use crate::storage::git::transport::remote; use crate::storage::git::transport::remote;
use crate::storage::{RemoteId, WriteStorage}; use crate::storage::{RemoteId, WriteStorage};
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::assert_matches;
use crate::test::simulator; use crate::test::simulator;
use crate::test::storage::MockStorage; use crate::test::storage::MockStorage;
use crate::Link; use crate::Link;
@ -272,6 +273,11 @@ where
let remote_addr = simulator::Peer::<S, G>::addr(peer); let remote_addr = simulator::Peer::<S, G>::addr(peer);
self.initialize(); self.initialize();
self.service
.command(Command::Connect(remote_id, remote_addr.clone()));
assert_matches!(self.outbox().next(), Some(Io::Connect { .. }));
self.service.attempted(remote_id, &remote_addr); self.service.attempted(remote_id, &remote_addr);
self.service.connected(remote_id, Link::Outbound); self.service.connected(remote_id, Link::Outbound);

View File

@ -109,6 +109,23 @@ fn test_disconnecting_unresponsive_peer() {
.expect("disconnect an unresponsive bob"); .expect("disconnect an unresponsive bob");
} }
#[test]
fn test_redundant_connect() {
let mut alice = Peer::new("alice", [8, 8, 8, 8]);
let bob = Peer::new("bob", [9, 9, 9, 9]);
alice.command(Command::Connect(bob.id(), bob.address()));
alice.command(Command::Connect(bob.id(), bob.address()));
alice.command(Command::Connect(bob.id(), bob.address()));
// Only one connection attempt is made.
assert_matches!(
alice.outbox().collect::<Vec<_>>().as_slice(),
[Io::Connect(id, addr)]
if *id == bob.id() && *addr == bob.addr()
);
}
#[test] #[test]
fn test_connection_kept_alive() { fn test_connection_kept_alive() {
let mut alice = Peer::new("alice", [8, 8, 8, 8]); let mut alice = Peer::new("alice", [8, 8, 8, 8]);
@ -854,14 +871,13 @@ fn test_persistent_peer_reconnect_success() {
..peer::Config::default() ..peer::Config::default()
}, },
); );
alice.connect_to(&bob);
alice.attempted(bob.id(), &bob.addr());
alice.connected(bob.id(), Link::Outbound);
// A transient error such as this will cause Alice to attempt a reconnection. // A transient error such as this will cause Alice to attempt a reconnection.
let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset)); let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset));
alice.disconnected(bob.id(), &DisconnectReason::Connection(error)); alice.disconnected(bob.id(), &DisconnectReason::Connection(error));
alice.elapse(service::MIN_RECONNECTION_DELTA); alice.elapse(service::MIN_RECONNECTION_DELTA);
alice.elapse(service::MIN_RECONNECTION_DELTA); // Trigger a second wakeup to test idempotence.
alice alice
.outbox() .outbox()