use std::collections::{HashMap, HashSet, VecDeque};
use std::net::{IpAddr, SocketAddr};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use arc_swap::ArcSwap;
use kinship_core::{
Command, CommandError, CommandId, CommandOutput, HeaderCheck, Instant, Key, KeyId, Member,
Node, State, StreamEvent, StreamId, Transmit,
};
use tokio::sync::{mpsc, oneshot};
use tokio::task::{AbortHandle, JoinSet};
use tokio::time::{Instant as TokioInstant, sleep_until};
use crate::conn::{self, Budget, Inbound, Report, Reports, Write};
use crate::events::{Event, Hub};
use crate::seed::{self, Seed};
use crate::transport::Transport;
use crate::{Error, Stats};
const MAX_DATAGRAM: usize = 65_536;
const MAX_BATCH: usize = 16_384;
const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
const RECV_BACKOFF: Duration = Duration::from_millis(10);
#[derive(Debug)]
pub(crate) enum Request {
Join {
seeds: Vec<SocketAddr>,
reply: oneshot::Sender<Result<usize, Error>>,
},
Leave {
reply: oneshot::Sender<Result<(), Error>>,
},
SetMeta {
meta: Vec<u8>,
reply: oneshot::Sender<Result<(), Error>>,
},
Keyring {
change: KeyChange,
reply: oneshot::Sender<Result<(), Error>>,
},
Close {
reply: oneshot::Sender<()>,
},
#[cfg(any(test, feature = "test-hooks"))]
Panic,
}
#[derive(Debug)]
pub(crate) enum KeyChange {
Install(Key),
Use(Key),
Remove(Key),
}
impl KeyChange {
fn command(self) -> (Command, &'static str, KeyId) {
match self {
Self::Install(k) => {
let id = k.key_id();
(Command::InstallKey(k), "installed", id)
}
Self::Use(k) => {
let id = k.key_id();
(Command::UseKey(k), "now encrypting with", id)
}
Self::Remove(k) => {
let id = k.key_id();
(Command::RemoveKey(k), "removed", id)
}
}
}
}
#[derive(Debug)]
pub(crate) struct Shared {
pub snapshot: ArcSwap<Snapshot>,
pub key_ids: ArcSwap<Vec<KeyId>>,
pub stats: Mutex<Stats>,
pub hub: Hub,
}
impl Shared {
pub fn stats(&self) -> MutexGuard<'_, Stats> {
self.stats.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Snapshot {
pub all: Vec<Member>,
}
impl Snapshot {
pub fn of(node: &Node) -> Self {
Self {
all: node.all_members().cloned().collect(),
}
}
pub fn local(&self) -> &Member {
&self.all[0]
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Clock {
start: TokioInstant,
}
impl Clock {
pub fn new() -> Self {
Self {
start: TokioInstant::now(),
}
}
pub fn now(&self) -> Instant {
let elapsed = TokioInstant::now().saturating_duration_since(self.start);
Instant::from_nanos(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX))
}
fn to_tokio(self, t: Instant) -> TokioInstant {
self.start
.checked_add(Duration::from_nanos(t.as_nanos()))
.unwrap_or_else(far_future)
}
}
fn far_future() -> TokioInstant {
TokioInstant::now() + Duration::from_secs(365 * 24 * 3600)
}
#[derive(Debug)]
enum Pending {
Join(oneshot::Sender<Result<usize, Error>>),
Done(oneshot::Sender<Result<(), Error>>),
Keyring {
reply: oneshot::Sender<Result<(), Error>>,
did: &'static str,
key: KeyId,
},
Rejoin {
alone: bool,
},
}
#[derive(Debug)]
struct Conn {
writes: mpsc::UnboundedSender<Write>,
abort: AbortHandle,
}
#[derive(Debug)]
pub(crate) struct Options {
pub limits: conn::Limits,
pub max_inbound_streams: usize,
pub max_inbound_streams_per_ip: usize,
pub max_inbound_bytes_per_ip: usize,
pub seeds: Vec<Seed>,
pub rejoin_interval: Duration,
}
pub(crate) struct Actor<T: Transport> {
node: Node,
clock: Clock,
transport: Arc<T>,
shared: Arc<Shared>,
requests: mpsc::Receiver<Request>,
reports_tx: Reports,
reports: mpsc::UnboundedReceiver<(StreamId, Report)>,
conns: HashMap<StreamId, Conn>,
inbound: VecDeque<(StreamId, IpAddr)>,
budget: Arc<Budget>,
header: Arc<ArcSwap<HeaderCheck>>,
next_inbound: u64,
tasks: JoinSet<()>,
pending: HashMap<CommandId, Pending>,
opts: Options,
next_rejoin: Option<Instant>,
rejoining: bool,
resolved_tx: mpsc::UnboundedSender<Vec<SocketAddr>>,
resolved: mpsc::UnboundedReceiver<Vec<SocketAddr>>,
accept_paused: Option<TokioInstant>,
recv_paused: Option<TokioInstant>,
events: Vec<Event>,
}
enum Input<S> {
Datagram(std::io::Result<(usize, SocketAddr)>),
Report((StreamId, Report)),
Accept(std::io::Result<(S, SocketAddr)>),
Request(Option<Request>),
Resolved(Vec<SocketAddr>),
Timer,
}
impl<T: Transport> Actor<T> {
pub fn new(
node: Node,
clock: Clock,
transport: Arc<T>,
shared: Arc<Shared>,
requests: mpsc::Receiver<Request>,
opts: Options,
) -> Self {
let (reports_tx, reports) = mpsc::unbounded_channel();
let (resolved_tx, resolved) = mpsc::unbounded_channel();
let next_rejoin = (!opts.rejoin_interval.is_zero() && !opts.seeds.is_empty())
.then(|| clock.now() + opts.rejoin_interval);
let budget =
Budget::for_frames(opts.limits.max_stream_frame, opts.max_inbound_bytes_per_ip);
let header = Arc::new(ArcSwap::from_pointee(node.header_check()));
Self {
node,
clock,
transport,
shared,
requests,
reports_tx,
reports,
conns: HashMap::new(),
inbound: VecDeque::new(),
budget,
header,
next_inbound: 0,
tasks: JoinSet::new(),
pending: HashMap::new(),
opts,
next_rejoin,
rejoining: false,
resolved_tx,
resolved,
accept_paused: None,
recv_paused: None,
events: Vec::new(),
}
}
pub async fn run(mut self) {
let mut buf = vec![0; MAX_DATAGRAM];
let close = loop {
let wake = self.wake_at();
let reading = self.recv_paused.is_none();
let accepting = self.accept_paused.is_none();
let input = tokio::select! {
biased;
r = self.transport.recv_datagram(&mut buf), if reading => Input::Datagram(r),
Some(r) = self.reports.recv() => Input::Report(r),
r = self.transport.accept(), if accepting => Input::Accept(r),
r = self.requests.recv() => Input::Request(r),
Some(addrs) = self.resolved.recv() => Input::Resolved(addrs),
() = sleep_until(wake) => Input::Timer,
};
match input {
Input::Datagram(Ok((n, from))) => self.datagram(&buf[..n], from),
Input::Datagram(Err(e)) => {
tracing::warn!(error = %e, "receiving a datagram failed");
self.recv_paused = Some(TokioInstant::now() + RECV_BACKOFF);
}
Input::Report((conn, r)) => self.report(conn, r),
Input::Accept(Ok((stream, from))) => self.accept(stream, from),
Input::Accept(Err(e)) => {
tracing::warn!(error = %e, "accepting a connection failed");
self.accept_paused = Some(TokioInstant::now() + ACCEPT_BACKOFF);
}
Input::Request(Some(Request::Close { reply })) => break Some(reply),
Input::Request(Some(req)) => self.request(req),
Input::Request(None) => break None,
Input::Resolved(addrs) => self.rejoin_resolved(addrs),
Input::Timer => {}
}
self.drain(&mut buf);
self.timers();
self.publish_stats();
};
drop(self);
if let Some(reply) = close {
let _ = reply.send(());
}
}
fn wake_at(&self) -> TokioInstant {
let core = [self.node.poll_timeout(), self.next_rejoin]
.into_iter()
.flatten()
.min()
.map(|t| self.clock.to_tokio(t));
[core, self.accept_paused, self.recv_paused]
.into_iter()
.flatten()
.min()
.unwrap_or_else(far_future)
}
fn drain(&mut self, buf: &mut [u8]) {
for _ in 0..MAX_BATCH {
let mut any = false;
if self.recv_paused.is_none() {
match self.transport.try_recv_datagram(buf) {
Ok((n, from)) => {
self.datagram(&buf[..n], from);
any = true;
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => {
tracing::warn!(error = %e, "receiving a datagram failed");
self.recv_paused = Some(TokioInstant::now() + RECV_BACKOFF);
}
}
}
if let Ok((conn, r)) = self.reports.try_recv() {
self.report(conn, r);
any = true;
}
if !any {
return;
}
}
}
fn timers(&mut self) {
let now = self.clock.now();
if self.node.poll_timeout().is_some_and(|t| t <= now) {
self.node.handle_timeout(now);
self.flush(true);
}
self.rejoin(now);
let tokio_now = TokioInstant::now();
if self.accept_paused.is_some_and(|t| t <= tokio_now) {
self.accept_paused = None;
}
if self.recv_paused.is_some_and(|t| t <= tokio_now) {
self.recv_paused = None;
}
}
fn rejoin(&mut self, now: Instant) {
let Some(at) = self.next_rejoin.filter(|&t| t <= now) else {
return;
};
let interval = self.opts.rejoin_interval;
let missed = (now - at).as_nanos() / interval.as_nanos();
self.next_rejoin = Some(at + interval * u32::try_from(missed + 1).unwrap_or(u32::MAX));
if self.rejoining || self.node.local().state == State::Left {
return;
}
let addrs = self.opts.seeds.iter().map(|s| match s {
Seed::Addr(a) => Some(*a),
_ => None,
});
if let Some(addrs) = addrs.collect::<Option<Vec<_>>>() {
return self.rejoin_resolved(addrs);
}
let seeds = self.opts.seeds.clone();
let tx = self.resolved_tx.clone();
self.rejoining = true;
self.tasks.spawn(async move {
let r = seed::resolve(&seeds, seed::LOOKUP_TIMEOUT, true).await;
let _ = tx.send(r.addrs);
});
}
fn rejoin_resolved(&mut self, addrs: Vec<SocketAddr>) {
self.rejoining = false;
while self.tasks.try_join_next().is_some() {}
if self.node.local().state == State::Left {
return;
}
let live: HashSet<SocketAddr> = self.node.members().map(|m| m.addr).collect();
let seeds: Vec<SocketAddr> = addrs.into_iter().filter(|s| !live.contains(s)).collect();
if seeds.is_empty() {
return;
}
let now = self.clock.now();
let alone = live.len() == 1;
tracing::debug!(
seeds = seeds.len(),
alone,
"rejoining seeds that are not live members"
);
let id = self.node.command(now, Command::Join { seeds });
self.pending.insert(id, Pending::Rejoin { alone });
self.rejoining = true;
self.flush(false);
}
fn datagram(&mut self, bytes: &[u8], from: SocketAddr) {
let now = self.clock.now();
self.node.handle_datagram(now, from, bytes);
self.flush(false);
}
fn report(&mut self, conn: StreamId, report: Report) {
let ev = match &report {
Report::Frame { frame, .. } => StreamEvent::Frame(frame),
Report::Refused(e) => {
self.node.count_refused(e);
return;
}
Report::Closed => StreamEvent::Closed,
Report::Failed => StreamEvent::Failed,
Report::Done => {
self.conns.remove(&conn);
if conn.is_inbound() {
self.inbound.retain(|&(c, _)| c != conn);
}
while self.tasks.try_join_next().is_some() {}
return;
}
};
let now = self.clock.now();
self.node.handle_stream(now, conn, ev);
self.flush(false);
}
fn accept(&mut self, stream: T::Stream, from: SocketAddr) {
let ip = from.ip().to_canonical();
let full = evict_for(
&self.inbound,
ip,
self.opts.max_inbound_streams_per_ip,
self.opts.max_inbound_streams,
);
if let Some(victim) = full {
self.inbound.retain(|&(c, _)| c != victim);
if let Some(c) = self.conns.remove(&victim) {
c.abort.abort();
}
tracing::debug!(%from, "too many inbound connections; dropped one");
}
let conn = StreamId::inbound(self.next_inbound);
self.next_inbound += 1;
let (writes, rx) = mpsc::unbounded_channel();
let inbound = Inbound {
held: self.budget.open(conn, ip),
header: Arc::clone(&self.header),
};
let task = conn::inbound(
stream,
conn,
self.opts.limits,
inbound,
rx,
self.reports_tx.clone(),
);
let abort = self.tasks.spawn(task);
self.conns.insert(conn, Conn { writes, abort });
self.inbound.push_back((conn, ip));
}
fn request(&mut self, req: Request) {
let now = self.clock.now();
let (cmd, pending) = match req {
Request::Join { seeds, reply } => (Command::Join { seeds }, Pending::Join(reply)),
Request::Leave { reply } => (Command::Leave, Pending::Done(reply)),
Request::SetMeta { meta, reply } => (Command::SetMeta(meta), Pending::Done(reply)),
Request::Keyring { change, reply } => {
let (cmd, did, key) = change.command();
(cmd, Pending::Keyring { reply, did, key })
}
Request::Close { .. } => unreachable!("handled by the loop"),
#[cfg(any(test, feature = "test-hooks"))]
Request::Panic => panic!("actor panic injected by a test"),
};
let keyring = matches!(pending, Pending::Keyring { .. });
let id = self.node.command(now, cmd);
self.pending.insert(id, pending);
if keyring {
self.shared.key_ids.store(Arc::new(self.node.key_ids()));
self.header.store(Arc::new(self.node.header_check()));
}
self.flush(false);
}
fn flush(&mut self, timer: bool) {
while let Some(t) = self.node.poll_transmit() {
self.transmit(t);
}
let mut replies = Vec::new();
let mut events = std::mem::take(&mut self.events);
while let Some(e) = self.node.poll_event() {
match e {
kinship_core::Event::CommandDone { id, result } => {
if let Some(p) = self.pending.remove(&id) {
replies.push((p, result));
}
}
other => events.extend(Event::from_core(other)),
}
}
let snapshot = self.shared.snapshot.load();
let stale = !events.is_empty()
|| self.node.local() != snapshot.local()
|| (timer && self.node.all_members().count() != snapshot.all.len());
if stale {
self.shared
.snapshot
.store(Arc::new(Snapshot::of(&self.node)));
}
self.shared.hub.publish(&events);
events.clear();
self.events = events;
for (pending, result) in replies {
self.complete(pending, result);
}
}
fn complete(&mut self, pending: Pending, result: Result<CommandOutput, CommandError>) {
match pending {
Pending::Join(reply) => {
let r = match result {
Ok(CommandOutput::Joined { seeds }) => Ok(seeds),
Ok(_) => Ok(0),
Err(e) => Err(e.into()),
};
let _ = reply.send(r);
}
Pending::Done(reply) => {
let _ = reply.send(result.map(drop).map_err(Error::from));
}
Pending::Keyring { reply, did, key } => {
match &result {
Ok(_) => tracing::info!(%key, "keyring: {did} key"),
Err(e) => tracing::warn!(%key, error = %e, "keyring: change refused"),
}
let _ = reply.send(result.map(drop).map_err(Error::from));
}
Pending::Rejoin { alone } => {
self.rejoining = false;
match result {
Ok(_) if alone => tracing::info!("rejoined the cluster through the seeds"),
Ok(_) => tracing::debug!("push-pulled with seeds that were not live members"),
Err(e) => tracing::debug!(error = %e, "rejoin failed; trying again later"),
}
}
}
}
fn transmit(&mut self, t: Transmit) {
match t {
Transmit::Datagram { to, payload } => {
if let Err(e) = self.transport.send_datagram(to, &payload) {
tracing::trace!(%to, error = %e, "datagram not sent");
}
}
Transmit::Connect { conn, to } => {
let (writes, rx) = mpsc::unbounded_channel();
let task = conn::outbound(
Arc::clone(&self.transport),
conn,
to,
self.opts.limits,
rx,
self.reports_tx.clone(),
);
let abort = self.tasks.spawn(task);
self.conns.insert(conn, Conn { writes, abort });
}
Transmit::Stream { conn, frame } => {
if let Some(c) = self.conns.get(&conn) {
let _ = c.writes.send(Write::Bytes(frame));
}
}
Transmit::Close { conn } => {
if let Some(c) = self.conns.get(&conn) {
let _ = c.writes.send(Write::Close);
}
}
}
}
fn publish_stats(&mut self) {
let mut stats = self.shared.stats();
if stats.metrics != *self.node.metrics() {
stats.metrics.clone_from(self.node.metrics());
}
stats.local_health = self.node.local_health();
}
}
impl<T: Transport> Drop for Actor<T> {
fn drop(&mut self) {
self.shared.hub.close(std::thread::panicking());
}
}
fn evict_for(
inbound: &VecDeque<(StreamId, IpAddr)>,
ip: IpAddr,
per_ip: usize,
total: usize,
) -> Option<StreamId> {
let oldest_of = |a: IpAddr| inbound.iter().find(|&&(_, b)| b == a).map(|&(c, _)| c);
if inbound.iter().filter(|&&(_, a)| a == ip).count() >= per_ip {
return oldest_of(ip);
}
if inbound.len() < total {
return None;
}
let mut counts: Vec<(IpAddr, usize)> = Vec::new();
for &(_, a) in inbound {
match counts.iter_mut().find(|(b, _)| *b == a) {
Some((_, n)) => *n += 1,
None => counts.push((a, 1)),
}
}
let most = counts.iter().map(|&(_, n)| n).max()?;
let (richest, _) = counts.into_iter().find(|&(_, n)| n == most)?;
oldest_of(richest)
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(i: u8) -> IpAddr {
IpAddr::from([10, 0, 0, i])
}
#[test]
fn a_new_connection_evicts_its_own_address_first_then_the_address_with_the_most() {
let conns = |of: &[u8]| -> VecDeque<(StreamId, IpAddr)> {
of.iter()
.enumerate()
.map(|(i, &a)| (StreamId::inbound(i as u64), ip(a)))
.collect()
};
let c = StreamId::inbound;
assert_eq!(evict_for(&conns(&[1, 1, 2]), ip(1), 3, 4), None);
assert_eq!(evict_for(&conns(&[2, 1, 1]), ip(1), 2, 64), Some(c(1)));
assert_eq!(evict_for(&conns(&[2, 1, 1, 2, 1]), ip(3), 4, 5), Some(c(1)));
assert_eq!(evict_for(&conns(&[2, 1, 1, 2]), ip(3), 4, 4), Some(c(0)));
assert_eq!(evict_for(&conns(&[2, 1, 1]), ip(1), 4, 3), Some(c(1)));
}
}