1use std::collections::{BTreeMap, BTreeSet, HashMap};
24use std::future::Future;
25use std::path::{Path, PathBuf};
26use std::sync::Arc;
27use std::time::{Duration, SystemTime};
28
29use tokio::task::JoinSet;
30use tokio::time::Instant;
31
32use crate::addr::Target;
33use crate::membership::{self, Membership};
34use crate::peers;
35use crate::sync::{Node, Report};
36
37#[derive(Debug, Clone, Copy)]
38pub struct Options {
39 pub interval: Duration,
40 pub poll: Duration,
41 pub backoff_max: Duration,
42 pub timeout: Duration,
44 pub grace: Duration,
45}
46
47impl Default for Options {
48 fn default() -> Self {
49 Options {
50 interval: Duration::from_secs(60),
51 poll: Duration::from_secs(2),
52 backoff_max: Duration::from_secs(30 * 60),
53 timeout: Duration::from_secs(30),
54 grace: Duration::from_secs(5),
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub enum Event {
62 Served(Result<Report, String>),
63 Synced {
64 peer: String,
65 result: Result<Report, String>,
66 },
67 Gate(Result<(), String>),
69}
70
71#[derive(Default)]
72struct Backoff {
73 failures: u32,
74 next: Option<Instant>,
75}
76
77pub type State = BTreeMap<PathBuf, (u64, Option<SystemTime>)>;
78
79fn local_state(home: &Path) -> State {
81 let mut files = crate::lines::logs(home);
82 files.push(membership::path(home));
83 state(files)
84}
85
86fn state(files: Vec<PathBuf>) -> State {
88 files
89 .into_iter()
90 .map(|p| {
91 let at = std::fs::metadata(&p).map_or((0, None), |m| (m.len(), m.modified().ok()));
92 (p, at)
93 })
94 .collect()
95}
96
97fn backoff_wait(opts: &Options, failures: u32) -> Duration {
101 match failures {
102 0 | 1 => opts.poll,
103 n => opts.interval.saturating_mul(1u32 << (n - 1).min(16)).min(opts.backoff_max),
104 }
105}
106
107pub async fn serve(node: Arc<Node>, opts: Options, shutdown: impl Future<Output = ()>, on: impl Fn(Event)) {
108 let mut sessions: JoinSet<Result<Report, String>> = JoinSet::new();
109 let mut dials: JoinSet<Result<Report, String>> = JoinSet::new();
111 let mut dialling: HashMap<tokio::task::Id, String> = HashMap::new();
112 let mut owed: BTreeSet<String> = BTreeSet::new();
115 let mut backoff: HashMap<String, Backoff> = HashMap::new();
116 let mut seen: Option<State> = None;
117 let mut last_round: Option<Instant> = None;
118 let mut dirty = true;
119 let mut gate_open: Option<bool> = None;
120 let mut tick = tokio::time::interval(opts.poll);
122 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
123 tokio::pin!(shutdown);
124 loop {
125 tokio::select! {
126 _ = &mut shutdown => break,
127 incoming = node.incoming() => match incoming {
128 Some(i) => {
129 let n = node.clone();
130 sessions.spawn(async move { n.handle(i).await });
131 }
132 None => break,
133 },
134 Some(done) = sessions.join_next() => {
135 let result = done.unwrap_or_else(|e| Err(format!("session task: {e}")));
136 if result.as_ref().is_ok_and(|r| r.received > 0 || r.quarantined > 0 || r.steered > 0 || r.joined > 0) {
137 dirty = true; }
139 on(Event::Served(result));
140 }
141 Some(done) = dials.join_next_with_id() => {
142 let (id, result) = match done {
143 Ok((id, r)) => (id, r),
144 Err(e) => (e.id(), Err(format!("dial task: {e}"))),
145 };
146 let Some(peer) = dialling.remove(&id) else { continue };
147 let b = backoff.entry(peer.clone()).or_default();
148 match &result {
149 Ok(r) => {
150 *b = Backoff::default();
151 dirty |= r.received > 0 || r.quarantined > 0 || r.steered > 0;
152 }
153 Err(_) => {
154 b.failures = b.failures.saturating_add(1);
155 b.next = Some(Instant::now() + backoff_wait(&opts, b.failures));
156 owed.insert(peer.clone()); node.unreachable(&peer);
158 }
159 }
160 on(Event::Synced { peer, result });
161 }
162 _ = tick.tick() => {
163 let gate = node.gate().map(|_| ());
164 if gate_open != Some(gate.is_ok()) {
165 gate_open = Some(gate.is_ok());
166 on(Event::Gate(gate.clone()));
167 }
168 if gate_open != Some(true) {
170 continue;
171 }
172 let mut changed = false;
173 let mut now_state = local_state(node.home());
174 if seen.as_ref() != Some(&now_state) {
175 let _ = node.enforce();
178 let _ = node.stamp(); now_state = local_state(node.home());
181 changed = true;
182 }
183 seen = Some(now_state);
184 let round = dirty || changed || last_round.is_none_or(|t: Instant| t.elapsed() >= opts.interval);
185 let now = Instant::now();
186 let ready = |k: &String| !dialling.values().any(|d| d == k) && backoff.get(k).and_then(|b| b.next).is_none_or(|at| at <= now);
187 if !round && !owed.iter().any(ready) {
190 continue;
191 }
192 let all = targets(&node);
193 if round {
194 (dirty, last_round) = (false, Some(now));
195 owed.extend(all.iter().map(|t| t.key.clone()));
196 }
197 owed.retain(|k| all.iter().any(|t| t.key == *k));
199 let due: Vec<Target> = all.into_iter().filter(|t| owed.contains(&t.key) && ready(&t.key)).collect();
200 for t in due {
201 owed.remove(&t.key);
202 let (n, timeout, key) = (node.clone(), opts.timeout, t.key.clone());
203 let handle = dials.spawn(async move {
204 match tokio::time::timeout(timeout, n.sync(&t.to_string())).await {
205 Ok(r) => r,
206 Err(_) => Err(format!("no answer within {timeout:?}")),
207 }
208 });
209 dialling.insert(handle.id(), key);
210 }
211 }
212 }
213 }
214 let _ = tokio::time::timeout(opts.grace, async {
216 while sessions.join_next().await.is_some() {}
217 while dials.join_next().await.is_some() {}
218 })
219 .await;
220 sessions.abort_all();
221 dials.abort_all();
222 node.close().await;
223}
224
225fn targets(node: &Node) -> Vec<Target> {
227 let Ok(Some(m)) = Membership::load(node.home()) else { return Vec::new() };
228 let me = node.public();
229 peers::targets(node.home()).into_iter().filter(|t| t.key != me && m.role_of(&t.key).is_some()).collect()
230}