use std::{
convert::Infallible,
fmt,
task::{Context, Poll},
time::{Duration, Instant},
};
use libp2p_core::{transport::PortUse, Endpoint, Multiaddr};
use libp2p_identity::PeerId;
use libp2p_swarm::{
dummy, ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent,
THandlerOutEvent, ToSwarm,
};
use sysinfo::MemoryRefreshKind;
pub struct Behaviour {
max_allowed_bytes: usize,
process_physical_memory_bytes: usize,
last_refreshed: Instant,
}
const MAX_STALE_DURATION: Duration = Duration::from_millis(100);
impl Behaviour {
pub fn with_max_bytes(max_allowed_bytes: usize) -> Self {
Self {
max_allowed_bytes,
process_physical_memory_bytes: memory_stats::memory_stats()
.map(|s| s.physical_mem)
.unwrap_or_default(),
last_refreshed: Instant::now(),
}
}
pub fn with_max_percentage(percentage: f64) -> Self {
use sysinfo::{RefreshKind, System};
let system_memory_bytes = System::new_with_specifics(
RefreshKind::default().with_memory(MemoryRefreshKind::default().with_ram()),
)
.total_memory();
Self::with_max_bytes((system_memory_bytes as f64 * percentage).round() as usize)
}
pub fn max_allowed_bytes(&self) -> usize {
self.max_allowed_bytes
}
fn check_limit(&mut self) -> Result<(), ConnectionDenied> {
self.refresh_memory_stats_if_needed();
if self.process_physical_memory_bytes > self.max_allowed_bytes {
return Err(ConnectionDenied::new(MemoryUsageLimitExceeded {
process_physical_memory_bytes: self.process_physical_memory_bytes,
max_allowed_bytes: self.max_allowed_bytes,
}));
}
Ok(())
}
fn refresh_memory_stats_if_needed(&mut self) {
let now = Instant::now();
if self.last_refreshed + MAX_STALE_DURATION > now {
return;
}
let Some(stats) = memory_stats::memory_stats() else {
tracing::warn!("Failed to retrieve process memory stats");
return;
};
self.last_refreshed = now;
self.process_physical_memory_bytes = stats.physical_mem;
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = dummy::ConnectionHandler;
type ToSwarm = Infallible;
fn handle_pending_inbound_connection(
&mut self,
_: ConnectionId,
_: &Multiaddr,
_: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.check_limit()
}
fn handle_established_inbound_connection(
&mut self,
_: ConnectionId,
_: PeerId,
_: &Multiaddr,
_: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(dummy::ConnectionHandler)
}
fn handle_pending_outbound_connection(
&mut self,
_: ConnectionId,
_: Option<PeerId>,
_: &[Multiaddr],
_: Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
self.check_limit()?;
Ok(vec![])
}
fn handle_established_outbound_connection(
&mut self,
_: ConnectionId,
_: PeerId,
_: &Multiaddr,
_: Endpoint,
_: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(dummy::ConnectionHandler)
}
fn on_swarm_event(&mut self, _: FromSwarm) {}
fn on_connection_handler_event(
&mut self,
_id: PeerId,
_: ConnectionId,
event: THandlerOutEvent<Self>,
) {
libp2p_core::util::unreachable(event)
}
fn poll(&mut self, _: &mut Context<'_>) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
Poll::Pending
}
}
#[derive(Debug, Clone, Copy)]
pub struct MemoryUsageLimitExceeded {
process_physical_memory_bytes: usize,
max_allowed_bytes: usize,
}
impl MemoryUsageLimitExceeded {
pub fn process_physical_memory_bytes(&self) -> usize {
self.process_physical_memory_bytes
}
pub fn max_allowed_bytes(&self) -> usize {
self.max_allowed_bytes
}
}
impl std::error::Error for MemoryUsageLimitExceeded {}
impl fmt::Display for MemoryUsageLimitExceeded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"process physical memory usage limit exceeded: process memory: {} bytes, max allowed: {} bytes",
self.process_physical_memory_bytes,
self.max_allowed_bytes,
)
}
}