pub mod atom_cache;
pub mod connection;
pub mod connection_events;
pub mod control;
pub mod control_link;
pub mod etf;
pub mod global;
pub mod handshake;
mod node;
pub mod pg;
pub mod remote_link;
pub mod resolver;
pub mod sender;
pub use connection::ConnectionManager;
pub use connection_events::{
ConnectionEvent, ConnectionGeneration, NodeDown, NodeUp, SubscriberId,
};
pub use node::{DEFAULT_NODE_NAME, Node};
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tokio::runtime::Runtime;
pub use resolver::{NodeResolver, ResolveError, ResolveFuture, Resolver, StaticResolver};
pub const DEFAULT_COOKIE: &str = "beamr-cookie";
pub const NET_KERNEL_CONNECT_DEADLINE: Duration = Duration::from_secs(15);
pub const NET_KERNEL_THREAD_NAME: &str = "beamr-net-kernel";
#[derive(Clone)]
pub struct DistributionConfig {
pub resolver: Resolver,
pub cookie: String,
}
struct NetKernelRuntime {
runtime: Mutex<Option<Runtime>>,
mark: u64,
}
impl NetKernelRuntime {
fn shutdown(&self) {
let runtime = self
.runtime
.lock()
.unwrap_or_else(|error| error.into_inner())
.take();
join_runtime_drop(runtime, self.mark);
}
}
impl Drop for NetKernelRuntime {
fn drop(&mut self) {
let runtime = self
.runtime
.get_mut()
.unwrap_or_else(|error| error.into_inner())
.take();
join_runtime_drop(runtime, self.mark);
}
}
static RUNTIME_MARK_COUNTER: AtomicU64 = AtomicU64::new(1);
thread_local! {
static RUNTIME_MARK: Cell<u64> = const { Cell::new(0) };
}
pub(crate) fn mint_runtime_mark() -> u64 {
RUNTIME_MARK_COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub(crate) fn stamp_runtime_threads(builder: &mut tokio::runtime::Builder, mark: u64) {
builder.on_thread_start(move || RUNTIME_MARK.with(|slot| slot.set(mark)));
}
pub(crate) fn join_runtime_drop(runtime: Option<Runtime>, own_mark: u64) {
let Some(runtime) = runtime else {
return;
};
if RUNTIME_MARK.with(Cell::get) == own_mark {
runtime.shutdown_background();
} else if tokio::runtime::Handle::try_current().is_ok() {
let joiner = thread::spawn(move || drop(runtime));
let _ = joiner.join();
} else {
drop(runtime);
}
}
#[derive(Clone)]
pub struct NetKernel {
connections: ConnectionManager,
runtime: Arc<NetKernelRuntime>,
}
impl NetKernel {
#[must_use]
pub fn new(connections: ConnectionManager) -> Self {
let mark = mint_runtime_mark();
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder
.worker_threads(1)
.thread_name(NET_KERNEL_THREAD_NAME)
.enable_all();
stamp_runtime_threads(&mut builder, mark);
let runtime = builder.build().ok();
Self {
connections,
runtime: Arc::new(NetKernelRuntime {
runtime: Mutex::new(runtime),
mark,
}),
}
}
#[must_use]
pub fn connection_manager(&self) -> &ConnectionManager {
&self.connections
}
#[must_use]
pub fn worker_thread_names(&self) -> Vec<String> {
if self
.runtime
.runtime
.lock()
.unwrap_or_else(|error| error.into_inner())
.is_some()
{
vec![NET_KERNEL_THREAD_NAME.to_owned()]
} else {
Vec::new()
}
}
pub fn shutdown(&self) {
self.runtime.shutdown();
}
pub fn connect_node(&self, node: crate::atom::Atom) -> bool {
self.connect_node_with_deadline(node, NET_KERNEL_CONNECT_DEADLINE)
}
#[cfg(any(test, feature = "test-support"))]
pub fn connect_node_deadline_for_test(
&self,
node: crate::atom::Atom,
deadline: Duration,
) -> bool {
self.connect_node_with_deadline(node, deadline)
}
fn connect_node_with_deadline(&self, node: crate::atom::Atom, deadline: Duration) -> bool {
if self.connections.get_connection(node).is_some() {
return true;
}
let guard = self
.runtime
.runtime
.lock()
.unwrap_or_else(|error| error.into_inner());
let Some(runtime) = guard.as_ref() else {
return false;
};
let connections = self.connections.clone();
let connect = || async {
tokio::time::timeout(deadline, connections.connect_node(node))
.await
.unwrap_or(false)
};
if tokio::runtime::Handle::try_current().is_ok() {
thread::scope(|scope| {
scope
.spawn(|| runtime.block_on(connect()))
.join()
.unwrap_or(false)
})
} else {
runtime.block_on(connect())
}
}
#[must_use]
pub fn nodes(&self) -> Vec<crate::atom::Atom> {
self.connections.connected_nodes()
}
pub fn disconnect_node(&self, node: crate::atom::Atom) -> bool {
self.connections.disconnect_node(node)
}
}
impl fmt::Debug for NetKernel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("NetKernel")
.field("connection_count", &self.connections.connection_count())
.finish()
}
}
impl Default for DistributionConfig {
fn default() -> Self {
Self {
resolver: Arc::new(StaticResolver::new(HashMap::new())),
cookie: DEFAULT_COOKIE.to_owned(),
}
}
}
impl fmt::Debug for DistributionConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DistributionConfig")
.field("resolver", &"<node resolver>")
.field("cookie", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod connection_events_tests;
#[cfg(test)]
mod pg_tests;
#[cfg(test)]
mod net_kernel_tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
use super::*;
use crate::atom::AtomTable;
use crate::distribution::connection::ConnectionManager;
use crate::distribution::resolver::{NodeResolver, ResolveFuture};
struct StalledResolver {
entered: Arc<AtomicBool>,
}
impl NodeResolver for StalledResolver {
fn resolve<'a>(&'a self, _name: &'a str) -> ResolveFuture<'a> {
self.entered.store(true, Ordering::Release);
Box::pin(std::future::pending())
}
}
#[test]
fn stalled_resolver_cannot_wedge_net_kernel_shutdown() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let entered = Arc::new(AtomicBool::new(false));
let manager = ConnectionManager::new(
Arc::clone(&atom_table),
Arc::new(StalledResolver {
entered: Arc::clone(&entered),
}),
"test-cookie",
"local@test",
0,
);
let net_kernel = NetKernel::new(manager);
let node = atom_table.intern("peer@stalled");
let on_thread = net_kernel.clone();
let connect = thread::spawn(move || {
on_thread.connect_node_deadline_for_test(node, Duration::from_millis(200))
});
let poll_deadline = Instant::now() + Duration::from_secs(10);
while !entered.load(Ordering::Acquire) {
assert!(
Instant::now() < poll_deadline,
"resolver never entered; connect thread failed to start"
);
thread::sleep(Duration::from_millis(2));
}
let started = Instant::now();
net_kernel.shutdown();
assert!(
started.elapsed() < Duration::from_secs(8),
"shutdown must complete once the connect deadline fires"
);
assert!(
!connect.join().unwrap_or(true),
"a timed-out connect reports false"
);
assert!(
net_kernel.worker_thread_names().is_empty(),
"post-shutdown net-kernel inventory reports no live worker"
);
}
}