use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::task::JoinSet;
use tokio::time::Instant;
use crate::addr::Target;
use crate::membership::{self, Membership};
use crate::peers;
use crate::sync::{Node, Report};
#[derive(Debug, Clone, Copy)]
pub struct Options {
pub interval: Duration,
pub poll: Duration,
pub backoff_max: Duration,
pub timeout: Duration,
pub grace: Duration,
}
impl Default for Options {
fn default() -> Self {
Options {
interval: Duration::from_secs(60),
poll: Duration::from_secs(2),
backoff_max: Duration::from_secs(30 * 60),
timeout: Duration::from_secs(30),
grace: Duration::from_secs(5),
}
}
}
#[derive(Debug, Clone)]
pub enum Event {
Served(Result<Report, String>),
Synced {
peer: String,
result: Result<Report, String>,
},
Gate(Result<(), String>),
}
#[derive(Default)]
struct Backoff {
failures: u32,
next: Option<Instant>,
}
pub type State = BTreeMap<PathBuf, (u64, Option<SystemTime>)>;
fn local_state(home: &Path) -> State {
let mut files = crate::lines::logs(home);
files.push(membership::path(home));
state(files)
}
fn state(files: Vec<PathBuf>) -> State {
files
.into_iter()
.map(|p| {
let at = std::fs::metadata(&p).map_or((0, None), |m| (m.len(), m.modified().ok()));
(p, at)
})
.collect()
}
fn backoff_wait(opts: &Options, failures: u32) -> Duration {
match failures {
0 | 1 => opts.poll,
n => opts.interval.saturating_mul(1u32 << (n - 1).min(16)).min(opts.backoff_max),
}
}
pub async fn serve(node: Arc<Node>, opts: Options, shutdown: impl Future<Output = ()>, on: impl Fn(Event)) {
let mut sessions: JoinSet<Result<Report, String>> = JoinSet::new();
let mut dials: JoinSet<Result<Report, String>> = JoinSet::new();
let mut dialling: HashMap<tokio::task::Id, String> = HashMap::new();
let mut owed: BTreeSet<String> = BTreeSet::new();
let mut backoff: HashMap<String, Backoff> = HashMap::new();
let mut seen: Option<State> = None;
let mut last_round: Option<Instant> = None;
let mut dirty = true;
let mut gate_open: Option<bool> = None;
let mut tick = tokio::time::interval(opts.poll);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
tokio::pin!(shutdown);
loop {
tokio::select! {
_ = &mut shutdown => break,
incoming = node.incoming() => match incoming {
Some(i) => {
let n = node.clone();
sessions.spawn(async move { n.handle(i).await });
}
None => break,
},
Some(done) = sessions.join_next() => {
let result = done.unwrap_or_else(|e| Err(format!("session task: {e}")));
if result.as_ref().is_ok_and(|r| r.received > 0 || r.quarantined > 0 || r.steered > 0 || r.joined > 0) {
dirty = true; }
on(Event::Served(result));
}
Some(done) = dials.join_next_with_id() => {
let (id, result) = match done {
Ok((id, r)) => (id, r),
Err(e) => (e.id(), Err(format!("dial task: {e}"))),
};
let Some(peer) = dialling.remove(&id) else { continue };
let b = backoff.entry(peer.clone()).or_default();
match &result {
Ok(r) => {
*b = Backoff::default();
dirty |= r.received > 0 || r.quarantined > 0 || r.steered > 0;
}
Err(_) => {
b.failures = b.failures.saturating_add(1);
b.next = Some(Instant::now() + backoff_wait(&opts, b.failures));
owed.insert(peer.clone()); node.unreachable(&peer);
}
}
on(Event::Synced { peer, result });
}
_ = tick.tick() => {
let gate = node.gate().map(|_| ());
if gate_open != Some(gate.is_ok()) {
gate_open = Some(gate.is_ok());
on(Event::Gate(gate.clone()));
}
if gate_open != Some(true) {
continue;
}
let mut changed = false;
let mut now_state = local_state(node.home());
if seen.as_ref() != Some(&now_state) {
let _ = node.enforce();
let _ = node.stamp(); now_state = local_state(node.home());
changed = true;
}
seen = Some(now_state);
let round = dirty || changed || last_round.is_none_or(|t: Instant| t.elapsed() >= opts.interval);
let now = Instant::now();
let ready = |k: &String| !dialling.values().any(|d| d == k) && backoff.get(k).and_then(|b| b.next).is_none_or(|at| at <= now);
if !round && !owed.iter().any(ready) {
continue;
}
let all = targets(&node);
if round {
(dirty, last_round) = (false, Some(now));
owed.extend(all.iter().map(|t| t.key.clone()));
}
owed.retain(|k| all.iter().any(|t| t.key == *k));
let due: Vec<Target> = all.into_iter().filter(|t| owed.contains(&t.key) && ready(&t.key)).collect();
for t in due {
owed.remove(&t.key);
let (n, timeout, key) = (node.clone(), opts.timeout, t.key.clone());
let handle = dials.spawn(async move {
match tokio::time::timeout(timeout, n.sync(&t.to_string())).await {
Ok(r) => r,
Err(_) => Err(format!("no answer within {timeout:?}")),
}
});
dialling.insert(handle.id(), key);
}
}
}
}
let _ = tokio::time::timeout(opts.grace, async {
while sessions.join_next().await.is_some() {}
while dials.join_next().await.is_some() {}
})
.await;
sessions.abort_all();
dials.abort_all();
node.close().await;
}
fn targets(node: &Node) -> Vec<Target> {
let Ok(Some(m)) = Membership::load(node.home()) else { return Vec::new() };
let me = node.public();
peers::targets(node.home()).into_iter().filter(|t| t.key != me && m.role_of(&t.key).is_some()).collect()
}