1use std::{
2 ops::Range,
3 sync::atomic::{AtomicU64, Ordering},
4 time::Duration,
5};
6
7use futures::FutureExt;
8use ruma::EventId;
9use tuwunel_core::{
10 Error, implement,
11 utils::{
12 BoolExt, continue_exponential_backoff,
13 stream::{ReadyExt, TryIgnore},
14 time::now_secs,
15 },
16};
17use tuwunel_database::{Ignore, Interfix};
18
19#[cfg(test)]
20mod tests;
21
22const QUANTUM: u64 = 60;
25
26const SUPPRESS_AFTER: u32 = 3;
28
29pub(super) const UPGRADE_RETRY: Range<Duration> =
35 Duration::from_mins(5)..Duration::from_hours(24);
36
37#[derive(Clone, Copy)]
39pub(super) enum Context {
40 Fetch = 0,
41 Auth = 1,
42 Upgrade = 2,
43 Incoming = 3,
44}
45
46impl From<Context> for u8 {
47 #[inline]
48 fn from(context: Context) -> Self {
49 match context {
50 | Context::Fetch => 0,
51 | Context::Auth => 1,
52 | Context::Upgrade => 2,
53 | Context::Incoming => 3,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Default)]
63pub(super) enum Disposition {
64 #[default]
65 Pending = 0,
66 Transient = 1,
67 Permanent = 2,
68}
69
70pub(super) enum Suppression {
72 Absent,
74
75 Allow,
77
78 Deny,
80}
81
82#[derive(Default)]
83struct Summary {
84 total: u32,
85 pending: u32,
86 latest_secs: u64,
87 latest_class: Disposition,
88}
89
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98pub struct BackoffMetrics {
99 pub fetch: Verdicts,
101
102 pub auth: Verdicts,
104
105 pub upgrade: Verdicts,
108
109 pub incoming: Verdicts,
111}
112
113#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
118pub struct Verdicts {
119 pub absent: u64,
121
122 pub allowed: u64,
124
125 pub denied: u64,
127}
128
129#[derive(Default)]
130pub(super) struct BackoffCounters {
131 fetch: VerdictCounters,
132 auth: VerdictCounters,
133 upgrade: VerdictCounters,
134 incoming: VerdictCounters,
135}
136
137#[derive(Default)]
138struct VerdictCounters {
139 absent: AtomicU64,
140 allowed: AtomicU64,
141 denied: AtomicU64,
142}
143
144impl From<u64> for Disposition {
145 #[inline]
146 fn from(disc: u64) -> Self {
147 match disc {
148 | 1 => Self::Transient,
149 | 2 => Self::Permanent,
150 | _ => Self::Pending,
151 }
152 }
153}
154
155impl From<Disposition> for u64 {
156 #[inline]
157 fn from(disposition: Disposition) -> Self {
158 match disposition {
159 | Disposition::Pending => 0,
160 | Disposition::Transient => 1,
161 | Disposition::Permanent => 2,
162 }
163 }
164}
165
166impl Suppression {
167 #[inline]
168 pub(super) fn is_deny(&self) -> bool { matches!(self, Self::Deny) }
169}
170
171impl Summary {
172 fn tally(mut self, (_, (class, secs)): (Ignore, (u64, u64))) -> Self {
173 let class = Disposition::from(class);
174
175 self.total = self.total.saturating_add(1);
176 if matches!(class, Disposition::Pending) {
177 self.pending = self.pending.saturating_add(1);
178 }
179
180 if secs >= self.latest_secs {
181 self.latest_secs = secs;
182 self.latest_class = class;
183 }
184
185 self
186 }
187}
188
189impl Verdicts {
190 #[inline]
194 #[must_use]
195 pub fn lookups(&self) -> u64 {
196 self.absent
197 .saturating_add(self.allowed)
198 .saturating_add(self.denied)
199 }
200}
201
202#[implement(super::Service)]
206#[inline]
207#[must_use]
208pub fn backoff_metrics(&self) -> BackoffMetrics { self.backoff.snapshot() }
209
210#[implement(BackoffCounters)]
211fn snapshot(&self) -> BackoffMetrics {
212 BackoffMetrics {
213 fetch: self.fetch.snapshot(),
214 auth: self.auth.snapshot(),
215 upgrade: self.upgrade.snapshot(),
216 incoming: self.incoming.snapshot(),
217 }
218}
219
220impl VerdictCounters {
221 fn snapshot(&self) -> Verdicts {
222 Verdicts {
223 absent: self.absent.load(Ordering::Relaxed),
224 allowed: self.allowed.load(Ordering::Relaxed),
225 denied: self.denied.load(Ordering::Relaxed),
226 }
227 }
228}
229
230#[implement(super::Service)]
235pub(super) fn record_attempt(&self, ctx: Context, event_id: &EventId) -> u32 {
236 let bucket = current_bucket();
237
238 self.record_outcome_at(ctx, event_id, bucket, Disposition::Pending);
239 bucket
240}
241
242#[implement(super::Service)]
253pub(super) async fn record_completion(
254 &self,
255 ctx: Context,
256 event_id: &EventId,
257 standing: Suppression,
258 attempt: Option<u32>,
259 appended: Result<bool, &Error>,
260) {
261 match (appended, standing, attempt) {
262 | (_, Suppression::Absent, None) | (Ok(false), ..) => {},
263 | (Err(error), ..) if error.is_interrupted() || self.services.server.is_stopping() => {},
264 | (Ok(true), Suppression::Absent, Some(bucket)) =>
265 self.clear_outcome_at(ctx, event_id, bucket),
266 | (Ok(true), ..) => self.record_success(ctx, event_id).await,
267 | (Err(_), _, attempt) => {
268 let bucket = attempt.unwrap_or_else(current_bucket);
269
270 self.record_outcome_at(ctx, event_id, bucket, Disposition::Transient);
271 },
272 }
273}
274
275#[implement(super::Service)]
276pub(super) fn record_outcome(&self, ctx: Context, event_id: &EventId, disposition: Disposition) {
277 self.record_outcome_at(ctx, event_id, current_bucket(), disposition);
278}
279
280#[implement(super::Service)]
286pub async fn clear_delivery_backoff(&self, event_id: &EventId) {
287 self.record_success(Context::Upgrade, event_id)
288 .await;
289
290 self.record_success(Context::Incoming, event_id)
291 .await;
292}
293
294#[implement(super::Service)]
295pub(super) async fn record_success(&self, ctx: Context, event_id: &EventId) {
296 self.db
297 .eventid_backoff
298 .del_prefix(&(u8::from(ctx), event_id, Interfix))
299 .await;
300}
301
302#[implement(super::Service)]
307pub(super) fn is_suppressed(
308 &self,
309 ctx: Context,
310 event_id: &EventId,
311 range: Range<Duration>,
312) -> impl Future<Output = Suppression> + Send {
313 self.verdict(ctx, event_id, range)
314 .inspect(move |verdict| self.backoff.count(ctx, verdict))
315}
316
317#[implement(super::Service)]
318async fn verdict(&self, ctx: Context, event_id: &EventId, range: Range<Duration>) -> Suppression {
319 let summary = self
320 .db
321 .eventid_backoff
322 .stream_prefix::<Ignore, (u64, u64), _>(&(u8::from(ctx), event_id, Interfix))
323 .ignore_err()
324 .ready_fold(Summary::default(), Summary::tally)
325 .await;
326
327 if summary.total == 0 {
328 return Suppression::Absent;
329 }
330
331 if matches!(summary.latest_class, Disposition::Permanent) {
332 return Suppression::Deny;
333 }
334
335 let elapsed = Duration::from_secs(now_secs().saturating_sub(summary.latest_secs));
336 let (tries, rate_ok) = match summary.latest_class {
337 | Disposition::Pending => (summary.pending, summary.pending >= SUPPRESS_AFTER),
338 | _ => (summary.total, true),
339 };
340
341 let deny = rate_ok && continue_exponential_backoff(range.start, range.end, elapsed, tries);
342
343 deny.map_or(Suppression::Allow, || Suppression::Deny)
344}
345
346#[implement(BackoffCounters)]
347fn count(&self, ctx: Context, verdict: &Suppression) {
348 let counters = match ctx {
349 | Context::Fetch => &self.fetch,
350 | Context::Auth => &self.auth,
351 | Context::Upgrade => &self.upgrade,
352 | Context::Incoming => &self.incoming,
353 };
354
355 let counter = match verdict {
356 | Suppression::Absent => &counters.absent,
357 | Suppression::Allow => &counters.allowed,
358 | Suppression::Deny => &counters.denied,
359 };
360
361 counter.fetch_add(1, Ordering::Relaxed);
362}
363
364fn current_bucket() -> u32 { u32::try_from(now_secs() / QUANTUM).unwrap_or(u32::MAX) }
365
366#[implement(super::Service)]
367fn record_outcome_at(
368 &self,
369 ctx: Context,
370 event_id: &EventId,
371 bucket: u32,
372 disposition: Disposition,
373) {
374 self.db
375 .eventid_backoff
376 .put((u8::from(ctx), event_id, bucket), (u64::from(disposition), now_secs()));
377}
378
379#[implement(super::Service)]
380fn clear_outcome_at(&self, ctx: Context, event_id: &EventId, bucket: u32) {
381 self.db
382 .eventid_backoff
383 .del((u8::from(ctx), event_id, bucket));
384}