1use crate::error::FaucetError;
46use crate::observability::RoundtripRecorder;
47use crate::resilience::RetryPolicy;
48use std::fmt;
49use std::future::Future;
50use std::sync::{Arc, OnceLock};
51use std::time::Duration;
52use tokio::time::Instant;
53use tokio_util::sync::CancellationToken;
54
55pub const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(300);
59
60pub type CallRedactor = Arc<dyn Fn(&str) -> String + Send + Sync>;
63
64#[derive(Clone, Default)]
65struct Inner {
66 timeout: Option<Option<Duration>>,
68 deadline: Option<Instant>,
69 retry: Option<RetryPolicy>,
70 rate_limit_retries: Option<u32>,
71 cancel: Option<CancellationToken>,
72 recorder: Option<Arc<RoundtripRecorder>>,
73 metered: Option<bool>,
74 redactor: Option<CallRedactor>,
75}
76
77#[derive(Clone, Default)]
83pub struct CallContext {
84 inner: Arc<Inner>,
85}
86
87impl std::panic::UnwindSafe for CallContext {}
89impl std::panic::RefUnwindSafe for CallContext {}
90
91impl fmt::Debug for CallContext {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("CallContext")
94 .field("timeout", &self.timeout())
95 .field("deadline", &self.inner.deadline)
96 .field("retry", &self.inner.retry)
97 .field("rate_limit_retries", &self.inner.rate_limit_retries)
98 .field("cancel", &self.inner.cancel.is_some())
99 .field("recorder", &self.inner.recorder.is_some())
100 .field("redactor", &self.inner.redactor.is_some())
101 .finish()
102 }
103}
104
105impl CallContext {
106 pub fn new() -> Self {
109 Self::default()
110 }
111
112 fn edit(mut self, f: impl FnOnce(&mut Inner)) -> Self {
113 f(Arc::make_mut(&mut self.inner));
114 self
115 }
116
117 pub fn with_timeout(self, timeout: Duration) -> Self {
120 self.edit(|i| i.timeout = Some(Some(timeout)))
121 }
122
123 pub fn without_timeout(self) -> Self {
127 self.edit(|i| i.timeout = Some(None))
128 }
129
130 pub fn with_deadline(self, deadline: Instant) -> Self {
133 self.edit(|i| i.deadline = Some(deadline))
134 }
135
136 pub fn with_retry(self, policy: RetryPolicy) -> Self {
140 self.edit(|i| i.retry = Some(policy))
141 }
142
143 pub fn without_retry(self) -> Self {
145 self.with_retry(RetryPolicy {
146 max_attempts: 1,
147 ..RetryPolicy::default()
148 })
149 }
150
151 pub fn with_rate_limit_retries(self, n: u32) -> Self {
157 self.edit(|i| i.rate_limit_retries = Some(n))
158 }
159
160 pub fn with_cancel(self, cancel: CancellationToken) -> Self {
163 self.edit(|i| i.cancel = Some(cancel))
164 }
165
166 pub fn with_recorder(self, recorder: Arc<RoundtripRecorder>) -> Self {
169 self.edit(|i| i.recorder = Some(recorder))
170 }
171
172 pub fn with_roundtrip_metering(self, on: bool) -> Self {
176 self.edit(|i| i.metered = Some(on))
177 }
178
179 pub fn with_redactor(self, redactor: CallRedactor) -> Self {
182 self.edit(|i| i.redactor = Some(redactor))
183 }
184
185 pub fn inherit(&self, base: &CallContext) -> CallContext {
187 let (mine, b) = (&*self.inner, &*base.inner);
188 CallContext {
189 inner: Arc::new(Inner {
190 timeout: mine.timeout.or(b.timeout),
191 deadline: match (mine.deadline, b.deadline) {
192 (Some(x), Some(y)) => Some(x.min(y)),
193 (x, y) => x.or(y),
194 },
195 retry: mine.retry.clone().or_else(|| b.retry.clone()),
196 rate_limit_retries: mine.rate_limit_retries.or(b.rate_limit_retries),
197 cancel: mine.cancel.clone().or_else(|| b.cancel.clone()),
198 recorder: mine.recorder.clone().or_else(|| b.recorder.clone()),
199 metered: mine.metered.or(b.metered),
200 redactor: mine.redactor.clone().or_else(|| b.redactor.clone()),
201 }),
202 }
203 }
204
205 pub fn timeout(&self) -> Option<Duration> {
207 self.inner.timeout.unwrap_or(Some(DEFAULT_CALL_TIMEOUT))
208 }
209
210 pub fn deadline(&self) -> Option<Instant> {
212 self.inner.deadline
213 }
214
215 pub fn retry(&self) -> Option<&RetryPolicy> {
217 self.inner.retry.as_ref()
218 }
219
220 pub fn rate_limit_retries(&self) -> Option<u32> {
223 self.inner.rate_limit_retries
224 }
225
226 pub fn cancel(&self) -> Option<&CancellationToken> {
228 self.inner.cancel.as_ref()
229 }
230
231 pub fn is_cancelled(&self) -> bool {
233 self.inner.cancel.as_ref().is_some_and(|c| c.is_cancelled())
234 }
235
236 pub fn recorder(&self) -> Option<&Arc<RoundtripRecorder>> {
238 self.inner.recorder.as_ref()
239 }
240
241 pub fn record(&self, op: &'static str) {
243 if let Some(r) = &self.inner.recorder {
244 r.record(op);
245 }
246 }
247
248 pub fn signal(&self, kind: &'static str, unit: &'static str, quantity: f64) {
251 if let Some(r) = &self.inner.recorder {
252 r.signal(kind, unit, quantity);
253 }
254 }
255
256 pub fn redact(&self, text: &str) -> String {
258 match &self.inner.redactor {
259 Some(f) => f(text),
260 None => crate::redact::redact(text),
261 }
262 }
263
264 pub fn redact_error(&self, err: FaucetError) -> FaucetError {
268 match err {
269 FaucetError::HttpStatus { status, url, body } => FaucetError::HttpStatus {
270 status,
271 url: self.redact(&url),
272 body: self.redact(&body),
273 },
274 FaucetError::Http(e) => FaucetError::Http(redact_reqwest_url(e, |u| self.redact(u))),
275 other => other,
276 }
277 }
278
279 #[allow(clippy::disallowed_methods)] pub async fn wait(&self, duration: Duration) -> Result<(), FaucetError> {
285 if self.is_cancelled() {
286 return Err(cancelled_error("wait"));
287 }
288 let cancelled = async {
289 match &self.inner.cancel {
290 Some(c) => c.cancelled().await,
291 None => std::future::pending().await,
292 }
293 };
294 let end = Instant::now() + duration;
295 match self.inner.deadline {
296 Some(deadline) if deadline < end => {
297 tokio::select! {
298 biased;
299 _ = cancelled => Err(cancelled_error("wait")),
300 _ = tokio::time::sleep_until(deadline) => Err(deadline_error("wait")),
301 }
302 }
303 _ => {
304 tokio::select! {
305 biased;
306 _ = cancelled => Err(cancelled_error("wait")),
307 _ = tokio::time::sleep_until(end) => Ok(()),
308 }
309 }
310 }
311 }
312
313 fn attempt_bound(&self, op: &'static str) -> Result<Option<Duration>, FaucetError> {
316 let timeout = self.timeout();
317 match self.inner.deadline {
318 None => Ok(timeout),
319 Some(deadline) => {
320 let left = deadline.saturating_duration_since(Instant::now());
321 if left.is_zero() {
322 return Err(deadline_error(op));
323 }
324 Ok(Some(timeout.map_or(left, |t| t.min(left))))
325 }
326 }
327 }
328
329 #[allow(clippy::disallowed_methods)] pub async fn call<T, F, Fut>(&self, op: &'static str, mut f: F) -> Result<T, FaucetError>
345 where
346 F: FnMut() -> Fut,
347 Fut: Future<Output = Result<T, FaucetError>>,
348 {
349 if self.is_cancelled() {
350 return Err(cancelled_error(op));
351 }
352 let single = RetryPolicy {
353 max_attempts: 1,
354 ..RetryPolicy::default()
355 };
356 let policy = self.inner.retry.as_ref().unwrap_or(&single);
357 let recorder = self.inner.recorder.as_ref();
358 let metered = self.inner.metered.unwrap_or(true);
359 let attempt = || {
360 let bound = self.attempt_bound(op);
361 let fut = f();
362 async move {
363 let bound = bound?;
364 let started = std::time::Instant::now();
365 let outcome = match bound {
366 Some(t) => match tokio::time::timeout(t, fut).await {
367 Ok(r) => r,
368 Err(_) => Err(FaucetError::Timeout {
369 operation: format!("{op} call"),
370 after: t,
371 }),
372 },
373 None => fut.await,
374 };
375 if metered && let Some(r) = recorder {
376 r.record_timed(op, started.elapsed());
377 }
378 outcome
379 }
380 };
381 let run = crate::resilience::run_call(
382 policy,
383 self.inner.cancel.as_ref(),
384 recorder,
385 self.inner.rate_limit_retries,
386 attempt,
387 );
388 let cancelled = async {
389 match &self.inner.cancel {
390 Some(c) => c.cancelled().await,
391 None => std::future::pending().await,
392 }
393 };
394 let deadline = async {
395 match self.inner.deadline {
396 Some(d) => tokio::time::sleep_until(d).await,
397 None => std::future::pending().await,
398 }
399 };
400 let outcome = tokio::select! {
401 biased;
402 _ = cancelled => Err(cancelled_error(op)),
403 _ = deadline => Err(deadline_error(op)),
404 r = run => r,
405 };
406 outcome.map_err(|e| self.redact_error(e))
407 }
408
409 pub async fn call_once<T, Fut>(&self, op: &'static str, fut: Fut) -> Result<T, FaucetError>
415 where
416 Fut: Future<Output = Result<T, FaucetError>>,
417 {
418 let reset;
419 let single = match &self.inner.retry {
420 Some(p) if p.max_attempts > 1 => {
421 reset = self.clone().without_retry();
422 &reset
423 }
424 _ => self,
425 };
426 let mut fut = Some(fut);
427 single
428 .call(op, || {
429 let f = fut.take();
430 async move { f.expect("call_once makes one attempt").await }
431 })
432 .await
433 }
434}
435
436fn cancelled_error(op: &'static str) -> FaucetError {
437 FaucetError::Cancelled {
438 operation: format!("{op} call"),
439 }
440}
441
442fn deadline_error(op: &'static str) -> FaucetError {
443 FaucetError::Timeout {
444 operation: format!("{op} call (deadline)"),
445 after: Duration::ZERO,
446 }
447}
448
449pub(crate) fn redact_reqwest_url(
452 e: reqwest::Error,
453 redact: impl Fn(&str) -> String,
454) -> reqwest::Error {
455 let Some(url) = e.url() else {
456 return e;
457 };
458 let scrubbed = crate::util::redact_uri_credentials(&redact(url.as_str()));
459 if scrubbed == url.as_str() {
460 return e;
461 }
462 match reqwest::Url::parse(&scrubbed) {
463 Ok(u) => e.with_url(u),
464 Err(_) => e.without_url(),
465 }
466}
467
468#[derive(Debug, Default)]
478pub struct CallSlot {
479 local: CallContext,
480 installed: OnceLock<CallContext>,
481 recorder: OnceLock<Arc<RoundtripRecorder>>,
482}
483
484impl std::panic::UnwindSafe for CallSlot {}
486impl std::panic::RefUnwindSafe for CallSlot {}
487
488impl CallSlot {
489 pub fn new(local: CallContext) -> Self {
491 Self {
492 local,
493 installed: OnceLock::new(),
494 recorder: OnceLock::new(),
495 }
496 }
497
498 pub fn local(&self) -> &CallContext {
500 &self.local
501 }
502
503 pub fn set_local(&mut self, local: CallContext) {
505 self.local = local;
506 }
507
508 pub fn install(&self, ctx: CallContext) {
510 let _ = self.installed.set(ctx);
511 }
512
513 pub fn install_recorder(&self, recorder: Arc<RoundtripRecorder>) {
517 let _ = self.recorder.set(recorder);
518 }
519
520 pub fn get(&self) -> CallContext {
523 let mut ctx = match self.installed.get() {
524 Some(base) => self.local.inherit(base),
525 None => self.local.clone(),
526 };
527 if ctx.inner.recorder.is_none()
528 && let Some(r) = self.recorder.get()
529 {
530 ctx = ctx.with_recorder(Arc::clone(r));
531 }
532 ctx
533 }
534
535 pub fn recorder(&self) -> Option<Arc<RoundtripRecorder>> {
537 self.get().inner.recorder.clone()
538 }
539
540 pub fn record(&self, op: &'static str) {
542 self.get().record(op);
543 }
544
545 pub async fn wait(&self, duration: Duration) -> Result<(), FaucetError> {
547 self.get().wait(duration).await
548 }
549
550 pub async fn call<T, F, Fut>(&self, op: &'static str, f: F) -> Result<T, FaucetError>
552 where
553 F: FnMut() -> Fut,
554 Fut: Future<Output = Result<T, FaucetError>>,
555 {
556 self.get().call(op, f).await
557 }
558
559 pub async fn call_once<T, Fut>(&self, op: &'static str, fut: Fut) -> Result<T, FaucetError>
562 where
563 Fut: Future<Output = Result<T, FaucetError>>,
564 {
565 self.get().call_once(op, fut).await
566 }
567}
568
569pub trait CallExt: Future + Send + Sized {
575 fn within<'a>(
577 self,
578 slot: &'a CallSlot,
579 op: &'static str,
580 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Self::Output, FaucetError>> + Send + 'a>>
581 where
582 Self: 'a,
583 Self::Output: Send,
584 {
585 Box::pin(slot.call_once(op, async move { Ok(self.await) }))
586 }
587
588 fn within_ctx<'a>(
590 self,
591 ctx: &'a CallContext,
592 op: &'static str,
593 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Self::Output, FaucetError>> + Send + 'a>>
594 where
595 Self: 'a,
596 Self::Output: Send,
597 {
598 Box::pin(ctx.call_once(op, async move { Ok(self.await) }))
599 }
600}
601
602impl<F: Future + Send> CallExt for F {}
603
604pub struct CallReader<R> {
609 inner: R,
610 cancel: Option<std::pin::Pin<Box<tokio_util::sync::WaitForCancellationFutureOwned>>>,
611 deadline: Option<std::pin::Pin<Box<tokio::time::Sleep>>>,
612}
613
614impl<R> fmt::Debug for CallReader<R> {
615 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616 f.debug_struct("CallReader")
617 .field("cancel", &self.cancel.is_some())
618 .field("deadline", &self.deadline.is_some())
619 .finish_non_exhaustive()
620 }
621}
622
623impl CallContext {
624 #[allow(clippy::disallowed_methods)] pub fn reader<R>(&self, inner: R) -> CallReader<R> {
627 CallReader {
628 inner,
629 cancel: self
630 .inner
631 .cancel
632 .clone()
633 .map(|c| Box::pin(c.cancelled_owned())),
634 deadline: self
635 .inner
636 .deadline
637 .map(|d| Box::pin(tokio::time::sleep_until(d))),
638 }
639 }
640}
641
642impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for CallReader<R> {
643 fn poll_read(
644 mut self: std::pin::Pin<&mut Self>,
645 cx: &mut std::task::Context<'_>,
646 buf: &mut tokio::io::ReadBuf<'_>,
647 ) -> std::task::Poll<std::io::Result<()>> {
648 use std::task::Poll;
649 if let Some(c) = self.cancel.as_mut()
650 && c.as_mut().poll(cx).is_ready()
651 {
652 return Poll::Ready(Err(std::io::Error::new(
653 std::io::ErrorKind::Interrupted,
654 cancelled_error("read"),
655 )));
656 }
657 if let Some(d) = self.deadline.as_mut()
658 && d.as_mut().poll(cx).is_ready()
659 {
660 return Poll::Ready(Err(std::io::Error::new(
661 std::io::ErrorKind::TimedOut,
662 deadline_error("read"),
663 )));
664 }
665 std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
666 }
667}
668
669#[cfg(test)]
670#[allow(clippy::disallowed_methods)] mod tests {
672 use super::*;
673 use crate::observability::RoundtripSide;
674 use crate::resilience::BackoffKind;
675 use std::sync::atomic::{AtomicU32, Ordering};
676
677 fn fast(max_attempts: u32) -> RetryPolicy {
678 RetryPolicy {
679 max_attempts,
680 backoff: BackoffKind::None,
681 base: Duration::ZERO,
682 max: Duration::ZERO,
683 jitter: false,
684 ..RetryPolicy::default()
685 }
686 }
687
688 fn status(code: u16) -> FaucetError {
689 FaucetError::HttpStatus {
690 status: code,
691 url: "http://h/x".into(),
692 body: String::new(),
693 }
694 }
695
696 #[test]
697 fn call_types_are_unwind_safe() {
698 fn assert_unwind_safe<T: std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
699 assert_unwind_safe::<CallContext>();
700 assert_unwind_safe::<CallSlot>();
701 }
702
703 #[tokio::test]
704 async fn call_once_makes_one_metered_attempt_whatever_the_policy() {
705 let meter = Arc::new(crate::usage::UsageMeter::new());
706 let rec = Arc::new(
707 RoundtripRecorder::new(RoundtripSide::Source, "p", "r", "c").with_meter(meter.clone()),
708 );
709 let slot = CallSlot::new(CallContext::new().with_retry(fast(5)));
710 slot.install_recorder(rec);
711 let err = slot
712 .call_once("query", async { Err::<(), _>(status(503)) })
713 .await
714 .unwrap_err();
715 assert!(matches!(err, FaucetError::HttpStatus { status: 503, .. }));
716 assert_eq!(slot.call_once("query", async { Ok(7) }).await.unwrap(), 7);
717 assert_eq!(meter.snapshot().source_roundtrips["query"], 2);
718 }
719
720 #[tokio::test]
722 async fn a_spent_deadline_refuses_the_call_without_running_it() {
723 let ctx = CallContext::new().with_deadline(Instant::now() - Duration::from_millis(1));
724 let ran = AtomicU32::new(0);
725 let r: Result<(), _> = ctx
726 .call("read", || async {
727 ran.fetch_add(1, Ordering::SeqCst);
728 Ok(())
729 })
730 .await;
731 match r {
732 Err(FaucetError::Timeout { operation, after }) => {
733 assert_eq!(operation, "read call (deadline)");
734 assert_eq!(after, Duration::ZERO);
735 }
736 other => panic!("expected a deadline timeout, got {other:?}"),
737 }
738 assert_eq!(ran.load(Ordering::SeqCst), 0);
739 }
740
741 #[tokio::test]
742 async fn call_once_ends_at_the_deadline_and_on_cancel() {
743 let ctx = CallContext::new().with_deadline(Instant::now() + Duration::from_millis(20));
744 let err = ctx
745 .call_once("query", std::future::pending::<Result<(), FaucetError>>())
746 .await
747 .unwrap_err();
748 assert!(matches!(err, FaucetError::Timeout { .. }));
749 let token = CancellationToken::new();
750 token.cancel();
751 let err = CallContext::new()
752 .with_cancel(token)
753 .call_once("query", async { Ok(()) })
754 .await
755 .unwrap_err();
756 assert!(matches!(err, FaucetError::Cancelled { .. }));
757 }
758
759 #[tokio::test]
760 async fn defaults_make_one_attempt_with_the_default_timeout() {
761 let ctx = CallContext::new();
762 assert_eq!(ctx.timeout(), Some(DEFAULT_CALL_TIMEOUT));
763 assert!(ctx.retry().is_none());
764 assert!(ctx.deadline().is_none());
765 let calls = AtomicU32::new(0);
766 let r: Result<(), _> = ctx
767 .call("op", || {
768 calls.fetch_add(1, Ordering::SeqCst);
769 async { Err(status(503)) }
770 })
771 .await;
772 assert!(matches!(
773 r,
774 Err(FaucetError::HttpStatus { status: 503, .. })
775 ));
776 assert_eq!(calls.load(Ordering::SeqCst), 1);
777 }
778
779 #[tokio::test(start_paused = true)]
780 async fn call_once_bounds_a_one_shot_future_and_never_retries() {
781 let ctx = CallContext::new()
782 .with_timeout(Duration::from_secs(1))
783 .with_retry(fast(3));
784 let polls = AtomicU32::new(0);
785 let r: Result<(), _> = ctx
786 .call_once("read", async {
787 polls.fetch_add(1, Ordering::SeqCst);
788 std::future::pending().await
789 })
790 .await;
791 assert!(matches!(r, Err(FaucetError::Timeout { .. })), "{r:?}");
792 assert_eq!(polls.load(Ordering::SeqCst), 1, "never retried");
793
794 let r = ctx
795 .call_once("read", async { Err::<(), _>(status(503)) })
796 .await;
797 assert!(matches!(
798 r,
799 Err(FaucetError::HttpStatus { status: 503, .. })
800 ));
801
802 let slot = CallSlot::new(CallContext::new());
803 let cancel = CancellationToken::new();
804 slot.install(CallContext::new().with_cancel(cancel.clone()));
805 assert_eq!(slot.call_once("read", async { Ok(5) }).await.unwrap(), 5);
806 cancel.cancel();
807 let r = slot.call_once("read", async { Ok(5) }).await;
808 assert!(matches!(r, Err(FaucetError::Cancelled { .. })), "{r:?}");
809 }
810
811 #[tokio::test(start_paused = true)]
812 async fn within_hands_back_the_output_or_the_spent_context() {
813 let slot = CallSlot::new(CallContext::new().with_timeout(Duration::from_secs(1)));
814 let out: Result<u8, &str> = async { Err("sdk") }.within(&slot, "put").await.unwrap();
815 assert_eq!(out, Err("sdk"));
816 let hung = std::future::pending::<u8>().within(&slot, "put").await;
817 assert!(matches!(hung, Err(FaucetError::Timeout { .. })), "{hung:?}");
818 let ctx = CallContext::new();
819 assert_eq!(async { 3 }.within_ctx(&ctx, "get").await.unwrap(), 3);
820 }
821
822 #[tokio::test(start_paused = true)]
823 async fn a_reader_fails_at_the_deadline_and_on_cancel() {
824 use tokio::io::AsyncReadExt;
825 let mut buf = Vec::new();
826 let mut plain = CallContext::new().reader(&b"abc"[..]);
827 plain.read_to_end(&mut buf).await.unwrap();
828 assert_eq!(buf, b"abc");
829 assert!(format!("{plain:?}").contains("CallReader"));
830
831 let (_tx, rx) = tokio::io::duplex(8);
832 let ctx = CallContext::new().with_deadline(Instant::now() + Duration::from_secs(1));
833 let mut stalled = ctx.reader(rx);
834 let err = stalled.read_to_end(&mut buf).await.unwrap_err();
835 assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
836
837 let (_tx, rx) = tokio::io::duplex(8);
838 let cancel = CancellationToken::new();
839 let mut stalled = CallContext::new().with_cancel(cancel.clone()).reader(rx);
840 cancel.cancel();
841 let err = stalled.read_to_end(&mut buf).await.unwrap_err();
842 assert_eq!(err.kind(), std::io::ErrorKind::Interrupted);
843 assert!(err.to_string().contains("cancelled"), "{err}");
844 }
845
846 #[tokio::test]
847 async fn retries_classified_errors_under_the_policy() {
848 let ctx = CallContext::new().with_retry(fast(3));
849 let calls = AtomicU32::new(0);
850 let r = ctx
851 .call("op", || {
852 let n = calls.fetch_add(1, Ordering::SeqCst);
853 async move { if n < 2 { Err(status(502)) } else { Ok(n) } }
854 })
855 .await;
856 assert_eq!(r.unwrap(), 2);
857 let calls = AtomicU32::new(0);
858 let r: Result<(), _> = ctx
859 .call("op", || {
860 calls.fetch_add(1, Ordering::SeqCst);
861 async { Err(status(404)) }
862 })
863 .await;
864 assert!(r.is_err());
865 assert_eq!(calls.load(Ordering::SeqCst), 1, "a 404 is not retried");
866 }
867
868 #[tokio::test(start_paused = true)]
869 async fn a_hung_attempt_times_out_and_is_retried() {
870 let ctx = CallContext::new()
871 .with_timeout(Duration::from_secs(1))
872 .with_retry(fast(2));
873 let calls = AtomicU32::new(0);
874 let r: Result<(), _> = ctx
875 .call("query", || {
876 calls.fetch_add(1, Ordering::SeqCst);
877 std::future::pending()
878 })
879 .await;
880 match r {
881 Err(FaucetError::Timeout { operation, after }) => {
882 assert_eq!(operation, "query call");
883 assert_eq!(after, Duration::from_secs(1));
884 }
885 other => panic!("expected a timeout, got {other:?}"),
886 }
887 assert_eq!(calls.load(Ordering::SeqCst), 2);
888 }
889
890 #[tokio::test(start_paused = true)]
891 async fn the_deadline_ends_the_whole_call() {
892 let start = Instant::now();
893 let ctx = CallContext::new()
894 .without_timeout()
895 .with_deadline(start + Duration::from_secs(5))
896 .with_retry(RetryPolicy {
897 max_attempts: 100,
898 backoff: BackoffKind::Fixed,
899 base: Duration::from_secs(2),
900 max: Duration::from_secs(2),
901 jitter: false,
902 ..RetryPolicy::default()
903 });
904 let r: Result<(), _> = ctx.call("op", || async { Err(status(503)) }).await;
905 assert!(matches!(r, Err(FaucetError::Timeout { .. })), "{r:?}");
906 assert!(Instant::now() - start <= Duration::from_secs(5));
907 let r: Result<(), _> = ctx.call("op", || async { Ok(()) }).await;
908 assert!(
909 matches!(r, Err(FaucetError::Timeout { .. })),
910 "past deadline"
911 );
912 }
913
914 #[tokio::test(start_paused = true)]
915 async fn the_deadline_cuts_an_attempt_short() {
916 let ctx = CallContext::new()
917 .with_timeout(Duration::from_secs(60))
918 .with_deadline(Instant::now() + Duration::from_secs(3));
919 let r: Result<(), _> = ctx.call("op", std::future::pending).await;
920 match r {
921 Err(FaucetError::Timeout { after, .. }) => assert!(after <= Duration::from_secs(3)),
922 other => panic!("{other:?}"),
923 }
924 }
925
926 #[tokio::test(start_paused = true)]
927 async fn cancellation_stops_an_attempt_and_fails_fast_afterwards() {
928 let token = CancellationToken::new();
929 let ctx = CallContext::new().with_cancel(token.clone());
930 let t = token.clone();
931 tokio::spawn(async move {
932 tokio::time::sleep(Duration::from_secs(1)).await;
933 t.cancel();
934 });
935 let r: Result<(), _> = ctx.call("op", std::future::pending).await;
936 assert!(matches!(r, Err(FaucetError::Cancelled { .. })), "{r:?}");
937 assert!(ctx.is_cancelled());
938 let calls = AtomicU32::new(0);
939 let r: Result<(), _> = ctx
940 .call("op", || {
941 calls.fetch_add(1, Ordering::SeqCst);
942 async { Ok(()) }
943 })
944 .await;
945 assert!(matches!(r, Err(FaucetError::Cancelled { .. })));
946 assert_eq!(calls.load(Ordering::SeqCst), 0);
947 }
948
949 #[tokio::test(start_paused = true)]
950 async fn wait_honours_cancel_and_deadline() {
951 let start = Instant::now();
952 CallContext::new()
953 .wait(Duration::from_secs(4))
954 .await
955 .unwrap();
956 assert!(Instant::now() - start >= Duration::from_secs(4));
957 let ctx = CallContext::new().with_deadline(Instant::now() + Duration::from_secs(1));
958 assert!(matches!(
959 ctx.wait(Duration::from_secs(10)).await,
960 Err(FaucetError::Timeout { .. })
961 ));
962 let token = CancellationToken::new();
963 let ctx = CallContext::new().with_cancel(token.clone());
964 let t = token.clone();
965 tokio::spawn(async move {
966 tokio::time::sleep(Duration::from_secs(1)).await;
967 t.cancel();
968 });
969 assert!(matches!(
970 ctx.wait(Duration::from_secs(10)).await,
971 Err(FaucetError::Cancelled { .. })
972 ));
973 assert!(matches!(
974 ctx.wait(Duration::ZERO).await,
975 Err(FaucetError::Cancelled { .. })
976 ));
977 let slot = CallSlot::default();
978 slot.wait(Duration::from_millis(1)).await.unwrap();
979 }
980
981 #[tokio::test(start_paused = true)]
982 async fn a_rate_limit_waits_its_stated_time() {
983 let ctx = CallContext::new().with_retry(fast(2));
984 let calls = AtomicU32::new(0);
985 let start = Instant::now();
986 let r = ctx
987 .call("op", || {
988 let n = calls.fetch_add(1, Ordering::SeqCst);
989 async move {
990 if n == 0 {
991 Err(FaucetError::RateLimited(Duration::from_secs(7)))
992 } else {
993 Ok(n)
994 }
995 }
996 })
997 .await;
998 assert_eq!(r.unwrap(), 1);
999 assert!(Instant::now() - start >= Duration::from_secs(7));
1000 }
1001
1002 #[tokio::test(start_paused = true)]
1003 async fn the_rate_limit_budget_does_not_use_attempts() {
1004 let ctx = CallContext::new()
1005 .with_retry(fast(1))
1006 .with_rate_limit_retries(3);
1007 let calls = AtomicU32::new(0);
1008 let r = ctx
1009 .call("op", || {
1010 let n = calls.fetch_add(1, Ordering::SeqCst);
1011 async move {
1012 if n < 3 {
1013 Err(FaucetError::RateLimited(Duration::from_millis(10)))
1014 } else {
1015 Ok(n)
1016 }
1017 }
1018 })
1019 .await;
1020 assert_eq!(r.unwrap(), 3, "three rate limits retried with one attempt");
1021 let calls = AtomicU32::new(0);
1022 let r: Result<(), _> = ctx
1023 .call("op", || {
1024 calls.fetch_add(1, Ordering::SeqCst);
1025 async { Err(FaucetError::RateLimited(Duration::from_millis(1))) }
1026 })
1027 .await;
1028 assert!(matches!(r, Err(FaucetError::RateLimited(_))));
1029 assert_eq!(calls.load(Ordering::SeqCst), 4, "1 + 3 consecutive retries");
1030 }
1031
1032 #[tokio::test]
1033 async fn every_attempt_is_metered() {
1034 let meter = Arc::new(crate::usage::UsageMeter::new());
1035 let rec = Arc::new(
1036 RoundtripRecorder::new(RoundtripSide::Source, "p", "r", "c").with_meter(meter.clone()),
1037 );
1038 let ctx = CallContext::new()
1039 .with_recorder(rec.clone())
1040 .with_retry(fast(3));
1041 let calls = AtomicU32::new(0);
1042 let _ = ctx
1043 .call("get", || {
1044 let n = calls.fetch_add(1, Ordering::SeqCst);
1045 async move {
1046 if n == 0 {
1047 Err(FaucetError::RateLimited(Duration::ZERO))
1048 } else {
1049 Ok(())
1050 }
1051 }
1052 })
1053 .await;
1054 ctx.record("list");
1055 ctx.signal("bytes_billed", "bytes", 5.0);
1056 let unmetered = ctx.clone().with_roundtrip_metering(false);
1057 unmetered.call("get", || async { Ok(()) }).await.unwrap();
1058 let snap = meter.snapshot();
1059 assert_eq!(snap.source_roundtrips["get"], 2);
1060 assert_eq!(snap.source_roundtrips["list"], 1);
1061 assert_eq!(snap.throttled, 1);
1062 assert_eq!(snap.signals.len(), 1);
1063 }
1064
1065 #[tokio::test]
1066 async fn the_final_error_is_redacted() {
1067 let ctx = CallContext::new().with_redactor(Arc::new(|s: &str| s.replace("SECRET", "***")));
1068 let r: Result<(), _> = ctx
1069 .call("op", || async {
1070 Err(FaucetError::HttpStatus {
1071 status: 400,
1072 url: "http://h/?k=SECRET".into(),
1073 body: "bad SECRET".into(),
1074 })
1075 })
1076 .await;
1077 match r {
1078 Err(FaucetError::HttpStatus { url, body, .. }) => {
1079 assert_eq!(url, "http://h/?k=***");
1080 assert_eq!(body, "bad ***");
1081 }
1082 other => panic!("{other:?}"),
1083 }
1084 assert!(matches!(
1085 ctx.redact_error(FaucetError::Config("SECRET".into())),
1086 FaucetError::Config(s) if s == "SECRET"
1087 ));
1088 assert_eq!(CallContext::new().redact("plain"), "plain");
1089 }
1090
1091 async fn refused(url: &str) -> FaucetError {
1092 let client = crate::http::Client::new().unwrap();
1093 client.send(client.get(url)).await.unwrap_err()
1094 }
1095
1096 #[tokio::test]
1097 async fn transport_error_urls_lose_credentials() {
1098 let e = refused("http://user:pw@127.0.0.1:1/x?token=SECRET").await;
1099 let text = CallContext::new().redact_error(e).to_string();
1100 assert!(!text.contains("pw@"), "{text}");
1101 assert!(!text.contains("SECRET"), "{text}");
1102 let FaucetError::Http(e) = refused("http://127.0.0.1:1/x").await else {
1103 panic!("expected a transport error");
1104 };
1105 assert!(e.url().is_some());
1106 let kept = redact_reqwest_url(e, |u| u.to_string());
1107 assert!(kept.url().is_some(), "an unchanged URL is kept");
1108 let dropped = redact_reqwest_url(kept, |_| "not a url".into());
1109 assert!(dropped.url().is_none());
1110 assert!(
1111 redact_reqwest_url(dropped, |_| String::new())
1112 .url()
1113 .is_none()
1114 );
1115 }
1116
1117 #[test]
1118 fn inherit_layers_connector_settings_over_the_installed_context() {
1119 let token = CancellationToken::new();
1120 let now = Instant::now();
1121 let rec = Arc::new(RoundtripRecorder::new(RoundtripSide::Sink, "p", "r", "c"));
1122 let installed = CallContext::new()
1123 .with_cancel(token.clone())
1124 .with_recorder(rec)
1125 .with_timeout(Duration::from_secs(9))
1126 .with_deadline(now + Duration::from_secs(50))
1127 .with_redactor(Arc::new(|_| "X".into()));
1128 let local = CallContext::new()
1129 .with_retry(fast(4))
1130 .with_rate_limit_retries(2)
1131 .with_deadline(now + Duration::from_secs(10))
1132 .with_roundtrip_metering(false);
1133 let eff = local.inherit(&installed);
1134 assert_eq!(eff.timeout(), Some(Duration::from_secs(9)));
1135 assert_eq!(eff.retry().unwrap().max_attempts, 4);
1136 assert_eq!(eff.rate_limit_retries(), Some(2));
1137 assert!(eff.cancel().is_some() && eff.recorder().is_some());
1138 assert_eq!(eff.deadline(), Some(now + Duration::from_secs(10)));
1139 assert_eq!(eff.redact("y"), "X");
1140 let eff = CallContext::new().without_timeout().inherit(&installed);
1141 assert_eq!(eff.timeout(), None);
1142 let eff = CallContext::new().without_retry().inherit(&installed);
1143 assert_eq!(eff.retry().unwrap().max_attempts, 1);
1144 assert!(format!("{eff:?}").contains("CallContext"));
1145 }
1146
1147 #[tokio::test]
1148 async fn a_slot_uses_its_own_settings_until_installed() {
1149 let mut slot = CallSlot::new(CallContext::new().with_timeout(Duration::from_secs(3)));
1150 assert_eq!(slot.get().timeout(), Some(Duration::from_secs(3)));
1151 assert!(slot.recorder().is_none());
1152 slot.record("noop");
1153 slot.set_local(CallContext::new().with_retry(fast(2)));
1154 assert_eq!(slot.local().retry().unwrap().max_attempts, 2);
1155 let rec = Arc::new(RoundtripRecorder::new(RoundtripSide::Source, "p", "r", "c"));
1156 slot.install_recorder(rec);
1157 assert!(slot.recorder().is_some());
1158 let token = CancellationToken::new();
1159 slot.install(CallContext::new().with_cancel(token.clone()));
1160 slot.install(CallContext::new());
1161 let eff = slot.get();
1162 assert!(eff.cancel().is_some(), "first install wins");
1163 assert!(eff.recorder().is_some(), "recorder fallback");
1164 assert_eq!(eff.retry().unwrap().max_attempts, 2);
1165 assert_eq!(slot.call("op", || async { Ok(1) }).await.unwrap(), 1);
1166 token.cancel();
1167 let r: Result<(), _> = slot.call("op", || async { Ok(()) }).await;
1168 assert!(matches!(r, Err(FaucetError::Cancelled { .. })));
1169 }
1170}