Skip to main content

ojak/deliverer/
mod.rs

1//! Sending activities through the queue.
2//!
3//! [`Deliverer::send`] queues an activity for a set of inboxes and returns;
4//! [`Deliverer::run`] is the loop that sends what is queued, and the
5//! application spawns it on whatever runtime and in whatever task context it
6//! uses — Ojak does not spawn tasks of its own. Any number of loops may run,
7//! in any number of processes, over one queue.
8//!
9//! A batch, such as moving every account's followers, has to finish:
10//! [`Deliverer::send_batch`] tags each delivery so that the batch can be
11//! followed, and gives it a deadline, past which it is given up on rather
12//! than retried, so that one server that is down or too slow does not hold
13//! the batch open for as long as the retry policy would.
14//!
15//! A portable inbox (FEP-ef61) has no host of its own, only the gateways its
16//! actor lists; [`Deliverer::send_portable`] queues it with them, and one
17//! attempt tries each in order until one accepts.
18
19mod keys;
20mod post;
21
22pub use keys::CachingSenderKeys;
23pub use post::{DeliveryError, deliver, deliver_with_headers, permanent_status};
24
25use crate::client::Client;
26use crate::portable::ApUri;
27use crate::queue::{Job, Queue, QueueError, RetryPolicy};
28use crate::sig::{Scheme, SenderKey};
29use futures_util::StreamExt as _;
30use futures_util::stream;
31use serde_json::{Value, json};
32use std::collections::{BTreeSet, HashMap};
33use std::future::Future;
34use std::sync::{Arc, Mutex};
35use std::time::{Duration, Instant};
36use tokio::sync::{Notify, Semaphore};
37use url::Url;
38
39/// Where a sender's signing key comes from: the application.
40pub trait SenderKeys: Send + Sync + 'static {
41    /// The key `sender` signs with, or `None` when there is no such sender.
42    fn key(
43        &self,
44        sender: &str,
45    ) -> impl Future<Output = Result<Option<SenderKey>, QueueError>> + Send;
46
47    /// Whether `sender` is gone for good, so that a status
48    /// [`DelivererConfig::permanent_if_sender_gone`] names will not change:
49    /// Mastodon gives up at once on a 401 to a delivery from an account that
50    /// is deleted or suspended with nothing left to undo
51    /// (`unsalvageable_authorization_failure?`). Asked only after such a
52    /// status; no sender is gone unless the application says so.
53    fn gone(&self, sender: &str) -> impl Future<Output = bool> + Send {
54        let _ = sender;
55        std::future::ready(false)
56    }
57}
58
59/// One key, for an application with one sender: it signs whatever is sent.
60impl SenderKeys for SenderKey {
61    fn key(
62        &self,
63        _sender: &str,
64    ) -> impl Future<Output = Result<Option<SenderKey>, QueueError>> + Send {
65        std::future::ready(Ok(Some(self.clone())))
66    }
67}
68
69/// A key for each sender, by the name it is sent as.
70impl<S: std::hash::BuildHasher + Send + Sync + 'static> SenderKeys
71    for HashMap<String, SenderKey, S>
72{
73    fn key(
74        &self,
75        sender: &str,
76    ) -> impl Future<Output = Result<Option<SenderKey>, QueueError>> + Send {
77        std::future::ready(Ok(self.get(sender).cloned()))
78    }
79}
80
81/// A key for each sender, by the name it is sent as.
82impl SenderKeys for std::collections::BTreeMap<String, SenderKey> {
83    fn key(
84        &self,
85        sender: &str,
86    ) -> impl Future<Output = Result<Option<SenderKey>, QueueError>> + Send {
87        std::future::ready(Ok(self.get(sender).cloned()))
88    }
89}
90
91/// A delivery that will not be tried again.
92#[derive(Clone, Debug)]
93pub struct DeliveryFailure {
94    pub inbox: Url,
95    pub sender: String,
96    /// The status the inbox last answered with, if it answered. A 410 says
97    /// the inbox, or its whole server, is gone for good.
98    pub status: Option<u16>,
99    pub error: String,
100    /// The tag of the batch it was sent in, if any.
101    pub tag: Option<String>,
102    /// Whether it was given up on for its batch's deadline, rather than for
103    /// the retry policy or an answer that will not change.
104    pub deadline: bool,
105}
106
107/// One attempt at a delivery, as it came out.
108#[derive(Clone, Debug)]
109pub struct DeliveryAttempt {
110    /// The inbox; a portable one's `ap` URI, percent-encoded.
111    pub inbox: Url,
112    pub sender: String,
113    pub outcome: AttemptOutcome,
114}
115
116/// How an attempt came out.
117#[derive(Clone, Debug, Eq, PartialEq)]
118pub enum AttemptOutcome {
119    /// The inbox accepted it.
120    Delivered,
121    /// It was sent and not accepted, or could not be sent. It is tried again
122    /// unless `permanent`, or the retry policy or its batch's deadline has
123    /// run out, which the failure handler is then told of.
124    Failed {
125        /// The status the inbox answered with, if it answered.
126        status: Option<u16>,
127        /// Whether the answer will not change, by
128        /// [`DelivererConfig::permanent`].
129        permanent: bool,
130        error: String,
131    },
132    /// It was not sent, because the circuit breaker for its destination is
133    /// open. It is tried again as a failure is.
134    Held,
135}
136
137/// Holding back deliveries to a destination that keeps failing, rather than
138/// sending each one to fail in turn, as a [Stoplight] with its default
139/// strategies does.
140///
141/// After `threshold` failures in a row a destination's breaker opens (red),
142/// and deliveries to it are held — retried as failures, without a request —
143/// until `cool_off` has passed since the failure that opened it. Then it is
144/// half-open (yellow): one delivery at a time is let through as a probe,
145/// and the others are held while it is under way. A probe that succeeds
146/// closes the breaker; one that fails opens it for another `cool_off`. A
147/// probe that never reports back stops being one after `cool_off`, and the
148/// next delivery probes instead.
149///
150/// A failure is what may pass: a 5xx, 408, 429, or no answer. An answer that
151/// will not change is the server working. A success while the breaker is
152/// closed starts its count of failures again; one that was sent before the
153/// breaker opened and answers after does not close it, as only a probe does.
154///
155/// It is kept in memory, for each deliverer, unless the application gives
156/// the deliverer a [`BreakerStore`] of its own: deliverers in several
157/// processes sharing one store count their failures together, and let one
158/// probe through between them.
159///
160/// [Stoplight]: https://github.com/bolshakov/stoplight
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct CircuitBreaker {
163    /// Failures in a row that open it.
164    pub threshold: u32,
165    /// How long it stays open before a probe is let through, and how long a
166    /// probe is waited for.
167    pub cool_off: Duration,
168    /// What one breaker is kept for.
169    pub scope: BreakerScope,
170}
171
172/// What a circuit breaker is kept for.
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum BreakerScope {
175    /// Each host: a server that is down fails every inbox on it.
176    Host,
177    /// Each inbox, as Mastodon keeps its breakers.
178    Inbox,
179}
180
181impl Default for CircuitBreaker {
182    /// Ten failures in a row and a minute's cool-off, as Mastodon's, but for
183    /// each host, where Mastodon keeps one for each inbox.
184    fn default() -> Self {
185        Self {
186            threshold: 10,
187            cool_off: Duration::from_secs(60),
188            scope: BreakerScope::Host,
189        }
190    }
191}
192
193/// Whether a delivery may be sent, as a [`BreakerStore`] answers.
194#[derive(Debug)]
195pub enum Admission {
196    /// The breaker is closed: send it.
197    Pass,
198    /// The breaker is half-open, and this delivery is its probe: send it,
199    /// and give the probe back with its outcome.
200    Probe(Probe),
201    /// The breaker is open, or half-open with a probe under way: hold the
202    /// delivery, for about this long.
203    Held(Duration),
204}
205
206/// The right to probe a half-open breaker, which only one delivery holds at
207/// a time. What a store keeps in it — a lock it releases when it is dropped,
208/// say — is the store's own.
209pub struct Probe(Box<dyn std::any::Any + Send + Sync>);
210
211impl Probe {
212    /// A probe holding `guard`, which is dropped once its outcome is
213    /// recorded.
214    #[must_use]
215    pub fn new(guard: impl std::any::Any + Send + Sync) -> Self {
216        Self(Box::new(guard))
217    }
218
219    /// What the store kept in it.
220    #[must_use]
221    pub fn guard(&self) -> &(dyn std::any::Any + Send + Sync) {
222        &*self.0
223    }
224}
225
226impl std::fmt::Debug for Probe {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        f.write_str("Probe")
229    }
230}
231
232/// A future a [`BreakerStore`] answers with.
233pub type BreakerFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
234
235/// Where circuit breakers are kept: in the deliverer's memory unless the
236/// application gives it a store of its own, such as one in Redis that the
237/// deliverers of every process share, as Mastodon keeps its Stoplights.
238pub trait BreakerStore: Send + Sync + 'static {
239    /// Whether a delivery to `key` may be sent, as [`CircuitBreaker`] says.
240    /// Letting one through as a probe takes the probe, so that no other
241    /// delivery is let through until it is given back.
242    fn admit<'a>(
243        &'a self,
244        breaker: &'a CircuitBreaker,
245        key: &'a str,
246    ) -> BreakerFuture<'a, Admission>;
247
248    /// Record how a delivery to `key` that was let through went: as a
249    /// probe, when `probe` is the one [`BreakerStore::admit`] took for it,
250    /// which closes the breaker or opens it again, and is given back after.
251    fn record<'a>(
252        &'a self,
253        breaker: &'a CircuitBreaker,
254        key: &'a str,
255        failed: bool,
256        probe: Option<Probe>,
257    ) -> BreakerFuture<'a, ()>;
258}
259
260/// A destination's breaker, in memory.
261#[derive(Clone, Copy, Debug)]
262struct Breaker {
263    /// Failures in a row while closed.
264    failures: u32,
265    /// When it opened and until when, while it is open or half-open.
266    open_until: Option<Instant>,
267    /// The probe under way, and when it stops being waited for.
268    probe: Option<(u64, Instant)>,
269    /// When it last changed, to forget breakers nothing uses.
270    last: Instant,
271}
272
273/// Breakers in memory, each deliverer its own: the default.
274#[derive(Default)]
275pub struct MemoryBreakers {
276    breakers: Mutex<HashMap<String, Breaker>>,
277    probes: std::sync::atomic::AtomicU64,
278}
279
280impl MemoryBreakers {
281    fn admit_now(&self, breaker: &CircuitBreaker, key: &str) -> Admission {
282        let mut breakers = self.breakers.lock().expect("breaker lock");
283        let Some(state) = breakers.get_mut(key) else {
284            return Admission::Pass;
285        };
286        let now = Instant::now();
287        let Some(open_until) = state.open_until else {
288            return Admission::Pass;
289        };
290        if now < open_until {
291            return Admission::Held(open_until - now);
292        }
293        if let Some((_, until)) = state.probe
294            && now < until
295        {
296            return Admission::Held(until - now);
297        }
298        let id = self
299            .probes
300            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
301        state.probe = Some((id, now + breaker.cool_off));
302        // Entering recovery starts the count of failures again.
303        state.failures = 0;
304        state.last = now;
305        Admission::Probe(Probe::new(id))
306    }
307
308    fn record_now(&self, breaker: &CircuitBreaker, key: &str, failed: bool, probe: Option<&Probe>) {
309        let mut breakers = self.breakers.lock().expect("breaker lock");
310        let now = Instant::now();
311        // Destinations nothing has happened to for a while are forgotten, so
312        // that the map holds what is failing now.
313        if breakers.len() >= 4096 {
314            let forget = (breaker.cool_off * 2).max(Duration::from_secs(600));
315            breakers.retain(|_, state| state.last.elapsed() < forget);
316        }
317        if let Some(probe) = probe {
318            let Some(state) = breakers.get_mut(key) else {
319                return;
320            };
321            if failed {
322                state.open_until = Some(now + breaker.cool_off);
323            } else {
324                state.open_until = None;
325            }
326            state.last = now;
327            let id = probe.guard().downcast_ref::<u64>().copied();
328            if state.probe.map(|(held, _)| held) == id {
329                state.probe = None;
330            }
331            if state.open_until.is_none() && state.failures == 0 {
332                breakers.remove(key);
333            }
334            return;
335        }
336        if !failed {
337            if let Some(state) = breakers.get_mut(key) {
338                state.failures = 0;
339                state.last = now;
340                if state.open_until.is_none() {
341                    breakers.remove(key);
342                }
343            }
344            return;
345        }
346        let state = breakers.entry(key.to_owned()).or_insert(Breaker {
347            failures: 0,
348            open_until: None,
349            probe: None,
350            last: now,
351        });
352        state.failures = state.failures.saturating_add(1);
353        state.last = now;
354        if state.failures >= breaker.threshold.max(1) {
355            state.open_until = Some(now + breaker.cool_off);
356        }
357    }
358}
359
360impl BreakerStore for MemoryBreakers {
361    fn admit<'a>(
362        &'a self,
363        breaker: &'a CircuitBreaker,
364        key: &'a str,
365    ) -> BreakerFuture<'a, Admission> {
366        Box::pin(std::future::ready(self.admit_now(breaker, key)))
367    }
368
369    fn record<'a>(
370        &'a self,
371        breaker: &'a CircuitBreaker,
372        key: &'a str,
373        failed: bool,
374        probe: Option<Probe>,
375    ) -> BreakerFuture<'a, ()> {
376        self.record_now(breaker, key, failed, probe.as_ref());
377        Box::pin(std::future::ready(()))
378    }
379}
380
381/// A delivery that is done with, other than by running out of retries or
382/// its batch's deadline: what [`Deliverer::on_settled`] is told.
383#[derive(Clone, Debug)]
384pub struct Settled {
385    pub inbox: Url,
386    pub sender: String,
387    /// The tag of the batch it was sent in, if any.
388    pub tag: Option<String>,
389    pub outcome: SettledOutcome,
390}
391
392/// How a delivery settled.
393#[derive(Clone, Debug, Eq, PartialEq)]
394pub enum SettledOutcome {
395    /// The inbox accepted it.
396    Delivered,
397    /// The inbox answered with a status that will not change, by
398    /// [`DelivererConfig::permanent`].
399    Refused { status: Option<u16> },
400    /// The application's [`Deliverer::skip_if`] dropped it unsent.
401    Skipped,
402}
403
404/// How the delivery loop behaves.
405#[derive(Clone, Debug)]
406pub struct DelivererConfig {
407    /// The queue deliveries wait in, within the backend.
408    pub queue: String,
409    pub retry: RetryPolicy,
410    /// How many deliveries one claim takes.
411    pub batch: usize,
412    /// How long a claimed delivery is held before another worker may take it.
413    pub lease: Duration,
414    /// Deliveries in flight at once, in all.
415    pub concurrency: usize,
416    /// Deliveries in flight at once to one host.
417    pub per_host: usize,
418    /// The scheme a host is tried in until it has accepted one.
419    pub first_scheme: Scheme,
420    /// The longest the loop sleeps with nothing due, so that deliveries
421    /// queued by another process are picked up. Each sleep is up to a quarter
422    /// shorter, at random, so that the loops of many servers in one process
423    /// do not all wake at once.
424    pub idle_poll: Duration,
425    /// A limit on deliveries in flight shared with other deliverers, such as
426    /// every tenant's in a process that serves several; a delivery holds one
427    /// of its permits while it is being sent.
428    pub shared_limit: Option<Arc<Semaphore>>,
429    /// A lane for sends to few inboxes — a direct message, a reply, a follow
430    /// — whose deliveries take free slots before any other's.
431    ///
432    /// Deliveries are otherwise sent in the order they were queued, so a
433    /// message to one person queued just after a post to thousands of
434    /// servers waits for every one of those: two and a half minutes behind
435    /// three posts to 9,258 servers. Off unless given.
436    pub priority: Option<Priority>,
437    /// Holding back deliveries to a destination that keeps failing. On by
438    /// default; `None` sends every delivery whatever came of the last.
439    pub breaker: Option<CircuitBreaker>,
440    /// Which statuses an inbox answers with say that trying again cannot
441    /// help, so that the delivery is given up on at once:
442    /// [`permanent_status`] unless given.
443    pub permanent: fn(u16) -> bool,
444    /// A lane for what can wait — [`Batch::low_priority`] — claimed only once
445    /// the other queues have nothing due, as Sidekiq's lighter queues are.
446    /// Off unless given; without it such a batch waits in `queue`.
447    pub low_priority: Option<String>,
448    /// Which statuses also say so when the sender is gone, by
449    /// [`SenderKeys::gone`]: none unless given.
450    pub permanent_if_sender_gone: fn(u16) -> bool,
451    /// Whether a retry waits at least as long as the inbox asked with
452    /// `Retry-After`, and a delivery the circuit breaker held at least until
453    /// its cool-off ends, rather than only what the retry policy says. On
454    /// unless turned off; Mastodon, for one, retries on its schedule alone.
455    pub wait_as_asked: bool,
456}
457
458/// Where sends to few inboxes go, and how few is few.
459#[derive(Clone, Debug)]
460pub struct Priority {
461    /// The queue they wait in, within the backend.
462    pub queue: String,
463    /// A send to this many inboxes or fewer goes to the priority queue.
464    pub max_inboxes: usize,
465}
466
467impl Default for DelivererConfig {
468    fn default() -> Self {
469        Self {
470            queue: "delivery".to_owned(),
471            retry: RetryPolicy::default(),
472            batch: 64,
473            lease: Duration::from_secs(300),
474            concurrency: 16,
475            per_host: 2,
476            first_scheme: Scheme::DraftCavage,
477            idle_poll: Duration::from_secs(30),
478            shared_limit: None,
479            priority: None,
480            breaker: Some(CircuitBreaker::default()),
481            permanent: permanent_status,
482            low_priority: None,
483            permanent_if_sender_gone: |_| false,
484            wait_as_asked: true,
485        }
486    }
487}
488
489/// A portable actor's inbox, and the gateways that accept deliveries to it,
490/// in the order its actor lists them.
491#[derive(Clone, Debug)]
492pub struct PortableInbox {
493    pub inbox: ApUri,
494    pub gateways: Vec<Url>,
495}
496
497/// What a batch of deliveries shares: a tag to find them by, when to give up
498/// on the ones that have not gone through, and what they are sent in order
499/// with.
500#[derive(Clone, Debug, Default)]
501pub struct Batch {
502    /// Kept with each delivery, as `tag` in its payload, for the application
503    /// to follow the batch by.
504    pub tag: Option<String>,
505    /// A delivery that has not gone through by then is given up on, reported
506    /// to the failure handler with [`DeliveryFailure::deadline`] set. One that
507    /// would next be tried after it is given up on at once.
508    pub deadline: Option<std::time::SystemTime>,
509    /// Deliveries sent with the same key reach each inbox in the order they
510    /// were queued: one waits until those before it to that inbox have gone
511    /// through or been given up on. The key is typically the object an
512    /// activity is about, so that its `Delete` cannot overtake its `Create`
513    /// while the `Create` is being retried. Inboxes do not wait on each other.
514    pub ordering_key: Option<String>,
515    /// How many times each delivery is tried at most, when fewer than the
516    /// retry policy allows: its backoff stays the policy's.
517    pub max_attempts: Option<u32>,
518    /// Sent in [`DelivererConfig::low_priority`]'s lane, when there is one.
519    pub low_priority: bool,
520    /// Sent with a `Collection-Synchronization` header (FEP-8fcf), as
521    /// [`Deliverer::collection_synchronization`] writes it for the sender
522    /// and the inbox when the delivery is made.
523    pub synchronize_collection: bool,
524}
525
526/// One delivery, as it waits in the queue.
527struct Delivery {
528    activity: Value,
529    /// The inbox; a portable one's `ap` URI, percent-encoded as a `Url`.
530    inbox: Url,
531    /// Where to send it, in order, when that is not `inbox` itself: a
532    /// portable inbox at each of its gateways.
533    via: Vec<Url>,
534    sender: String,
535    tag: Option<String>,
536    /// Seconds since the Unix epoch.
537    deadline: Option<u64>,
538    max_attempts: Option<u32>,
539    synchronize_collection: bool,
540}
541
542impl Delivery {
543    fn new(activity: &Value, inbox: Url, via: Vec<Url>, sender: &str, batch: &Batch) -> Self {
544        Self {
545            activity: activity.clone(),
546            inbox,
547            via,
548            sender: sender.to_owned(),
549            tag: batch.tag.clone(),
550            deadline: batch.deadline.map(unix),
551            max_attempts: batch.max_attempts,
552            synchronize_collection: batch.synchronize_collection,
553        }
554    }
555
556    /// Whether the deadline, if there is one, is past at `now`.
557    fn expired(&self, now: std::time::SystemTime) -> bool {
558        self.deadline.is_some_and(|deadline| unix(now) >= deadline)
559    }
560
561    fn payload(&self) -> Value {
562        let mut payload = json!({
563            "activity": self.activity,
564            "inbox": self.inbox.as_str(),
565            "sender": self.sender,
566        });
567        if !self.via.is_empty() {
568            payload["via"] = self.via.iter().map(Url::as_str).collect::<Vec<_>>().into();
569        }
570        if let Some(tag) = &self.tag {
571            payload["tag"] = tag.as_str().into();
572        }
573        if let Some(deadline) = self.deadline {
574            payload["deadline"] = deadline.into();
575        }
576        if let Some(max_attempts) = self.max_attempts {
577            payload["max_attempts"] = max_attempts.into();
578        }
579        if self.synchronize_collection {
580            payload["synchronize_collection"] = true.into();
581        }
582        payload
583    }
584
585    fn from_payload(payload: &Value) -> Option<Self> {
586        let via = match payload.get("via") {
587            Some(Value::Array(via)) => via
588                .iter()
589                .map(|url| Url::parse(url.as_str()?).ok())
590                .collect::<Option<Vec<_>>>()?,
591            _ => Vec::new(),
592        };
593        Some(Self {
594            activity: payload.get("activity")?.clone(),
595            inbox: Url::parse(payload.get("inbox")?.as_str()?).ok()?,
596            via,
597            sender: payload.get("sender")?.as_str()?.to_owned(),
598            tag: payload
599                .get("tag")
600                .and_then(Value::as_str)
601                .map(str::to_owned),
602            deadline: payload.get("deadline").and_then(Value::as_u64),
603            max_attempts: payload
604                .get("max_attempts")
605                .and_then(Value::as_u64)
606                .and_then(|n| u32::try_from(n).ok()),
607            synchronize_collection: payload
608                .get("synchronize_collection")
609                .and_then(Value::as_bool)
610                .unwrap_or(false),
611        })
612    }
613
614    /// Where the delivery is sent, in the order to try.
615    fn targets(&self) -> Vec<&Url> {
616        if self.via.is_empty() {
617            vec![&self.inbox]
618        } else {
619            self.via.iter().collect()
620        }
621    }
622}
623
624type FailureHandler = Arc<dyn Fn(&DeliveryFailure) + Send + Sync>;
625type AttemptHandler = Arc<dyn Fn(&DeliveryAttempt) + Send + Sync>;
626type SkipPredicate = Arc<dyn Fn(&Url, &Value) -> bool + Send + Sync>;
627type SynchronizationFn = Arc<
628    dyn Fn(String, Url) -> std::pin::Pin<Box<dyn Future<Output = Option<String>> + Send>>
629        + Send
630        + Sync,
631>;
632type SettledHandler =
633    Arc<dyn Fn(Settled) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
634
635/// Queues activities and sends them.
636pub struct Deliverer<Q, K> {
637    queue: Q,
638    keys: K,
639    client: Client,
640    config: DelivererConfig,
641    wake: Notify,
642    /// The scheme each host last accepted.
643    schemes: Mutex<HashMap<String, Scheme>>,
644    hosts: Mutex<HashMap<String, Arc<Semaphore>>>,
645    /// Each destination's failures in a row, while it has any.
646    breakers: Arc<dyn BreakerStore>,
647    on_failure: Option<FailureHandler>,
648    on_attempt: Option<AttemptHandler>,
649    on_settled: Option<SettledHandler>,
650    skip_if: Option<SkipPredicate>,
651    synchronization: Option<SynchronizationFn>,
652}
653
654impl<Q: Queue, K: SenderKeys> Deliverer<Q, K> {
655    pub fn new(queue: Q, keys: K, client: Client, config: DelivererConfig) -> Self {
656        Self {
657            queue,
658            keys,
659            client,
660            config,
661            wake: Notify::new(),
662            schemes: Mutex::new(HashMap::new()),
663            hosts: Mutex::new(HashMap::new()),
664            breakers: Arc::new(MemoryBreakers::default()),
665            on_failure: None,
666            on_attempt: None,
667            on_settled: None,
668            skip_if: None,
669            synchronization: None,
670        }
671    }
672
673    /// The `Collection-Synchronization` header (FEP-8fcf) a delivery made
674    /// with [`Batch::synchronize_collection`] carries, as `header`, given
675    /// the sender and the inbox, writes it when the delivery is made: the
676    /// sender's followers collection, the digest of its followers on the
677    /// inbox's server, and where they are listed
678    /// ([`crate::synchronization::CollectionSynchronization`]). `None` sends
679    /// none.
680    #[must_use]
681    pub fn collection_synchronization<F, Fut>(mut self, header: F) -> Self
682    where
683        F: Fn(String, Url) -> Fut + Send + Sync + 'static,
684        Fut: Future<Output = Option<String>> + Send + 'static,
685    {
686        self.synchronization = Some(Arc::new(move |sender, inbox| {
687            Box::pin(header(sender, inbox))
688        }));
689        self
690    }
691
692    /// Keep circuit breakers in `store` rather than in this deliverer's
693    /// memory.
694    #[must_use]
695    pub fn breaker_store(mut self, store: impl BreakerStore) -> Self {
696        self.breakers = Arc::new(store);
697        self
698    }
699
700    /// Await `handler` for every delivery that settles — delivered, refused
701    /// for good, or skipped — before it leaves the queue: where an
702    /// application does what is to follow a delivery having gone through,
703    /// such as leaving the account a follower moved away from once the
704    /// `Follow` of the new one has. A process that stops in between sends
705    /// the delivery again, and calls `handler` again, so it is to be safe to
706    /// repeat.
707    #[must_use]
708    pub fn on_settled<F, Fut>(mut self, handler: F) -> Self
709    where
710        F: Fn(Settled) -> Fut + Send + Sync + 'static,
711        Fut: Future<Output = ()> + Send + 'static,
712    {
713        self.on_settled = Some(Arc::new(move |settled| Box::pin(handler(settled))));
714        self
715    }
716
717    /// Drop, unsent, each delivery that `skip`, given its inbox and its
718    /// activity, says no longer to send, when it comes due: where an
719    /// application stops delivering to a server it has marked unavailable
720    /// since the delivery was queued, as Mastodon does for all but a
721    /// `Follow`. A skipped delivery is complete, and neither attempted
722    /// nor failed; `skip` is asked as often as a delivery comes due, so it
723    /// is to answer from memory.
724    #[must_use]
725    pub fn skip_if(mut self, skip: impl Fn(&Url, &Value) -> bool + Send + Sync + 'static) -> Self {
726        self.skip_if = Some(Arc::new(skip));
727        self
728    }
729
730    /// Call `handler` after every attempt at a delivery, whatever came of it:
731    /// where an application keeps its own account of which servers answer,
732    /// as Mastodon's delivery failure tracker does.
733    #[must_use]
734    pub fn on_attempt(
735        mut self,
736        handler: impl Fn(&DeliveryAttempt) + Send + Sync + 'static,
737    ) -> Self {
738        self.on_attempt = Some(Arc::new(handler));
739        self
740    }
741
742    /// Call `handler` for every delivery given up on, which is where an
743    /// application marks a server that answered 410 as gone.
744    #[must_use]
745    pub fn on_failure(
746        mut self,
747        handler: impl Fn(&DeliveryFailure) + Send + Sync + 'static,
748    ) -> Self {
749        self.on_failure = Some(Arc::new(handler));
750        self
751    }
752
753    /// The queue backend deliveries wait in.
754    pub fn queue(&self) -> &Q {
755        &self.queue
756    }
757
758    /// Queue `activity`, signed by `sender`, for each of `inboxes`, once each.
759    ///
760    /// # Errors
761    ///
762    /// When the backend cannot queue them.
763    pub async fn send(
764        &self,
765        sender: &str,
766        activity: &Value,
767        inboxes: impl IntoIterator<Item = Url>,
768    ) -> Result<(), QueueError> {
769        self.send_batch(sender, activity, inboxes, &Batch::default())
770            .await
771    }
772
773    /// [`Deliverer::send`], in `batch`: tagged, given up on at its deadline,
774    /// and sent after the deliveries queued before it with its ordering key.
775    ///
776    /// # Errors
777    ///
778    /// When the backend cannot queue them.
779    pub async fn send_batch(
780        &self,
781        sender: &str,
782        activity: &Value,
783        inboxes: impl IntoIterator<Item = Url>,
784        batch: &Batch,
785    ) -> Result<(), QueueError> {
786        let inboxes: BTreeSet<Url> = inboxes.into_iter().collect();
787        if inboxes.is_empty() {
788            return Ok(());
789        }
790        let queue = match (&self.config.low_priority, batch.low_priority) {
791            (Some(low), true) => low.as_str(),
792            _ => self.queue_for(inboxes.len()),
793        };
794        match &batch.ordering_key {
795            Some(key) => {
796                let jobs = inboxes
797                    .into_iter()
798                    .map(|inbox| {
799                        // Each inbox is a sequence of its own.
800                        let ordering = format!("{key} {inbox}");
801                        let payload =
802                            Delivery::new(activity, inbox, Vec::new(), sender, batch).payload();
803                        (ordering, payload)
804                    })
805                    .collect();
806                self.queue.enqueue_ordered(queue, jobs).await?;
807            }
808            None => {
809                let payloads = inboxes
810                    .into_iter()
811                    .map(|inbox| {
812                        Delivery::new(activity, inbox, Vec::new(), sender, batch).payload()
813                    })
814                    .collect();
815                self.queue.enqueue(queue, payloads).await?;
816            }
817        }
818        self.wake.notify_one();
819        Ok(())
820    }
821
822    /// Queue `activity`, signed by `sender`, for each portable inbox, once
823    /// each: sent to the inbox at its first gateway that accepts it, of the
824    /// first [`MAX_GATEWAYS`](crate::portable::MAX_GATEWAYS) it lists that
825    /// are origins.
826    ///
827    /// # Errors
828    ///
829    /// When the backend cannot queue them.
830    pub async fn send_portable(
831        &self,
832        sender: &str,
833        activity: &Value,
834        inboxes: impl IntoIterator<Item = PortableInbox>,
835    ) -> Result<(), QueueError> {
836        let mut seen = BTreeSet::new();
837        let mut payloads = Vec::new();
838        for PortableInbox { inbox, gateways } in inboxes {
839            if !seen.insert(inbox.canonical()) {
840                continue;
841            }
842            let mut via: Vec<Url> = Vec::new();
843            for gateway in gateways
844                .iter()
845                .filter(|gateway| crate::portable::is_gateway(gateway.as_str()))
846            {
847                if let Ok(target) = Url::parse(&inbox.at_gateway(gateway.as_str()))
848                    && !via.contains(&target)
849                {
850                    via.push(target);
851                }
852            }
853            via.truncate(crate::portable::MAX_GATEWAYS);
854            let Ok(encoded) = Url::parse(&inbox.encoded()) else {
855                continue;
856            };
857            if via.is_empty() {
858                continue;
859            }
860            payloads
861                .push(Delivery::new(activity, encoded, via, sender, &Batch::default()).payload());
862        }
863        if payloads.is_empty() {
864            return Ok(());
865        }
866        self.queue
867            .enqueue(self.queue_for(payloads.len()), payloads)
868            .await?;
869        self.wake.notify_one();
870        Ok(())
871    }
872
873    /// Send what is queued, forever.
874    ///
875    /// A backend error is waited out and retried, never returned: the loop is
876    /// what keeps federation going, and a database that is briefly away should
877    /// not stop it.
878    pub async fn run(&self) {
879        self.run_until(std::future::pending()).await;
880    }
881
882    /// Send what is queued until `stop` completes.
883    ///
884    /// `concurrency` deliveries are kept in flight: as each finishes, its
885    /// slot is taken by the next due delivery, claimed a few slots at a time
886    /// rather than one round trip per delivery. A slow or silent inbox holds
887    /// one slot for as long as it takes, not a whole batch — sending a batch
888    /// and waiting for all of it held every healthy delivery claimed with a
889    /// server that never answered until the client gave up on it.
890    ///
891    /// Once `stop` has completed nothing more is claimed, and what is in
892    /// flight is finished first, so that a server shutting down does not drop
893    /// deliveries in the middle of sending them.
894    pub async fn run_until(&self, stop: impl Future<Output = ()>) {
895        tokio::pin!(stop);
896        let concurrency = self.config.concurrency.max(1);
897        // Claim once this many slots are free, or none are busy.
898        let refill = (concurrency / 4).max(1);
899        let mut in_flight = stream::FuturesUnordered::new();
900        // A claim under way, with how many it asked for. It is polled
901        // alongside the deliveries in flight, never instead of them: they may
902        // hold the database connections it is waiting for, and a delivery not
903        // polled holds its connection for as long. Nor is it ever dropped part
904        // way, which would strand what it had claimed until the lease lapsed.
905        let mut claiming: Option<(usize, Claim<'_>)> = None;
906        let mut stopping = false;
907        // Whether the last claim found the queue with nothing more due.
908        let mut dry = false;
909        loop {
910            let free = concurrency.saturating_sub(in_flight.len());
911            // Not after a claim that found nothing more due: until a delivery
912            // finishes, one is queued, or the idle wait ends, the next would
913            // find the same, and with nothing in flight would be asked for
914            // again at once, for ever.
915            if claiming.is_none()
916                && !stopping
917                && !dry
918                && free > 0
919                && (in_flight.is_empty() || free >= refill)
920            {
921                let asked = free.min(self.config.batch.max(1));
922                claiming = Some((asked, Box::pin(self.claim_due(asked))));
923            }
924            if in_flight.is_empty() && claiming.is_none() {
925                if stopping {
926                    return;
927                }
928                tokio::select! {
929                    () = &mut stop => return,
930                    () = self.idle() => {}
931                }
932                dry = false;
933                continue;
934            }
935            tokio::select! {
936                biased;
937                () = &mut stop, if !stopping => stopping = true,
938                result = poll_claim(&mut claiming), if claiming.is_some() => {
939                    let (asked, _) = claiming.take().expect("a claim was under way");
940                    match result {
941                        // What a claim took after a stop is sent too: it is
942                        // claimed, and would otherwise wait out its lease.
943                        Ok(jobs) => {
944                            dry = jobs.len() < asked;
945                            for job in jobs {
946                                in_flight.push(self.process(job));
947                            }
948                        }
949                        Err(_) if in_flight.is_empty() => {
950                            tokio::select! {
951                                () = &mut stop, if !stopping => return,
952                                () = tokio::time::sleep(Duration::from_secs(5)) => {}
953                            }
954                        }
955                        Err(_) => dry = true,
956                    }
957                }
958                // A delivery that finishes may leave room for a retry that has
959                // come due since the last claim.
960                Some(()) = in_flight.next(), if !in_flight.is_empty() => dry = false,
961                // New deliveries queued while slots are free are sent now,
962                // not when the next one in flight happens to finish.
963                () = self.wake.notified(), if dry && !stopping && claiming.is_none() => dry = false,
964            }
965        }
966    }
967
968    /// Claim one batch and send it. Returns how many deliveries it held.
969    ///
970    /// # Errors
971    ///
972    /// When the backend cannot be read.
973    pub async fn run_once(&self) -> Result<usize, QueueError> {
974        let batch = self.claim_due(self.config.batch).await?;
975        let count = batch.len();
976        stream::iter(batch)
977            .for_each_concurrent(self.config.concurrency, |job| self.process(job))
978            .await;
979        Ok(count)
980    }
981
982    /// The queue a send to `inboxes` inboxes waits in.
983    fn queue_for(&self, inboxes: usize) -> &str {
984        match &self.config.priority {
985            Some(priority) if inboxes <= priority.max_inboxes => &priority.queue,
986            _ => &self.config.queue,
987        }
988    }
989
990    /// Claim up to `limit` due deliveries, the priority queue's first.
991    async fn claim_due(&self, limit: usize) -> Result<Vec<Job>, QueueError> {
992        let mut jobs = match &self.config.priority {
993            Some(priority) => {
994                self.queue
995                    .claim(&priority.queue, limit, self.config.lease)
996                    .await?
997            }
998            None => Vec::new(),
999        };
1000        if jobs.len() < limit {
1001            jobs.extend(
1002                self.queue
1003                    .claim(&self.config.queue, limit - jobs.len(), self.config.lease)
1004                    .await?,
1005            );
1006        }
1007        if let Some(low) = &self.config.low_priority
1008            && jobs.len() < limit
1009        {
1010            jobs.extend(
1011                self.queue
1012                    .claim(low, limit - jobs.len(), self.config.lease)
1013                    .await?,
1014            );
1015        }
1016        Ok(jobs)
1017    }
1018
1019    async fn idle(&self) {
1020        let mut due = self.queue.next_due(&self.config.queue).await.ok().flatten();
1021        let lanes = self
1022            .config
1023            .priority
1024            .iter()
1025            .map(|priority| priority.queue.as_str())
1026            .chain(self.config.low_priority.as_deref());
1027        for lane in lanes {
1028            let next = self.queue.next_due(lane).await.ok().flatten();
1029            due = match (due, next) {
1030                (Some(a), Some(b)) => Some(a.min(b)),
1031                (a, b) => a.or(b),
1032            };
1033        }
1034        let sleep = jitter(due.map_or(self.config.idle_poll, |due| due.min(self.config.idle_poll)));
1035        tokio::select! {
1036            () = tokio::time::sleep(sleep) => {}
1037            () = self.wake.notified() => {}
1038        }
1039    }
1040
1041    async fn process(&self, job: Job) {
1042        let Some(delivery) = Delivery::from_payload(&job.payload) else {
1043            let _ = self.queue.fail(&job.id, "not a delivery").await;
1044            return;
1045        };
1046        // Past its batch's deadline, it is given up on unsent: the batch is
1047        // to have finished by now.
1048        if delivery.expired(std::time::SystemTime::now()) {
1049            let _ = self
1050                .give_up(
1051                    &job,
1052                    &delivery,
1053                    None,
1054                    "the batch's deadline passed before it was sent",
1055                    true,
1056                )
1057                .await;
1058            return;
1059        }
1060        if self
1061            .skip_if
1062            .as_ref()
1063            .is_some_and(|skip| skip(&delivery.inbox, &delivery.activity))
1064        {
1065            self.settle(&delivery, SettledOutcome::Skipped).await;
1066            let _ = self.queue.complete(&job.id).await;
1067            return;
1068        }
1069        let outcome = self.attempt(&delivery).await;
1070        let permanent = match &outcome {
1071            Ok(()) => false,
1072            Err(error) => self.ends(&delivery.sender, error).await,
1073        };
1074        if let Some(handler) = &self.on_attempt {
1075            handler(&DeliveryAttempt {
1076                inbox: delivery.inbox.clone(),
1077                sender: delivery.sender.clone(),
1078                outcome: match &outcome {
1079                    Ok(()) => AttemptOutcome::Delivered,
1080                    Err(DeliveryError::Held { .. }) => AttemptOutcome::Held,
1081                    Err(error) => AttemptOutcome::Failed {
1082                        status: error.status(),
1083                        permanent,
1084                        error: error.to_string(),
1085                    },
1086                },
1087            });
1088        }
1089        // A backend error here leaves the job claimed; its lease lapses and it
1090        // is tried again, which is the safe way to be wrong.
1091        let _ = match outcome {
1092            Ok(()) => {
1093                self.settle(&delivery, SettledOutcome::Delivered).await;
1094                self.queue.complete(&job.id).await
1095            }
1096            Err(error) => {
1097                if permanent {
1098                    let status = error.status();
1099                    self.settle(&delivery, SettledOutcome::Refused { status })
1100                        .await;
1101                }
1102                self.record_failure(&job, &delivery, &error, permanent)
1103                    .await
1104            }
1105        };
1106    }
1107
1108    async fn settle(&self, delivery: &Delivery, outcome: SettledOutcome) {
1109        if let Some(handler) = &self.on_settled {
1110            handler(Settled {
1111                inbox: delivery.inbox.clone(),
1112                sender: delivery.sender.clone(),
1113                tag: delivery.tag.clone(),
1114                outcome,
1115            })
1116            .await;
1117        }
1118    }
1119
1120    /// Whether `error` ends a delivery from `sender`: a status
1121    /// [`DelivererConfig::permanent`] names, or one
1122    /// [`DelivererConfig::permanent_if_sender_gone`] names when the sender is
1123    /// gone.
1124    async fn ends(&self, sender: &str, error: &DeliveryError) -> bool {
1125        if error.is_permanent_by(self.config.permanent) {
1126            return true;
1127        }
1128        match error.status() {
1129            Some(status) if (self.config.permanent_if_sender_gone)(status) => {
1130                self.keys.gone(sender).await
1131            }
1132            _ => false,
1133        }
1134    }
1135
1136    async fn attempt(&self, delivery: &Delivery) -> Result<(), DeliveryError> {
1137        let key = match self.keys.key(&delivery.sender).await {
1138            Ok(Some(key)) => key,
1139            Ok(None) => {
1140                return Err(DeliveryError::Signing(format!(
1141                    "no key for sender {}",
1142                    delivery.sender
1143                )));
1144            }
1145            // The key store is the application's database; it being away
1146            // for a moment does not make the delivery wrong.
1147            Err(error) => {
1148                return Err(DeliveryError::Unavailable(format!("key lookup: {error}")));
1149            }
1150        };
1151        let body = serde_json::to_vec(&delivery.activity)
1152            .map_err(|error| DeliveryError::Signing(error.to_string()))?;
1153        // Each gateway in turn: the first that accepts completes the
1154        // delivery, and one that fails is passed over rather than retried.
1155        let mut extra = Vec::new();
1156        if delivery.synchronize_collection
1157            && let Some(synchronization) = &self.synchronization
1158            && let Some(header) =
1159                synchronization(delivery.sender.clone(), delivery.inbox.clone()).await
1160        {
1161            extra.push(("collection-synchronization".to_owned(), header));
1162        }
1163        let mut last = None;
1164        for target in delivery.targets() {
1165            match self
1166                .attempt_at(target, &body, &key, &delivery.sender, &extra)
1167                .await
1168            {
1169                Ok(()) => return Ok(()),
1170                Err(error) => last = Some(error),
1171            }
1172        }
1173        Err(last.unwrap_or_else(|| DeliveryError::Signing("nowhere to deliver".into())))
1174    }
1175
1176    async fn attempt_at(
1177        &self,
1178        target: &Url,
1179        body: &[u8],
1180        key: &SenderKey,
1181        sender: &str,
1182        extra: &[(String, String)],
1183    ) -> Result<(), DeliveryError> {
1184        let host = target.host_str().unwrap_or_default().to_owned();
1185        let breaker = self.config.breaker.map(|breaker| {
1186            let key = match breaker.scope {
1187                BreakerScope::Host => host.clone(),
1188                BreakerScope::Inbox => target.as_str().to_owned(),
1189            };
1190            (breaker, key)
1191        });
1192        let mut probe = None;
1193        if let Some((breaker, key)) = &breaker {
1194            match self.breakers.admit(breaker, key).await {
1195                Admission::Pass => {}
1196                Admission::Probe(taken) => probe = Some(taken),
1197                Admission::Held(retry_after) => {
1198                    return Err(DeliveryError::Held { retry_after });
1199                }
1200            }
1201        }
1202        let permit = self.host_semaphore(&host).acquire_owned().await;
1203        let shared_permit = match &self.config.shared_limit {
1204            Some(limit) => Some(limit.clone().acquire_owned().await),
1205            None => None,
1206        };
1207        let first = self
1208            .schemes
1209            .lock()
1210            .expect("scheme lock")
1211            .get(&host)
1212            .copied()
1213            .unwrap_or(self.config.first_scheme);
1214        let result =
1215            post::deliver_with_headers(&self.client, target, body, key, first, extra).await;
1216        drop(shared_permit);
1217        drop(permit);
1218        if let Some((breaker, key)) = &breaker {
1219            let failed = match &result {
1220                Ok(_) => false,
1221                Err(error) => !self.ends(sender, error).await,
1222            };
1223            self.breakers.record(breaker, key, failed, probe).await;
1224        }
1225        let accepted = result?;
1226        self.schemes
1227            .lock()
1228            .expect("scheme lock")
1229            .insert(host, accepted);
1230        Ok(())
1231    }
1232
1233    fn host_semaphore(&self, host: &str) -> Arc<Semaphore> {
1234        self.hosts
1235            .lock()
1236            .expect("host lock")
1237            .entry(host.to_owned())
1238            .or_insert_with(|| Arc::new(Semaphore::new(self.config.per_host)))
1239            .clone()
1240    }
1241
1242    async fn record_failure(
1243        &self,
1244        job: &Job,
1245        delivery: &Delivery,
1246        error: &DeliveryError,
1247        permanent: bool,
1248    ) -> Result<(), QueueError> {
1249        let attempts = job.attempts + 1;
1250        let delay = if permanent || delivery.max_attempts.is_some_and(|max| attempts >= max) {
1251            None
1252        } else {
1253            self.config.retry.delay(attempts)
1254        };
1255        let message = error.to_string();
1256        match delay {
1257            Some(delay) => {
1258                let delay = match error.retry_after() {
1259                    Some(asked) if self.config.wait_as_asked => asked.max(delay),
1260                    _ => delay,
1261                };
1262                // A retry that would come after the batch's deadline is not
1263                // made: the batch finishes on time, not when the server
1264                // comes back.
1265                if delivery.expired(std::time::SystemTime::now() + delay) {
1266                    let message = format!("the batch's deadline passed: {message}");
1267                    return self
1268                        .give_up(job, delivery, error.status(), &message, true)
1269                        .await;
1270                }
1271                self.queue.retry(&job.id, delay, &message).await
1272            }
1273            None => {
1274                self.give_up(job, delivery, error.status(), &message, false)
1275                    .await
1276            }
1277        }
1278    }
1279
1280    /// Fail `job` for good, and tell the failure handler.
1281    async fn give_up(
1282        &self,
1283        job: &Job,
1284        delivery: &Delivery,
1285        status: Option<u16>,
1286        message: &str,
1287        deadline: bool,
1288    ) -> Result<(), QueueError> {
1289        if let Some(handler) = &self.on_failure {
1290            handler(&DeliveryFailure {
1291                inbox: delivery.inbox.clone(),
1292                sender: delivery.sender.clone(),
1293                status,
1294                error: message.to_owned(),
1295                tag: delivery.tag.clone(),
1296                deadline,
1297            });
1298        }
1299        self.queue.fail(&job.id, message).await
1300    }
1301}
1302
1303/// `time` as whole seconds since the Unix epoch.
1304fn unix(time: std::time::SystemTime) -> u64 {
1305    time.duration_since(std::time::UNIX_EPOCH)
1306        .map_or(0, |since| since.as_secs())
1307}
1308
1309/// `duration`, up to a quarter shorter at random.
1310fn jitter(duration: Duration) -> Duration {
1311    use std::hash::{BuildHasher as _, RandomState};
1312
1313    // A fresh RandomState is seeded from the operating system's randomness,
1314    // which is all a wake-up time needs; no random-number crate for this.
1315    let random = RandomState::new().hash_one(std::time::SystemTime::now());
1316    let quarter = duration / 4;
1317    let cut = quarter.mul_f64((random % 1_000_000) as f64 / 1_000_000.0);
1318    duration.saturating_sub(cut)
1319}
1320
1321/// A claim under way in [`Deliverer::run_until`].
1322type Claim<'a> = std::pin::Pin<Box<dyn Future<Output = Result<Vec<Job>, QueueError>> + Send + 'a>>;
1323
1324/// Wait on the claim under way, leaving it in place: the caller takes it once
1325/// it has finished.
1326async fn poll_claim(claiming: &mut Option<(usize, Claim<'_>)>) -> Result<Vec<Job>, QueueError> {
1327    match claiming {
1328        Some((_, claim)) => claim.as_mut().await,
1329        None => std::future::pending().await,
1330    }
1331}
1332
1333#[cfg(test)]
1334mod tests {
1335    use super::jitter;
1336    use std::time::Duration;
1337
1338    #[test]
1339    fn jitter_shortens_by_at_most_a_quarter() {
1340        let base = Duration::from_secs(300);
1341        for _ in 0..100 {
1342            let slept = jitter(base);
1343            assert!(slept <= base && slept >= base * 3 / 4, "{slept:?}");
1344        }
1345    }
1346
1347    mod breakers {
1348        use super::super::{Admission, BreakerScope, CircuitBreaker, MemoryBreakers};
1349        use std::time::Duration;
1350
1351        const BREAKER: CircuitBreaker = CircuitBreaker {
1352            threshold: 2,
1353            cool_off: Duration::from_millis(50),
1354            scope: BreakerScope::Inbox,
1355        };
1356
1357        fn open(breakers: &MemoryBreakers) {
1358            breakers.record_now(&BREAKER, "inbox", true, None);
1359            breakers.record_now(&BREAKER, "inbox", true, None);
1360        }
1361
1362        #[test]
1363        fn a_success_while_closed_starts_the_count_again() {
1364            let breakers = MemoryBreakers::default();
1365            breakers.record_now(&BREAKER, "inbox", true, None);
1366            breakers.record_now(&BREAKER, "inbox", false, None);
1367            breakers.record_now(&BREAKER, "inbox", true, None);
1368            assert!(matches!(
1369                breakers.admit_now(&BREAKER, "inbox"),
1370                Admission::Pass
1371            ));
1372            breakers.record_now(&BREAKER, "inbox", true, None);
1373            assert!(matches!(
1374                breakers.admit_now(&BREAKER, "inbox"),
1375                Admission::Held(_)
1376            ));
1377        }
1378
1379        #[test]
1380        fn a_success_sent_before_it_opened_does_not_close_it() {
1381            let breakers = MemoryBreakers::default();
1382            open(&breakers);
1383            breakers.record_now(&BREAKER, "inbox", false, None);
1384            assert!(matches!(
1385                breakers.admit_now(&BREAKER, "inbox"),
1386                Admission::Held(_)
1387            ));
1388        }
1389
1390        #[test]
1391        fn after_the_cool_off_one_probe_at_a_time_is_let_through() {
1392            let breakers = MemoryBreakers::default();
1393            open(&breakers);
1394            std::thread::sleep(BREAKER.cool_off);
1395            let Admission::Probe(probe) = breakers.admit_now(&BREAKER, "inbox") else {
1396                panic!("the first delivery after the cool-off probes");
1397            };
1398            assert!(
1399                matches!(breakers.admit_now(&BREAKER, "inbox"), Admission::Held(_)),
1400                "the others are held while the probe is under way"
1401            );
1402            // A failed probe opens it for another cool-off.
1403            breakers.record_now(&BREAKER, "inbox", true, Some(&probe));
1404            assert!(matches!(
1405                breakers.admit_now(&BREAKER, "inbox"),
1406                Admission::Held(_)
1407            ));
1408            std::thread::sleep(BREAKER.cool_off);
1409            let Admission::Probe(probe) = breakers.admit_now(&BREAKER, "inbox") else {
1410                panic!("probed again after another cool-off");
1411            };
1412            // A successful one closes it.
1413            breakers.record_now(&BREAKER, "inbox", false, Some(&probe));
1414            assert!(matches!(
1415                breakers.admit_now(&BREAKER, "inbox"),
1416                Admission::Pass
1417            ));
1418            breakers.record_now(&BREAKER, "inbox", true, None);
1419            assert!(
1420                matches!(breakers.admit_now(&BREAKER, "inbox"), Admission::Pass),
1421                "closed, it counts its failures from nought"
1422            );
1423        }
1424
1425        #[test]
1426        fn a_probe_that_never_reports_back_is_given_up_after_the_cool_off() {
1427            let breakers = MemoryBreakers::default();
1428            open(&breakers);
1429            std::thread::sleep(BREAKER.cool_off);
1430            assert!(matches!(
1431                breakers.admit_now(&BREAKER, "inbox"),
1432                Admission::Probe(_)
1433            ));
1434            std::thread::sleep(BREAKER.cool_off);
1435            assert!(matches!(
1436                breakers.admit_now(&BREAKER, "inbox"),
1437                Admission::Probe(_)
1438            ));
1439        }
1440    }
1441}