tuwunel_service/federation/
peer.rs1use std::{
24 collections::BTreeMap,
25 time::{Duration, SystemTime, UNIX_EPOCH},
26};
27
28use futures::{Stream, StreamExt};
29use http::StatusCode;
30use ruma::{OwnedServerName, ServerName, api::error::ErrorBody};
31use tuwunel_core::{
32 Error, implement,
33 utils::{
34 stream::{ReadyExt, TryIgnore},
35 time::now_secs,
36 },
37};
38use tuwunel_database::Interfix;
39
40pub(super) const MAX_BACKOFF: Duration = Duration::from_hours(24);
42
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
45pub enum Classification {
46 #[default]
47 Transient,
48 Permanent,
49}
50
51impl Classification {
52 #[inline]
55 #[must_use]
56 fn from_byte(byte: u8) -> Self {
57 match byte {
58 | 1 => Self::Permanent,
59 | _ => Self::Transient,
60 }
61 }
62}
63
64impl From<Classification> for u8 {
65 #[inline]
66 fn from(c: Classification) -> Self {
67 match c {
68 | Classification::Transient => 0,
69 | Classification::Permanent => 1,
70 }
71 }
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum ShouldAttempt {
77 Yes,
78 No {
79 earliest_retry: SystemTime,
80 },
81
82 Deprioritize,
85}
86
87pub(super) struct Backoff {
89 pub(super) class: Classification,
90
91 pub(super) anchor_secs: u64,
93
94 pub(super) streak: u32,
95
96 pub(super) now: u64,
98
99 pub(super) window_secs: u64,
100 pub(super) grace_secs: u64,
101}
102
103#[derive(Clone, Copy)]
105pub(super) struct Streak {
106 pub(super) class: Classification,
107 pub(super) anchor_secs: u64,
108 pub(super) oldest_bucket: u64,
109 pub(super) latest_bucket: u64,
110}
111
112#[derive(Clone, Copy, Debug)]
115pub struct PeerBackoff {
116 pub anchor_secs: u64,
118
119 pub oldest_secs: u64,
121
122 pub delay_secs: u64,
124}
125
126#[implement(super::Service)]
127pub async fn record_success(&self, server: &ServerName) {
128 self.statuses
129 .del_prefix(&(server, Interfix))
130 .await;
131}
132
133#[implement(super::Service)]
137#[tracing::instrument(
138 level = "trace",
139 skip(self),
140 fields(
141 %server,
142 ),
143)]
144pub async fn note_peer_alive(&self, server: &ServerName) -> bool {
145 let sad = self.peer_has_failures(server).await;
146
147 if sad {
148 self.statuses
149 .del_prefix(&(server, Interfix))
150 .await;
151 }
152
153 sad
154}
155
156#[implement(super::Service)]
158#[tracing::instrument(
159 level = "trace",
160 skip(self),
161 fields(
162 %server,
163 ),
164)]
165pub async fn peer_has_failures(&self, server: &ServerName) -> bool {
166 self.statuses
167 .stream_prefix_raw(&(server, Interfix))
168 .ignore_err()
169 .ready_any(|_| true)
170 .await
171}
172
173#[implement(super::Service)]
174pub fn record_failure(&self, server: &ServerName, classification: Classification) {
175 let mut value = [0_u8; 9];
177 value[0] = u8::from(classification);
178 value[1..].copy_from_slice(&now_secs().to_be_bytes());
179
180 self.statuses
181 .put_raw((server, self.current_bucket()), value);
182}
183
184#[implement(super::Service)]
185#[tracing::instrument(skip(self), fields(%server), level = "trace")]
186pub async fn should_attempt(&self, server: &ServerName) -> ShouldAttempt {
187 let Some(streak) = self.peer_streak(server).await else {
188 return ShouldAttempt::Yes;
189 };
190
191 attempt_verdict(&self.backoff(streak))
192}
193
194#[implement(super::Service)]
197pub async fn peer_backoff(&self, server: &ServerName) -> Option<PeerBackoff> {
198 self.peer_streak(server)
199 .await
200 .map(|streak| self.peer_backoff_from(streak))
201}
202
203#[implement(super::Service)]
207pub async fn peer_backoffs(&self) -> BTreeMap<OwnedServerName, PeerBackoff> {
208 let window_secs = self.window_secs;
209
210 self.statuses
211 .stream()
212 .ignore_err()
213 .ready_fold(
214 Vec::<(OwnedServerName, Streak)>::new(),
215 |mut runs, ((server, bucket), value): ((&ServerName, u64), &[u8])| {
216 match runs.last_mut() {
217 | Some((last, streak)) if *last == *server =>
218 *streak = fold_streak(window_secs, Some(*streak), bucket, value),
219 | _ => runs
220 .push((server.to_owned(), fold_streak(window_secs, None, bucket, value))),
221 }
222
223 runs
224 },
225 )
226 .await
227 .into_iter()
228 .map(|(server, streak)| (server, self.peer_backoff_from(streak)))
229 .collect()
230}
231
232#[implement(super::Service)]
235pub fn peer_snapshot(
236 &self,
237) -> impl Stream<Item = (&ServerName, SystemTime, Classification)> + Send + '_ {
238 self.statuses.stream().ignore_err().map(
239 move |((server, bucket), value): ((&ServerName, u64), &[u8])| {
240 (server, self.bucket_start(bucket), classify(value))
241 },
242 )
243}
244
245#[implement(super::Service)]
246#[inline]
247#[must_use]
248fn current_bucket(&self) -> u64 {
249 now_secs()
250 .checked_div(self.window_secs.max(1))
251 .unwrap_or(0)
252}
253
254#[implement(super::Service)]
256#[inline]
257#[must_use]
258fn bucket_start(&self, bucket: u64) -> SystemTime {
259 let offset = bucket.saturating_mul(self.window_secs);
260
261 UNIX_EPOCH
262 .checked_add(Duration::from_secs(offset))
263 .unwrap_or(UNIX_EPOCH)
264}
265
266#[implement(super::Service)]
267#[inline]
268#[must_use]
269fn streak(&self, latest_bucket: u64, oldest_bucket: u64) -> u32 {
270 let span = latest_bucket
271 .saturating_sub(oldest_bucket)
272 .saturating_add(1);
273
274 u32::try_from(span)
275 .unwrap_or(u32::MAX)
276 .min(self.n_max)
277}
278
279#[implement(super::Service)]
281async fn peer_streak(&self, server: &ServerName) -> Option<Streak> {
282 let window_secs = self.window_secs;
283
284 self.statuses
285 .stream_prefix(&(server, Interfix))
286 .ignore_err()
287 .ready_fold(None, |state, ((_, bucket), value): ((&ServerName, u64), &[u8])| {
288 Some(fold_streak(window_secs, state, bucket, value))
289 })
290 .await
291}
292
293#[implement(super::Service)]
295fn backoff(&self, run: Streak) -> Backoff {
296 Backoff {
297 class: run.class,
298 anchor_secs: run.anchor_secs,
299 streak: self.streak(run.latest_bucket, run.oldest_bucket),
300 now: now_secs(),
301 window_secs: self.window_secs,
302 grace_secs: self.grace.as_secs(),
303 }
304}
305
306#[implement(super::Service)]
308fn peer_backoff_from(&self, streak: Streak) -> PeerBackoff {
309 PeerBackoff {
310 anchor_secs: streak.anchor_secs,
311 oldest_secs: streak
312 .oldest_bucket
313 .saturating_mul(self.window_secs),
314 delay_secs: self.backoff(streak).delay_secs(),
315 }
316}
317
318#[must_use]
321pub(super) fn attempt_verdict(backoff: &Backoff) -> ShouldAttempt {
322 let earliest_secs = backoff
323 .anchor_secs
324 .saturating_add(backoff.delay_secs());
325
326 if backoff.now >= earliest_secs {
327 return ShouldAttempt::Yes;
328 }
329
330 ShouldAttempt::No {
331 earliest_retry: UNIX_EPOCH
332 .checked_add(Duration::from_secs(earliest_secs))
333 .unwrap_or_else(SystemTime::now),
334 }
335}
336
337impl Backoff {
338 #[must_use]
342 pub(super) fn delay_secs(&self) -> u64 {
343 let max_backoff = MAX_BACKOFF.as_secs();
344
345 match self.class {
346 | Classification::Permanent => max_backoff,
347 | Classification::Transient if self.streak <= 1 && self.grace_secs != 0 =>
348 self.grace_secs.min(max_backoff),
349 | Classification::Transient => self
350 .window_secs
351 .saturating_mul(u64::from(self.streak))
352 .saturating_mul(u64::from(self.streak))
353 .min(max_backoff),
354 }
355 }
356}
357
358#[must_use]
361pub(super) fn fold_streak(
362 window_secs: u64,
363 state: Option<Streak>,
364 bucket: u64,
365 value: &[u8],
366) -> Streak {
367 let anchor_secs = failure_secs(value).unwrap_or_else(|| bucket.saturating_mul(window_secs));
368
369 let oldest_bucket = state.map_or(bucket, |streak| streak.oldest_bucket);
370
371 Streak {
372 class: classify(value),
373 anchor_secs,
374 oldest_bucket,
375 latest_bucket: bucket,
376 }
377}
378
379#[inline]
380#[must_use]
381pub(super) fn classify(bytes: &[u8]) -> Classification {
382 bytes
383 .first()
384 .copied()
385 .map_or(Classification::Transient, Classification::from_byte)
386}
387
388#[must_use]
391pub(super) fn failure_secs(bytes: &[u8]) -> Option<u64> {
392 bytes
393 .get(1..9)
394 .and_then(|tail| tail.try_into().ok())
395 .map(u64::from_be_bytes)
396}
397
398#[must_use]
410pub(super) fn classify_error(error: &Error) -> Option<Classification> {
411 let Error::Federation(_, response) = error else {
412 return Some(Classification::Transient);
413 };
414
415 let status = response.status_code;
416
417 match status {
418 | _ if status == StatusCode::GONE => Some(Classification::Permanent),
419 | _ if status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS =>
420 Some(Classification::Transient),
421 | _ if matches!(response.body, ErrorBody::NotJson { .. }) =>
422 Some(Classification::Transient),
423 | _ => None,
424 }
425}