1mod policy;
4
5use std::collections::HashMap;
6use std::collections::hash_map::RandomState;
7use std::hash::{BuildHasher, Hasher};
8use std::num::NonZeroU32;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Duration;
12
13use governor::clock::DefaultClock;
14use governor::state::{InMemoryState, NotKeyed};
15use governor::{Jitter, Quota, RateLimiter};
16use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore};
17use tokio::time::Instant;
18
19pub use policy::{
20 CAUTIOUS_LIMIT, DEFAULT_RETRY_AFTER, HostLimit, MAX_RETRY_AFTER, RESOLVER_HOST, RESOLVER_LIMIT,
21 clamp_retry_after, published_limit, starting_limit,
22};
23
24const PROBE_DEADLINE: Duration = Duration::from_secs(30);
25
26const BEHIND_A_PROBE: Duration = Duration::from_millis(250);
28
29const SETTLED_AFTER: Duration = Duration::from_secs(60);
31
32type TokenBucket = RateLimiter<NotKeyed, InMemoryState, DefaultClock>;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Refusal {
37 Throttled { retry_after: Option<Duration> },
38 Blocked,
39 Dropped,
40}
41
42impl Refusal {
43 #[must_use]
44 fn base_wait(&self) -> Duration {
45 match self {
46 Self::Throttled { retry_after } => clamp_retry_after(*retry_after),
47 Self::Blocked => MAX_RETRY_AFTER,
48 Self::Dropped => DEFAULT_RETRY_AFTER,
49 }
50 }
51
52 #[must_use]
53 const fn has_stated_wait(&self) -> bool {
54 matches!(
55 self,
56 Self::Throttled {
57 retry_after: Some(_)
58 }
59 )
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct PacingLimits {
65 pub total_concurrency: usize,
66 pub refusal_threshold: u32,
67 pub recovery_threshold: u32,
68 pub jitter: Duration,
70 pub max_backoff: Duration,
71 pub is_cautious: bool,
72 pub per_registry: Option<usize>,
73 pub rate: Option<u32>,
74}
75
76impl Default for PacingLimits {
77 fn default() -> Self {
78 Self {
79 total_concurrency: 24,
80 refusal_threshold: 3,
81 recovery_threshold: 8,
82 jitter: Duration::from_millis(120),
83 max_backoff: Duration::from_secs(120),
84 is_cautious: false,
85 per_registry: None,
86 rate: None,
87 }
88 }
89}
90
91impl PacingLimits {
92 pub fn check(&self) -> Result<(), &'static str> {
94 if self.total_concurrency == 0 {
95 return Err("total_concurrency must be at least one");
96 }
97 if self.per_registry == Some(0) {
98 return Err("per_registry must be at least one");
99 }
100 if self.rate == Some(0) {
101 return Err("rate must be at least one");
102 }
103 if self.refusal_threshold == 0 {
104 return Err("refusal_threshold must be at least one");
105 }
106 Ok(())
107 }
108
109 pub fn cautious() -> Self {
110 Self {
111 total_concurrency: 8,
112 refusal_threshold: 2,
113 recovery_threshold: 16,
114 jitter: Duration::from_millis(300),
115 is_cautious: true,
116 ..Self::default()
117 }
118 }
119
120 fn limit_for(&self, host: &str) -> HostLimit {
121 let mut limit = if self.is_cautious {
122 CAUTIOUS_LIMIT
123 } else {
124 starting_limit(host)
125 };
126 if let Some(concurrency) = self.per_registry {
127 limit.concurrency = concurrency.max(1);
128 }
129 if let Some(queries) = self.rate {
130 limit.queries = queries.max(1);
131 limit.window = Duration::from_secs(1);
132 }
133
134 if self.is_cautious {
136 limit.concurrency = limit.concurrency.min(CAUTIOUS_LIMIT.concurrency);
137 if limit.per_second_rate() > CAUTIOUS_LIMIT.per_second_rate() {
138 limit.queries = CAUTIOUS_LIMIT.queries;
139 limit.window = CAUTIOUS_LIMIT.window;
140 }
141 }
142 limit
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum BreakerState {
148 Closed,
149 Open,
150 HalfOpen,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct PausedHost {
155 pub host: String,
156 pub remaining_wait: Duration,
157 pub refusals: u32,
158}
159
160#[derive(Debug)]
162pub struct RequestPermit {
163 host: String,
164 is_probe: bool,
165 _global_slot: OwnedSemaphorePermit,
166 _host_slot: OwnedSemaphorePermit,
167}
168
169impl RequestPermit {
170 #[must_use]
171 pub const fn is_probe(&self) -> bool {
172 self.is_probe
173 }
174
175 #[must_use]
176 pub fn host(&self) -> &str {
177 &self.host
178 }
179}
180
181#[derive(Debug)]
182struct HostState {
183 limiter: Arc<TokenBucket>,
184 slots: Arc<Semaphore>,
185 concurrency: usize,
186 max_concurrency: usize,
187 refusals: u32,
188 successes: u32,
189 open_until: Option<Instant>,
190 probe_until: Option<Instant>,
192 is_probing: bool,
193 last_used: Instant,
195}
196
197fn usable_permits(requested: usize) -> usize {
199 requested.clamp(1, Semaphore::MAX_PERMITS)
200}
201
202impl HostState {
203 fn new(limit: HostLimit) -> Self {
204 Self {
205 limiter: Arc::new(RateLimiter::direct(quota_for(limit))),
206 slots: Arc::new(Semaphore::new(usable_permits(limit.concurrency))),
207 concurrency: limit.concurrency.max(1),
208 max_concurrency: limit.concurrency.max(1),
209 refusals: 0,
210 successes: 0,
211 open_until: None,
212 probe_until: None,
213 is_probing: false,
214 last_used: Instant::now(),
215 }
216 }
217
218 fn breaker(&self, now: Instant) -> BreakerState {
219 match self.open_until {
220 Some(until) if now < until => BreakerState::Open,
221 Some(_) => BreakerState::HalfOpen,
222 None if self.is_probing => BreakerState::HalfOpen,
223 None => BreakerState::Closed,
224 }
225 }
226
227 fn probe_abandoned(&self, now: Instant) -> bool {
229 self.probe_until.is_none_or(|until| now >= until)
230 }
231}
232
233fn quota_for(limit: HostLimit) -> Quota {
234 let rate = limit.per_second_rate().max(0.05);
235 let interval = Duration::from_secs_f64(1.0 / rate);
236 let burst = NonZeroU32::new(limit.queries.max(1)).unwrap_or(NonZeroU32::MIN);
237 Quota::with_period(interval).map_or_else(
238 || Quota::per_second(NonZeroU32::MIN),
239 |quota| quota.allow_burst(burst),
240 )
241}
242
243#[derive(Debug)]
244struct JitterRng(AtomicU64);
245
246impl JitterRng {
247 fn new() -> Self {
248 let seed = RandomState::new().build_hasher().finish() | 1;
249 Self(AtomicU64::new(seed))
250 }
251
252 fn next_u64(&self) -> u64 {
253 let mut x = self.0.load(Ordering::Relaxed);
254 x ^= x << 13;
255 x ^= x >> 7;
256 x ^= x << 17;
257 self.0.store(x, Ordering::Relaxed);
258 x
259 }
260
261 fn sample_up_to(&self, ceiling: Duration) -> Duration {
263 let nanos = u64::try_from(ceiling.as_nanos()).unwrap_or(u64::MAX);
264 if nanos == 0 {
265 return Duration::ZERO;
266 }
267 Duration::from_nanos(self.next_u64() % nanos.saturating_add(1))
268 }
269}
270
271#[derive(Debug)]
272pub struct Pacer {
273 pacing: PacingLimits,
274 global_slots: Arc<Semaphore>,
275 hosts: Mutex<HashMap<String, HostState>>,
276 jitter: JitterRng,
277}
278
279impl Pacer {
280 #[must_use]
281 pub fn new(pacing: PacingLimits) -> Self {
282 Self {
283 global_slots: Arc::new(Semaphore::new(usable_permits(pacing.total_concurrency))),
284 hosts: Mutex::new(HashMap::new()),
285 jitter: JitterRng::new(),
286 pacing,
287 }
288 }
289
290 #[must_use]
291 pub const fn pacing(&self) -> &PacingLimits {
292 &self.pacing
293 }
294
295 pub async fn acquire_patiently(
297 &self,
298 host: &str,
299 budget: Duration,
300 ) -> Result<RequestPermit, PausedHost> {
301 match self.acquire(host).await {
302 Ok(permit) => Ok(permit),
303 Err(paused) => {
304 let wait = paused.remaining_wait;
305 if wait.is_zero() || wait > budget {
306 return Err(paused);
307 }
308 tokio::time::sleep(wait).await;
309 self.acquire(host).await
310 }
311 }
312 }
313
314 pub async fn acquire(&self, host: &str) -> Result<RequestPermit, PausedHost> {
315 let key = normalize_host(host);
316
317 let (limiter, slots, probe) = {
318 let mut hosts = self.hosts.lock().await;
319 let state = hosts
320 .entry(key.clone())
321 .or_insert_with(|| HostState::new(self.pacing.limit_for(&key)));
322
323 let now = Instant::now();
324 state.last_used = now;
325 match state.breaker(now) {
326 BreakerState::Open => {
327 let remaining = state
328 .open_until
329 .map_or(Duration::ZERO, |until| until.saturating_duration_since(now));
330 return Err(PausedHost {
331 host: crate::lookup::scrub(&key),
332 remaining_wait: remaining,
333 refusals: state.refusals,
334 });
335 }
336 BreakerState::HalfOpen => {
337 if state.is_probing && !state.probe_abandoned(now) {
338 return Err(PausedHost {
340 host: crate::lookup::scrub(&key),
341 remaining_wait: BEHIND_A_PROBE,
342 refusals: state.refusals,
343 });
344 }
345 state.is_probing = true;
346 state.probe_until = Some(now + PROBE_DEADLINE);
347 state.open_until = None;
348 (Arc::clone(&state.limiter), Arc::clone(&state.slots), true)
349 }
350 BreakerState::Closed => {
351 (Arc::clone(&state.limiter), Arc::clone(&state.slots), false)
352 }
353 }
354 };
355
356 if self.pacing.jitter.is_zero() {
358 limiter.until_ready().await;
359 } else {
360 limiter
361 .until_ready_with_jitter(Jitter::up_to(self.pacing.jitter))
362 .await;
363 }
364
365 let host_permit = slots
366 .acquire_owned()
367 .await
368 .map_err(|_| self.shutdown_pause(&key))?;
369 let global = Arc::clone(&self.global_slots)
370 .acquire_owned()
371 .await
372 .map_err(|_| self.shutdown_pause(&key))?;
373
374 Ok(RequestPermit {
375 host: crate::lookup::scrub(&key),
376 is_probe: probe,
377 _global_slot: global,
378 _host_slot: host_permit,
379 })
380 }
381
382 fn shutdown_pause(&self, host: &str) -> PausedHost {
383 PausedHost {
384 host: host.to_owned(),
385 remaining_wait: Duration::ZERO,
386 refusals: 0,
387 }
388 }
389
390 pub async fn record_success(&self, host: &str) {
392 let key = normalize_host(host);
393 let mut hosts = self.hosts.lock().await;
394 let Some(state) = hosts.get_mut(&key) else {
395 return;
396 };
397
398 let now = Instant::now();
400 if state.open_until.is_some_and(|until| until > now) {
401 state.successes = state.successes.saturating_add(1);
402 return;
403 }
404 state.refusals = 0;
405 state.open_until = None;
406 state.is_probing = false;
407 state.probe_until = None;
408 state.successes = state.successes.saturating_add(1);
409
410 if state.successes >= self.pacing.recovery_threshold
411 && state.concurrency < state.max_concurrency
412 {
413 state.successes = 0;
414 state.concurrency = state
415 .concurrency
416 .saturating_add(1)
417 .min(state.max_concurrency);
418 state.slots.add_permits(1);
419 tracing::debug!(host = %key, concurrency = state.concurrency, "registry concurrency raised");
420 }
421 }
422
423 pub async fn record_refusal(&self, host: &str, refusal: &Refusal) -> Duration {
424 let key = normalize_host(host);
425 let mut hosts = self.hosts.lock().await;
426 let state = hosts
427 .entry(key.clone())
428 .or_insert_with(|| HostState::new(self.pacing.limit_for(&key)));
429
430 state.refusals = state.refusals.saturating_add(1);
431 state.successes = 0;
432 state.is_probing = false;
433 state.probe_until = None;
434
435 let wait = if refusal.has_stated_wait() {
436 refusal.base_wait()
437 } else {
438 let exponent = state.refusals.saturating_sub(1).min(6);
439 let ceiling = refusal
440 .base_wait()
441 .saturating_mul(1_u32 << exponent)
442 .min(self.pacing.max_backoff);
443 self.jitter.sample_up_to(ceiling)
444 };
445 let wait = wait.min(self.pacing.max_backoff);
446
447 if state.concurrency > 1 {
448 let target = state
450 .concurrency
451 .saturating_mul(3)
452 .div_ceil(4)
453 .min(state.concurrency.saturating_sub(1))
454 .max(1);
455 let surplus = state.concurrency.saturating_sub(target);
456 if surplus > 0 {
457 let forgotten = state.slots.forget_permits(surplus);
459 state.concurrency = state.concurrency.saturating_sub(forgotten);
460 tracing::debug!(host = %key, concurrency = state.concurrency, "registry concurrency cut");
461 }
462 }
463
464 let should_open =
465 matches!(refusal, Refusal::Blocked) || state.refusals >= self.pacing.refusal_threshold;
466 if should_open {
467 let until = Instant::now() + wait;
469 state.open_until = Some(state.open_until.map_or(until, |held| held.max(until)));
470 tracing::debug!(host = %key, refusals = state.refusals, ?wait, "registry paused");
471 }
472
473 wait
474 }
475
476 pub async fn breaker(&self, host: &str) -> BreakerState {
477 let key = normalize_host(host);
478 let hosts = self.hosts.lock().await;
479 hosts
480 .get(&key)
481 .map_or(BreakerState::Closed, |state| state.breaker(Instant::now()))
482 }
483
484 pub async fn paused(&self, host: &str) -> Option<PausedHost> {
485 let key = normalize_host(host);
486 let mut hosts = self.hosts.lock().await;
487 let state = hosts.get_mut(&key)?;
488 let open_until = state.open_until?;
489 let now = Instant::now();
490 if now >= open_until {
491 return None;
492 }
493 Some(PausedHost {
494 host: crate::lookup::scrub(&key),
495 remaining_wait: open_until.saturating_duration_since(now),
496 refusals: state.refusals,
497 })
498 }
499
500 pub async fn paused_hosts(&self) -> Vec<PausedHost> {
501 let now = Instant::now();
502 let hosts = self.hosts.lock().await;
503 let mut paused: Vec<PausedHost> = hosts
504 .iter()
505 .filter_map(|(host, state)| {
506 let open_until = state.open_until?;
507 (open_until > now).then(|| PausedHost {
508 host: host.clone(),
509 remaining_wait: open_until.saturating_duration_since(now),
510 refusals: state.refusals,
511 })
512 })
513 .collect();
514 paused.sort_by(|a, b| a.host.cmp(&b.host));
515 paused
516 }
517
518 pub async fn host_concurrency(&self, host: &str) -> usize {
519 let key = normalize_host(host);
520 let hosts = self.hosts.lock().await;
521 hosts.get(&key).map_or(0, |state| state.concurrency)
522 }
523
524 pub async fn prune_settled_hosts(&self) {
526 let now = Instant::now();
527 let mut hosts = self.hosts.lock().await;
528 hosts.retain(|_, state| {
529 now.saturating_duration_since(state.last_used) < SETTLED_AFTER
530 || state.refusals > 0
531 || state.is_probing
532 || state.concurrency < state.max_concurrency
533 || state.open_until.is_some_and(|until| until > now)
534 || state.probe_until.is_some_and(|until| until > now)
535 });
536 }
537}
538
539fn normalize_host(host: &str) -> String {
540 host.trim().trim_end_matches('.').to_lowercase()
541}
542
543#[cfg(test)]
544mod tests {
545
546 #[test]
547 fn cautious_cannot_be_raised_by_a_flag_that_asks_for_more() {
548 let reckless = PacingLimits {
549 rate: Some(500),
550 per_registry: Some(64),
551 ..PacingLimits::cautious()
552 };
553 let limit = reckless.limit_for("rdap.example");
554
555 assert!(
556 limit.per_second_rate() <= CAUTIOUS_LIMIT.per_second_rate(),
557 "cautious means the rate can only go down, never up"
558 );
559 assert!(limit.concurrency <= CAUTIOUS_LIMIT.concurrency);
560 }
561
562 #[test]
563 fn cautious_still_lets_a_flag_ask_for_less() {
564 let slower = PacingLimits {
565 rate: Some(1),
566 per_registry: Some(1),
567 ..PacingLimits::cautious()
568 };
569 let limit = slower.limit_for("rdap.example");
570
571 assert_eq!(limit.queries, 1);
572 assert_eq!(limit.concurrency, 1);
573 }
574 use super::*;
575
576 fn throttled(seconds: u64) -> Refusal {
577 Refusal::Throttled {
578 retry_after: Some(Duration::from_secs(seconds)),
579 }
580 }
581
582 const UNSTATED: Refusal = Refusal::Throttled { retry_after: None };
583
584 fn fast() -> PacingLimits {
585 PacingLimits {
586 jitter: Duration::ZERO,
587 ..PacingLimits::default()
588 }
589 }
590
591 #[tokio::test(start_paused = true)]
592 async fn a_permit_is_granted_and_released() {
593 let pacer = Pacer::new(fast());
594 let permit = pacer
595 .acquire("rdap.example.test")
596 .await
597 .expect("first permit");
598 assert_eq!(permit.host(), "rdap.example.test");
599 assert!(!permit.is_probe());
600 drop(permit);
601 assert!(pacer.acquire("rdap.example.test").await.is_ok());
602 }
603
604 #[tokio::test(start_paused = true)]
605 async fn hosts_are_keyed_case_and_dot_insensitively() {
606 let pacer = Pacer::new(fast());
607 pacer
608 .record_refusal("RDAP.Example.Test.", &throttled(30))
609 .await;
610 pacer
611 .record_refusal("rdap.example.test", &throttled(30))
612 .await;
613 pacer
614 .record_refusal("rdap.example.test", &throttled(30))
615 .await;
616 assert!(pacer.paused("rdap.example.test").await.is_some());
617 }
618
619 #[tokio::test(start_paused = true)]
620 async fn a_published_registry_starts_with_its_published_allowance() {
621 let pacer = Pacer::new(fast());
622 let _permit = pacer
623 .acquire("rdap.identitydigital.services")
624 .await
625 .unwrap();
626 assert_eq!(
627 pacer
628 .host_concurrency("rdap.identitydigital.services")
629 .await,
630 4
631 );
632 }
633
634 #[tokio::test(start_paused = true)]
635 async fn gentle_pacing_ignores_a_generous_published_allowance() {
636 let pacer = Pacer::new(PacingLimits {
637 jitter: Duration::ZERO,
638 ..PacingLimits::cautious()
639 });
640 let _permit = pacer
641 .acquire("rdap.identitydigital.services")
642 .await
643 .unwrap();
644 assert_eq!(
645 pacer
646 .host_concurrency("rdap.identitydigital.services")
647 .await,
648 CAUTIOUS_LIMIT.concurrency
649 );
650 }
651
652 #[tokio::test(start_paused = true)]
653 async fn the_breaker_stays_shut_until_the_threshold_is_reached() {
654 let pacer = Pacer::new(PacingLimits {
655 refusal_threshold: 3,
656 ..fast()
657 });
658 pacer.record_refusal("slow.test", &UNSTATED).await;
659 assert_eq!(pacer.breaker("slow.test").await, BreakerState::Closed);
660 pacer.record_refusal("slow.test", &UNSTATED).await;
661 assert_eq!(pacer.breaker("slow.test").await, BreakerState::Closed);
662 pacer.record_refusal("slow.test", &UNSTATED).await;
663 assert_eq!(pacer.breaker("slow.test").await, BreakerState::Open);
664 }
665
666 #[tokio::test(start_paused = true)]
667 async fn an_outright_block_opens_the_breaker_on_the_first_refusal() {
668 let pacer = Pacer::new(PacingLimits {
669 refusal_threshold: 99,
670 ..fast()
671 });
672 pacer
673 .record_refusal("blocked.test", &Refusal::Blocked)
674 .await;
675 assert_eq!(pacer.breaker("blocked.test").await, BreakerState::Open);
676 }
677
678 #[tokio::test(start_paused = true)]
679 async fn a_dropped_connection_counts_as_backpressure() {
680 let pacer = Pacer::new(PacingLimits {
681 refusal_threshold: 1,
682 ..fast()
683 });
684 pacer.record_refusal("quiet.test", &Refusal::Dropped).await;
685 assert_eq!(pacer.breaker("quiet.test").await, BreakerState::Open);
686 }
687
688 #[tokio::test(start_paused = true)]
689 async fn an_open_breaker_refuses_a_permit_instead_of_blocking() {
690 let pacer = Pacer::new(PacingLimits {
691 refusal_threshold: 1,
692 ..fast()
693 });
694 pacer.record_refusal("busy.test", &throttled(5)).await;
695
696 match pacer.acquire("busy.test").await {
697 Err(paused) => {
698 assert_eq!(paused.host, "busy.test");
699 assert!(paused.remaining_wait <= Duration::from_secs(5));
700 }
701 Ok(_) => panic!("an open breaker must refuse the permit"),
702 }
703 }
704
705 #[tokio::test(start_paused = true)]
706 async fn the_cooldown_lets_exactly_one_probe_through() {
707 let pacer = Pacer::new(PacingLimits {
708 refusal_threshold: 1,
709 ..fast()
710 });
711 pacer.record_refusal("recovering.test", &throttled(2)).await;
712 tokio::time::advance(Duration::from_secs(3)).await;
713
714 assert_eq!(
715 pacer.breaker("recovering.test").await,
716 BreakerState::HalfOpen
717 );
718 let probe = pacer
719 .acquire("recovering.test")
720 .await
721 .expect("probe allowed");
722 assert!(probe.is_probe());
723
724 assert!(
725 pacer.acquire("recovering.test").await.is_err(),
726 "only one probe may be in flight"
727 );
728 }
729
730 #[tokio::test(start_paused = true)]
731 async fn a_successful_probe_closes_the_breaker() {
732 let pacer = Pacer::new(PacingLimits {
733 refusal_threshold: 1,
734 ..fast()
735 });
736 pacer.record_refusal("healing.test", &throttled(2)).await;
737 tokio::time::advance(Duration::from_secs(3)).await;
738 let probe = pacer.acquire("healing.test").await.expect("probe");
739 drop(probe);
740
741 pacer.record_success("healing.test").await;
742 assert_eq!(pacer.breaker("healing.test").await, BreakerState::Closed);
743 assert!(pacer.acquire("healing.test").await.is_ok());
744 }
745
746 #[tokio::test(start_paused = true)]
747 async fn a_failed_probe_reopens_the_breaker() {
748 let pacer = Pacer::new(PacingLimits {
749 refusal_threshold: 1,
750 ..fast()
751 });
752 pacer.record_refusal("stubborn.test", &throttled(2)).await;
753 tokio::time::advance(Duration::from_secs(3)).await;
754 let probe = pacer.acquire("stubborn.test").await.expect("probe");
755 drop(probe);
756
757 pacer.record_refusal("stubborn.test", &throttled(4)).await;
758 assert_eq!(pacer.breaker("stubborn.test").await, BreakerState::Open);
759 }
760
761 #[tokio::test(start_paused = true)]
762 async fn a_stated_wait_is_honored_exactly() {
763 let pacer = Pacer::new(fast());
764 let wait = pacer.record_refusal("polite.test", &throttled(7)).await;
765 assert_eq!(wait, Duration::from_secs(7));
766 }
767
768 #[tokio::test(start_paused = true)]
769 async fn an_absurd_stated_wait_is_capped() {
770 let pacer = Pacer::new(fast());
771 let wait = pacer
772 .record_refusal("hostile.test", &throttled(86_400))
773 .await;
774 assert_eq!(wait, MAX_RETRY_AFTER);
775 }
776
777 #[tokio::test(start_paused = true)]
778 async fn an_unstated_wait_stays_inside_the_growing_ceiling() {
779 let pacer = Pacer::new(PacingLimits {
780 refusal_threshold: 99,
781 ..fast()
782 });
783 for _ in 0..12 {
784 let wait = pacer.record_refusal("steep.test", &UNSTATED).await;
785 assert!(
786 wait <= pacer.pacing().max_backoff,
787 "{wait:?} exceeded the cap"
788 );
789 }
790 }
791
792 #[tokio::test(start_paused = true)]
793 async fn a_refusal_cuts_the_allowance_and_success_earns_it_back() {
794 let pacer = Pacer::new(PacingLimits {
795 refusal_threshold: 99,
796 recovery_threshold: 2,
797 ..fast()
798 });
799 let host = "rdap.identitydigital.services";
800 let permit = pacer.acquire(host).await.unwrap();
801 drop(permit);
802 assert_eq!(pacer.host_concurrency(host).await, 4);
803
804 pacer.record_refusal(host, &UNSTATED).await;
805 assert_eq!(pacer.host_concurrency(host).await, 3);
806
807 for _ in 0..2 {
808 pacer.record_success(host).await;
809 }
810 assert_eq!(pacer.host_concurrency(host).await, 4);
811 }
812
813 #[tokio::test(start_paused = true)]
814 async fn a_refusal_still_cuts_a_host_that_starts_at_two() {
815 let pacer = Pacer::new(PacingLimits {
817 refusal_threshold: 99,
818 ..fast()
819 });
820 let host = "unpublished.test";
821 let permit = pacer.acquire(host).await.unwrap();
822 drop(permit);
823 assert_eq!(pacer.host_concurrency(host).await, 2);
824
825 pacer.record_refusal(host, &UNSTATED).await;
826 assert_eq!(
827 pacer.host_concurrency(host).await,
828 1,
829 "backpressure must reach a host that starts at the cautious limit"
830 );
831 }
832
833 #[tokio::test(start_paused = true)]
834 async fn the_allowance_never_climbs_past_where_it_started() {
835 let pacer = Pacer::new(PacingLimits {
836 recovery_threshold: 1,
837 ..fast()
838 });
839 let host = "rdap.identitydigital.services";
840 let permit = pacer.acquire(host).await.unwrap();
841 drop(permit);
842
843 for _ in 0..50 {
844 pacer.record_success(host).await;
845 }
846 assert_eq!(pacer.host_concurrency(host).await, 4);
847 }
848
849 #[tokio::test(start_paused = true)]
850 async fn paused_hosts_lists_only_the_registries_actually_on_hold() {
851 let pacer = Pacer::new(PacingLimits {
852 refusal_threshold: 1,
853 ..fast()
854 });
855 pacer.record_refusal("one.test", &throttled(10)).await;
856 pacer.record_success("two.test").await;
857
858 let paused = pacer.paused_hosts().await;
859 assert_eq!(paused.len(), 1);
860 assert_eq!(
861 paused.first().map(|entry| entry.host.as_str()),
862 Some("one.test")
863 );
864 }
865
866 #[tokio::test(start_paused = true)]
867 async fn an_answer_sent_before_the_pause_does_not_lift_it() {
868 let pacer = Pacer::new(PacingLimits {
869 refusal_threshold: 1,
870 ..fast()
871 });
872 pacer.record_refusal("busy.test", &throttled(120)).await;
874 let paused = pacer
875 .paused("busy.test")
876 .await
877 .expect("the registry asked for a wait");
878 assert!(paused.remaining_wait >= Duration::from_secs(100));
879
880 pacer.record_success("busy.test").await;
882 let still = pacer
883 .paused("busy.test")
884 .await
885 .expect("the wait the registry asked for has to stand");
886 assert!(
887 still.remaining_wait >= Duration::from_secs(100),
888 "an answer already in flight must not discard the registry's own wait"
889 );
890
891 tokio::time::advance(Duration::from_secs(121)).await;
892 assert!(
893 pacer.paused("busy.test").await.is_none(),
894 "and once the wait is over the host is usable again"
895 );
896 }
897
898 #[tokio::test(start_paused = true)]
899 async fn a_dropped_connection_never_shortens_a_wait_the_registry_asked_for() {
900 let pacer = Pacer::new(PacingLimits {
901 refusal_threshold: 1,
902 ..fast()
903 });
904 pacer.record_refusal("busy.test", &throttled(300)).await;
905 let asked = pacer
906 .paused("busy.test")
907 .await
908 .expect("the registry asked for a wait")
909 .remaining_wait;
910
911 pacer.record_refusal("busy.test", &Refusal::Dropped).await;
913 let after = pacer
914 .paused("busy.test")
915 .await
916 .expect("the host is still paused")
917 .remaining_wait;
918 assert!(
919 after >= asked.saturating_sub(Duration::from_secs(1)),
920 "a shorter backoff must never replace a longer one: {asked:?} became {after:?}"
921 );
922 }
923
924 #[tokio::test(start_paused = true)]
925 async fn pruning_keeps_troubled_hosts_and_drops_settled_ones() {
926 let pacer = Pacer::new(PacingLimits {
927 refusal_threshold: 1,
928 ..fast()
929 });
930 let settled = pacer.acquire("calm.test").await.unwrap();
931 drop(settled);
932 pacer.record_success("calm.test").await;
933 pacer.record_refusal("angry.test", &throttled(600)).await;
935
936 pacer.prune_settled_hosts().await;
938 assert!(
939 pacer.host_concurrency("calm.test").await > 0,
940 "a host this sweep is still using must not be dropped"
941 );
942
943 tokio::time::advance(SETTLED_AFTER + Duration::from_secs(1)).await;
944 pacer.prune_settled_hosts().await;
945 assert_eq!(
946 pacer.host_concurrency("calm.test").await,
947 0,
948 "once it has gone quiet it is dropped"
949 );
950 assert!(pacer.paused("angry.test").await.is_some());
951 }
952
953 #[test]
954 fn full_jitter_draws_inside_the_window_and_actually_varies() {
955 let jitter = JitterRng::new();
956 let ceiling = Duration::from_secs(10);
957 let draws: Vec<Duration> = (0..64).map(|_| jitter.sample_up_to(ceiling)).collect();
958
959 assert!(draws.iter().all(|draw| *draw <= ceiling));
960 let unique = draws
961 .iter()
962 .collect::<std::collections::BTreeSet<_>>()
963 .len();
964 assert!(
965 unique > 32,
966 "jitter is not spreading: {unique} distinct draws"
967 );
968 }
969
970 #[test]
971 fn a_zero_window_yields_no_wait_rather_than_panicking() {
972 let jitter = JitterRng::new();
973 assert_eq!(jitter.sample_up_to(Duration::ZERO), Duration::ZERO);
974 }
975
976 #[test]
977 fn a_quota_survives_an_extremely_slow_published_limit() {
978 let quota = quota_for(HostLimit::per_minute(1, 1));
979 assert!(quota.burst_size().get() >= 1);
980 }
981
982 #[tokio::test]
983 async fn arming_a_probe_does_not_overwrite_the_wait_a_registry_asked_for() {
984 let pacer = Pacer::new(PacingLimits::default());
985 let host = "registry.test";
986
987 pacer.record_refusal(host, &Refusal::Blocked).await;
989
990 let paused = pacer.paused_hosts().await;
991 let waiting = paused.iter().find(|entry| entry.host == host);
992 assert!(
993 waiting.is_some_and(|entry| entry.remaining_wait > Duration::ZERO),
994 "a refusal has to leave a wait a caller can report"
995 );
996 }
997
998 #[tokio::test]
999 async fn a_settled_host_is_forgotten_while_one_still_waiting_is_kept() {
1000 let pacer = Pacer::new(PacingLimits::default());
1001 pacer.record_success("quiet.test").await;
1002 pacer.record_refusal("cross.test", &Refusal::Blocked).await;
1003
1004 pacer.prune_settled_hosts().await;
1005
1006 let still_paused = pacer.paused_hosts().await;
1007 assert!(
1008 still_paused.iter().any(|entry| entry.host == "cross.test"),
1009 "a host still serving a wait must survive the prune"
1010 );
1011 }
1012}