use crate::{
counters,
logging::NetworkSchema,
peer_manager::{self, conn_notifs_channel, ConnectionRequestSender, PeerManagerError},
transport::ConnectionMetadata,
};
use aptos_config::{
config::{Peer, PeerRole, PeerSet},
network_id::NetworkContext,
};
use aptos_crypto::x25519;
use aptos_infallible::RwLock;
use aptos_logger::prelude::*;
use aptos_time_service::{TimeService, TimeServiceTrait};
use aptos_types::{network_address::NetworkAddress, PeerId};
use futures::{
channel::oneshot,
future::{BoxFuture, FutureExt},
stream::{FuturesUnordered, StreamExt},
};
use netcore::transport::ConnectionOrigin;
use num_variants::NumVariants;
use rand::{
prelude::{SeedableRng, SmallRng},
seq::SliceRandom,
};
use serde::Serialize;
use short_hex_str::AsShortHexStr;
use std::{
cmp::{min, Ordering},
collections::{hash_map::Entry, HashMap, HashSet},
fmt, mem,
sync::Arc,
time::{Duration, SystemTime},
};
use tokio_retry::strategy::jitter;
pub mod builder;
#[cfg(test)]
mod test;
const MAX_CONNECTION_DELAY_JITTER: Duration = Duration::from_millis(100);
const TRY_DIAL_BACKOFF_TIME: Duration = Duration::from_secs(300);
pub struct ConnectivityManager<TBackoff> {
network_context: NetworkContext,
time_service: TimeService,
eligible: Arc<RwLock<PeerSet>>,
connected: HashMap<PeerId, ConnectionMetadata>,
discovered_peers: DiscoveredPeerSet,
connection_reqs_tx: ConnectionRequestSender,
connection_notifs_rx: conn_notifs_channel::Receiver,
requests_rx: channel::Receiver<ConnectivityRequest>,
dial_queue: HashMap<PeerId, oneshot::Sender<()>>,
dial_states: HashMap<PeerId, DialState<TBackoff>>,
connectivity_check_interval: Duration,
backoff_strategy: TBackoff,
max_delay: Duration,
event_id: u32,
outbound_connection_limit: Option<usize>,
rng: SmallRng,
mutual_authentication: bool,
}
#[repr(u8)]
#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd, NumVariants, Serialize)]
pub enum DiscoverySource {
OnChainValidatorSet,
File,
Config,
}
impl fmt::Debug for DiscoverySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl fmt::Display for DiscoverySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
DiscoverySource::OnChainValidatorSet => "OnChainValidatorSet",
DiscoverySource::File => "File",
DiscoverySource::Config => "Config",
}
)
}
}
#[derive(Debug, Serialize)]
pub enum ConnectivityRequest {
UpdateDiscoveredPeers(DiscoverySource, PeerSet),
#[serde(skip)]
GetConnectedSize(oneshot::Sender<usize>),
#[serde(skip)]
GetDialQueueSize(oneshot::Sender<usize>),
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
struct DiscoveredPeerSet(HashMap<PeerId, DiscoveredPeer>);
impl DiscoveredPeerSet {
fn get_mut(&mut self, peer_id: &PeerId) -> Option<&mut DiscoveredPeer> {
self.0.get_mut(peer_id)
}
fn try_remove_empty(&mut self, peer_id: &PeerId) -> bool {
match self.0.entry(*peer_id) {
Entry::Occupied(entry) => {
let peer = entry.get();
if peer.addrs.is_empty() && peer.keys.is_empty() {
entry.remove();
true
} else {
false
}
}
Entry::Vacant(_) => true,
}
}
pub fn to_eligible_peers(&self) -> PeerSet {
self.0
.iter()
.filter(|(_, peer)| peer.is_eligible())
.map(|(peer_id, peer)| (*peer_id, peer.into()))
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct DiscoveredPeer {
role: PeerRole,
addrs: Addresses,
keys: PublicKeys,
last_dial_time: SystemTime,
}
impl DiscoveredPeer {
pub fn new(role: PeerRole) -> Self {
Self {
role,
addrs: Addresses::default(),
keys: PublicKeys::default(),
last_dial_time: SystemTime::UNIX_EPOCH,
}
}
pub fn is_eligible(&self) -> bool {
!self.keys.is_empty()
}
pub fn is_eligible_to_be_dialed(&self) -> bool {
self.is_eligible() && !self.addrs.is_empty()
}
pub fn set_last_dial_time(&mut self, time: SystemTime) {
self.last_dial_time = time;
}
pub fn has_dialed_recently(&self, backoff_duration: Duration) -> bool {
if let Ok(duration_since_last_dial) = self.last_dial_time.elapsed() {
duration_since_last_dial < backoff_duration
} else {
false
}
}
}
impl PartialOrd for DiscoveredPeer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_dialed_recently = self.has_dialed_recently(TRY_DIAL_BACKOFF_TIME);
let other_dialed_recently = other.has_dialed_recently(TRY_DIAL_BACKOFF_TIME);
if !self_dialed_recently && other_dialed_recently {
Some(Ordering::Less)
} else if self_dialed_recently && !other_dialed_recently {
Some(Ordering::Greater)
} else {
self.role.partial_cmp(&other.role)
}
}
}
impl From<&DiscoveredPeer> for Peer {
fn from(peer: &DiscoveredPeer) -> Self {
Peer::new(peer.addrs.union(), peer.keys.union(), peer.role)
}
}
#[derive(Clone, Default, PartialEq, Serialize)]
struct Addresses([Vec<NetworkAddress>; DiscoverySource::NUM_VARIANTS]);
#[derive(Clone, Default, PartialEq, Serialize)]
struct PublicKeys([HashSet<x25519::PublicKey>; DiscoverySource::NUM_VARIANTS]);
#[derive(Debug)]
enum DialResult {
Success,
Cancelled,
Failed(PeerManagerError),
}
#[derive(Debug, Clone)]
struct DialState<TBackoff> {
backoff: TBackoff,
addr_idx: usize,
}
impl<TBackoff> ConnectivityManager<TBackoff>
where
TBackoff: Iterator<Item = Duration> + Clone,
{
pub fn new(
network_context: NetworkContext,
time_service: TimeService,
eligible: Arc<RwLock<PeerSet>>,
seeds: PeerSet,
connection_reqs_tx: ConnectionRequestSender,
connection_notifs_rx: conn_notifs_channel::Receiver,
requests_rx: channel::Receiver<ConnectivityRequest>,
connectivity_check_interval: Duration,
backoff_strategy: TBackoff,
max_delay: Duration,
outbound_connection_limit: Option<usize>,
mutual_authentication: bool,
) -> Self {
assert!(
eligible.read().is_empty(),
"Eligible peers must be initially empty. eligible: {:?}",
eligible
);
info!(
NetworkSchema::new(&network_context),
"{} Initialized connectivity manager", network_context
);
let mut connmgr = Self {
network_context,
time_service,
eligible,
connected: HashMap::new(),
discovered_peers: DiscoveredPeerSet::default(),
connection_reqs_tx,
connection_notifs_rx,
requests_rx,
dial_queue: HashMap::new(),
dial_states: HashMap::new(),
connectivity_check_interval,
backoff_strategy,
max_delay,
event_id: 0,
outbound_connection_limit,
rng: SmallRng::from_entropy(),
mutual_authentication,
};
connmgr.handle_update_discovered_peers(DiscoverySource::Config, seeds);
connmgr
}
pub async fn start(mut self) {
let mut pending_dials = FuturesUnordered::new();
let ticker = self.time_service.interval(self.connectivity_check_interval);
tokio::pin!(ticker);
info!(
NetworkSchema::new(&self.network_context),
"{} Starting ConnectivityManager actor", self.network_context
);
loop {
self.event_id = self.event_id.wrapping_add(1);
futures::select! {
_ = ticker.select_next_some() => {
self.check_connectivity(&mut pending_dials).await;
},
req = self.requests_rx.select_next_some() => {
self.handle_request(req);
},
maybe_notif = self.connection_notifs_rx.next() => {
match maybe_notif {
Some(notif) => {
self.handle_control_notification(notif.clone());
},
None => break,
}
},
peer_id = pending_dials.select_next_some() => {
trace!(
NetworkSchema::new(&self.network_context)
.remote_peer(&peer_id),
"{} Dial complete to {}",
self.network_context,
peer_id.short_str(),
);
self.dial_queue.remove(&peer_id);
},
}
}
warn!(
NetworkSchema::new(&self.network_context),
"{} ConnectivityManager actor terminated", self.network_context
);
}
async fn close_stale_connections(&mut self) {
let eligible = self.eligible.read().clone();
let stale_connections: Vec<_> = self
.connected
.iter()
.filter(|(peer_id, _)| !eligible.contains_key(peer_id))
.filter_map(|(peer_id, metadata)| {
if !self.mutual_authentication
&& metadata.origin == ConnectionOrigin::Inbound
&& metadata.role == PeerRole::Unknown
{
None
} else {
Some(*peer_id)
}
})
.collect();
for p in stale_connections.into_iter() {
info!(
NetworkSchema::new(&self.network_context).remote_peer(&p),
"{} Closing stale connection to peer {}",
self.network_context,
p.short_str()
);
if let Err(e) = self.connection_reqs_tx.disconnect_peer(p).await {
info!(
NetworkSchema::new(&self.network_context)
.remote_peer(&p),
error = %e,
"{} Failed to close stale connection to peer {} : {}",
self.network_context,
p.short_str(),
e
);
}
}
}
async fn cancel_stale_dials(&mut self) {
let eligible = self.eligible.read().clone();
let stale_dials: Vec<_> = self
.dial_queue
.keys()
.filter(|peer_id| !eligible.contains_key(peer_id))
.cloned()
.collect();
for p in stale_dials.into_iter() {
debug!(
NetworkSchema::new(&self.network_context).remote_peer(&p),
"{} Cancelling stale dial {}",
self.network_context,
p.short_str()
);
self.dial_queue.remove(&p);
}
}
fn dial_eligible_peers<'a>(
&'a mut self,
pending_dials: &'a mut FuturesUnordered<BoxFuture<'static, PeerId>>,
) {
let to_connect = self.choose_peers_to_dial();
for (peer_id, peer) in to_connect {
self.queue_dial_peer(peer_id, peer, pending_dials);
}
}
fn choose_peers_to_dial(&mut self) -> Vec<(PeerId, DiscoveredPeer)> {
let network_id = self.network_context.network_id();
let role = self.network_context.role();
let roles_to_dial = network_id.upstream_roles(&role);
let mut eligible: Vec<_> = self
.discovered_peers
.0
.iter()
.filter(|(peer_id, peer)| {
peer.is_eligible_to_be_dialed() && !self.connected.contains_key(peer_id) && !self.dial_queue.contains_key(peer_id) && roles_to_dial.contains(&peer.role) })
.collect();
eligible.shuffle(&mut self.rng);
eligible
.sort_by(|(_, peer), (_, other)| peer.partial_cmp(other).unwrap_or(Ordering::Equal));
let num_eligible = eligible.len();
let to_connect = if let Some(conn_limit) = self.outbound_connection_limit {
let outbound_connections = self
.connected
.iter()
.filter(|(_, metadata)| metadata.origin == ConnectionOrigin::Outbound)
.count();
min(
conn_limit
.saturating_sub(outbound_connections.saturating_add(self.dial_queue.len())),
num_eligible,
)
} else {
num_eligible
};
eligible
.iter()
.take(to_connect)
.map(|(peer_id, peer)| (**peer_id, (*peer).clone()))
.collect()
}
fn queue_dial_peer<'a>(
&'a mut self,
peer_id: PeerId,
peer: DiscoveredPeer,
pending_dials: &'a mut FuturesUnordered<BoxFuture<'static, PeerId>>,
) {
counters::peer_connected(&self.network_context, &peer_id, 0);
let connection_reqs_tx = self.connection_reqs_tx.clone();
let init_dial_state = DialState::new(self.backoff_strategy.clone());
let dial_state = self
.dial_states
.entry(peer_id)
.or_insert_with(|| init_dial_state);
let addr = dial_state.next_addr(&peer.addrs).clone();
let dial_delay = dial_state.next_backoff_delay(self.max_delay);
let f_delay = self.time_service.sleep(dial_delay);
let (cancel_tx, cancel_rx) = oneshot::channel();
let network_context = self.network_context;
let f = async move {
let dial_result = futures::select! {
_ = f_delay.fuse() => {
info!(
NetworkSchema::new(&network_context)
.remote_peer(&peer_id)
.network_address(&addr),
"{} Dialing peer {} at {}",
network_context,
peer_id.short_str(),
addr
);
match connection_reqs_tx.dial_peer(peer_id, addr.clone()).await {
Ok(_) => DialResult::Success,
Err(e) => DialResult::Failed(e),
}
},
_ = cancel_rx.fuse() => DialResult::Cancelled,
};
log_dial_result(network_context, peer_id, addr, dial_result);
peer_id
};
pending_dials.push(f.boxed());
if let Some(discovered_peer) = self.discovered_peers.get_mut(&peer_id) {
discovered_peer.set_last_dial_time(SystemTime::now())
}
self.dial_queue.insert(peer_id, cancel_tx);
}
async fn check_connectivity<'a>(
&'a mut self,
pending_dials: &'a mut FuturesUnordered<BoxFuture<'static, PeerId>>,
) {
trace!(
NetworkSchema::new(&self.network_context),
"{} Checking connectivity",
self.network_context
);
sample!(SampleRate::Duration(Duration::from_secs(60)), {
info!(
NetworkSchema::new(&self.network_context),
discovered_peers = ?self.discovered_peers,
"Current eligible peers"
)
});
self.cancel_stale_dials().await;
self.close_stale_connections().await;
self.dial_eligible_peers(pending_dials);
}
fn reset_dial_state(&mut self, peer_id: &PeerId) {
if let Some(dial_state) = self.dial_states.get_mut(peer_id) {
*dial_state = DialState::new(self.backoff_strategy.clone());
}
}
fn handle_request(&mut self, req: ConnectivityRequest) {
trace!(
NetworkSchema::new(&self.network_context),
connectivity_request = req,
"{} Handling ConnectivityRequest",
self.network_context
);
match req {
ConnectivityRequest::UpdateDiscoveredPeers(src, discovered_peers) => {
trace!(
NetworkSchema::new(&self.network_context),
"{} Received updated list of discovered peers: src: {:?}",
self.network_context,
src,
);
self.handle_update_discovered_peers(src, discovered_peers);
}
ConnectivityRequest::GetDialQueueSize(sender) => {
sender.send(self.dial_queue.len()).unwrap();
}
ConnectivityRequest::GetConnectedSize(sender) => {
sender.send(self.connected.len()).unwrap();
}
}
}
fn handle_update_discovered_peers(
&mut self,
src: DiscoverySource,
new_discovered_peers: PeerSet,
) {
let self_peer_id = self.network_context.peer_id();
let mut keys_updated = false;
let mut peers_to_check_remove = Vec::new();
for (peer_id, peer) in self.discovered_peers.0.iter_mut() {
let new_peer = new_discovered_peers.get(peer_id);
let check_remove = if let Some(new_peer) = new_peer {
if new_peer.keys.is_empty() {
keys_updated |= peer.keys.clear_src(src);
}
if new_peer.addresses.is_empty() {
peer.addrs.clear_src(src);
}
new_peer.addresses.is_empty() && new_peer.keys.is_empty()
} else {
keys_updated |= peer.keys.clear_src(src);
peer.addrs.clear_src(src);
true
};
if check_remove {
peers_to_check_remove.push(*peer_id);
}
}
for peer_id in peers_to_check_remove {
self.discovered_peers.try_remove_empty(&peer_id);
}
for (peer_id, discovered_peer) in new_discovered_peers {
if peer_id == self_peer_id {
continue;
}
let peer = self
.discovered_peers
.0
.entry(peer_id)
.or_insert_with(|| DiscoveredPeer::new(discovered_peer.role));
let mut peer_updated = false;
if peer.keys.update(src, discovered_peer.keys) {
info!(
NetworkSchema::new(&self.network_context)
.remote_peer(&peer_id)
.discovery_source(&src),
"{} pubkey sets updated for peer: {}, pubkeys: {}",
self.network_context,
peer_id.short_str(),
peer.keys
);
keys_updated = true;
peer_updated = true;
}
if peer.addrs.update(src, discovered_peer.addresses) {
info!(
NetworkSchema::new(&self.network_context).remote_peer(&peer_id),
network_addresses = &peer.addrs,
"{} addresses updated for peer: {}, update src: {:?}, addrs: {}",
self.network_context,
peer_id.short_str(),
src,
&peer.addrs,
);
peer_updated = true;
}
if peer_updated {
self.reset_dial_state(&peer_id)
}
}
if keys_updated {
let new_eligible = self.discovered_peers.to_eligible_peers();
let _old_eligible = {
let mut eligible = self.eligible.write();
mem::replace(&mut *eligible, new_eligible)
};
}
}
fn handle_control_notification(&mut self, notif: peer_manager::ConnectionNotification) {
trace!(
NetworkSchema::new(&self.network_context),
connection_notification = notif,
"Connection notification"
);
match notif {
peer_manager::ConnectionNotification::NewPeer(metadata, _context) => {
let peer_id = metadata.remote_peer_id;
counters::peer_connected(&self.network_context, &peer_id, 1);
self.connected.insert(peer_id, metadata);
self.dial_states.remove(&peer_id);
self.dial_queue.remove(&peer_id);
}
peer_manager::ConnectionNotification::LostPeer(metadata, _context, _reason) => {
let peer_id = metadata.remote_peer_id;
if let Some(stored_metadata) = self.connected.get(&peer_id) {
counters::peer_connected(&self.network_context, &peer_id, 0);
info!(
NetworkSchema::new(&self.network_context)
.remote_peer(&peer_id)
.connection_metadata(&metadata),
stored_metadata = stored_metadata,
"{} Removing peer '{}' metadata: {}, vs event metadata: {}",
self.network_context,
peer_id.short_str(),
stored_metadata,
metadata
);
self.connected.remove(&peer_id);
} else {
info!(
NetworkSchema::new(&self.network_context)
.remote_peer(&peer_id)
.connection_metadata(&metadata),
"{} Ignoring stale lost peer event for peer: {}, addr: {}",
self.network_context,
peer_id.short_str(),
metadata.addr
);
}
}
}
}
}
fn log_dial_result(
network_context: NetworkContext,
peer_id: PeerId,
addr: NetworkAddress,
dial_result: DialResult,
) {
match dial_result {
DialResult::Success => {
info!(
NetworkSchema::new(&network_context)
.remote_peer(&peer_id)
.network_address(&addr),
"{} Successfully connected to peer: {} at address: {}",
network_context,
peer_id.short_str(),
addr
);
}
DialResult::Cancelled => {
info!(
NetworkSchema::new(&network_context).remote_peer(&peer_id),
"{} Cancelled pending dial to peer: {}",
network_context,
peer_id.short_str()
);
}
DialResult::Failed(err) => match err {
PeerManagerError::AlreadyConnected(a) => {
info!(
NetworkSchema::new(&network_context)
.remote_peer(&peer_id)
.network_address(&a),
"{} Already connected to peer: {} at address: {}",
network_context,
peer_id.short_str(),
a
);
}
e => {
info!(
NetworkSchema::new(&network_context)
.remote_peer(&peer_id)
.network_address(&addr),
error = %e,
"{} Failed to connect to peer: {} at address: {}; error: {}",
network_context,
peer_id.short_str(),
addr,
e
);
}
},
}
}
impl DiscoverySource {
fn as_usize(self) -> usize {
self as u8 as usize
}
}
impl Addresses {
fn len(&self) -> usize {
self.0.iter().map(Vec::len).sum()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn update(&mut self, src: DiscoverySource, addrs: Vec<NetworkAddress>) -> bool {
let src_idx = src.as_usize();
if self.0[src_idx] != addrs {
self.0[src_idx] = addrs;
true
} else {
false
}
}
fn clear_src(&mut self, src: DiscoverySource) -> bool {
self.update(src, Vec::new())
}
fn get(&self, idx: usize) -> Option<&NetworkAddress> {
self.0.iter().flatten().nth(idx)
}
fn union(&self) -> Vec<NetworkAddress> {
let set: HashSet<_> = self.0.iter().flatten().cloned().collect();
set.into_iter().collect()
}
}
impl fmt::Display for Addresses {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl fmt::Debug for Addresses {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl PublicKeys {
fn len(&self) -> usize {
self.0.iter().map(HashSet::len).sum()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn update(&mut self, src: DiscoverySource, pubkeys: HashSet<x25519::PublicKey>) -> bool {
let src_idx = src.as_usize();
if self.0[src_idx] != pubkeys {
self.0[src_idx] = pubkeys;
true
} else {
false
}
}
fn clear_src(&mut self, src: DiscoverySource) -> bool {
self.update(src, HashSet::new())
}
fn union(&self) -> HashSet<x25519::PublicKey> {
self.0.iter().flatten().copied().collect()
}
}
impl fmt::Display for PublicKeys {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl fmt::Debug for PublicKeys {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl<TBackoff> DialState<TBackoff>
where
TBackoff: Iterator<Item = Duration> + Clone,
{
fn new(backoff: TBackoff) -> Self {
Self {
backoff,
addr_idx: 0,
}
}
fn next_addr<'a>(&mut self, addrs: &'a Addresses) -> &'a NetworkAddress {
assert!(!addrs.is_empty());
let addr_idx = self.addr_idx;
self.addr_idx = self.addr_idx.wrapping_add(1);
addrs.get(addr_idx % addrs.len()).unwrap()
}
fn next_backoff_delay(&mut self, max_delay: Duration) -> Duration {
let jitter = jitter(MAX_CONNECTION_DELAY_JITTER);
min(max_delay, self.backoff.next().unwrap_or(max_delay)) + jitter
}
}