1use std::sync::Arc;
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::time::Duration;
22
23use axum_core::body::Body;
24use axum_core::extract::Request;
25use axum_core::response::{IntoResponse, Response};
26use http::{HeaderMap, HeaderValue, StatusCode};
27use r402_core::facilitator::Facilitator;
28use r402_core::wire;
29use r402_core::wire::Base64Bytes;
30use serde_json::json;
31use tokio::sync::Notify;
32use tower::Service;
33#[cfg(feature = "telemetry")]
34use tracing::{Instrument, instrument};
35use url::Url;
36
37use super::hooks::DynPaygateHooks;
38
39const PAYMENT_HEADER: &str = "Payment-Signature";
40
41#[derive(Debug, thiserror::Error)]
43pub enum VerificationError {
44 #[error("Payment-Signature header is required")]
46 PaymentHeaderMissing,
47 #[error("Invalid or malformed payment header")]
49 InvalidPaymentHeader,
50 #[error("Unable to find matching payment requirements")]
52 NoPaymentMatching,
53 #[error("Verification failed: {0}")]
55 VerificationFailed(String),
56}
57
58#[derive(Debug, thiserror::Error)]
60pub enum PaygateError {
61 #[error(transparent)]
63 Verification(#[from] VerificationError),
64 #[error("settlement failed: {}", settlement_failure_summary(.0))]
70 Settlement(Box<wire::SettleResponse>),
71 #[error("settlement aborted: {0}")]
75 SettlementAborted(String),
76}
77
78#[allow(
79 clippy::missing_const_for_fn,
80 reason = "const fn would prevent matching on `Box` indirection"
81)]
82fn settlement_failure_summary(resp: &wire::SettleResponse) -> String {
83 match resp {
84 wire::SettleResponse::Failure {
85 reason,
86 message,
87 network,
88 ..
89 } => format!(
90 "{} ({}){}",
91 reason,
92 network,
93 message
94 .as_ref()
95 .map(|m| format!(": {m}"))
96 .unwrap_or_default(),
97 ),
98 wire::SettleResponse::Success { .. } => "success returned via error path".to_owned(),
99 _ => "unknown settlement variant".to_owned(),
102 }
103}
104
105type PaymentPayload = wire::PaymentPayload<wire::PaymentRequirements, serde_json::Value>;
106
107#[derive(Debug, Clone)]
112pub struct ResourceTemplate {
113 pub description: String,
115 pub mime_type: String,
117 pub url: Option<String>,
119}
120
121impl Default for ResourceTemplate {
122 fn default() -> Self {
123 Self {
124 description: String::new(),
125 mime_type: "application/json".to_owned(),
126 url: None,
127 }
128 }
129}
130
131impl ResourceTemplate {
132 #[allow(clippy::unwrap_used, reason = "fallback URL is a hardcoded constant")]
143 pub fn resolve(&self, base_url: Option<&Url>, req: &Request) -> wire::ResourceInfo {
144 let url = self.url.clone().unwrap_or_else(|| {
145 let mut url = base_url.cloned().unwrap_or_else(|| {
146 let host = req
147 .headers()
148 .get("host")
149 .and_then(|h| h.to_str().ok())
150 .unwrap_or("localhost");
151 let origin = format!("http://{host}");
152 let url =
153 Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
154 #[cfg(feature = "telemetry")]
155 tracing::warn!(
156 "X402Middleware base_url is not configured; \
157 using {url} as origin for resource resolution"
158 );
159 url
160 });
161 url.set_path(req.uri().path());
162 url.set_query(req.uri().query());
163 url.to_string()
164 });
165 let mut info = wire::ResourceInfo::new(url);
166 if !self.description.is_empty() {
167 info = info.with_description(self.description.clone());
168 }
169 if !self.mime_type.is_empty() {
170 info = info.with_mime_type(self.mime_type.clone());
171 }
172 info
173 }
174}
175
176#[allow(
187 missing_debug_implementations,
188 reason = "generic facilitator may not impl Debug"
189)]
190pub struct PaygateBuilder<TFacilitator> {
191 facilitator: TFacilitator,
192 accepts: Vec<wire::PriceTag>,
193 resource: Option<wire::ResourceInfo>,
194 hooks: Option<Arc<dyn DynPaygateHooks>>,
195 settlement_tracker: Option<BackgroundSettlementTracker>,
196}
197
198impl<TFacilitator> PaygateBuilder<TFacilitator> {
199 #[must_use]
201 pub fn accept(mut self, price_tag: wire::PriceTag) -> Self {
202 self.accepts.push(price_tag);
203 self
204 }
205
206 #[must_use]
208 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = wire::PriceTag>) -> Self {
209 self.accepts.extend(price_tags);
210 self
211 }
212
213 #[must_use]
215 pub fn resource(mut self, resource: wire::ResourceInfo) -> Self {
216 self.resource = Some(resource);
217 self
218 }
219
220 #[must_use]
225 pub fn hooks<H>(mut self, hooks: H) -> Self
226 where
227 H: super::hooks::PaygateHooks + 'static,
228 {
229 self.hooks = Some(Arc::new(hooks));
230 self
231 }
232
233 #[must_use]
239 pub fn hooks_dyn(mut self, hooks: Arc<dyn DynPaygateHooks>) -> Self {
240 self.hooks = Some(hooks);
241 self
242 }
243
244 #[must_use]
249 pub fn with_settlement_tracker(mut self, tracker: BackgroundSettlementTracker) -> Self {
250 self.settlement_tracker = Some(tracker);
251 self
252 }
253
254 pub fn build(self) -> Paygate<TFacilitator> {
258 Paygate {
259 facilitator: self.facilitator,
260 accepts: self.accepts.into(),
261 resource: self
262 .resource
263 .unwrap_or_else(|| wire::ResourceInfo::new("").with_mime_type("application/json")),
264 hooks: self.hooks,
265 settlement_tracker: self.settlement_tracker,
266 }
267 }
268}
269
270#[allow(
281 missing_debug_implementations,
282 reason = "generic facilitator may not impl Debug"
283)]
284pub struct Paygate<TFacilitator> {
285 pub(crate) facilitator: TFacilitator,
286 pub(crate) accepts: Arc<[wire::PriceTag]>,
287 pub(crate) resource: wire::ResourceInfo,
288 pub(crate) hooks: Option<Arc<dyn DynPaygateHooks>>,
289 pub(crate) settlement_tracker: Option<BackgroundSettlementTracker>,
298}
299
300impl<TFacilitator> Paygate<TFacilitator> {
301 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
303 PaygateBuilder {
304 facilitator,
305 accepts: Vec::new(),
306 resource: None,
307 hooks: None,
308 settlement_tracker: None,
309 }
310 }
311
312 pub const fn facilitator(&self) -> &TFacilitator {
314 &self.facilitator
315 }
316
317 pub fn accepts(&self) -> &[wire::PriceTag] {
319 &self.accepts
320 }
321
322 #[must_use]
340 pub const fn settlement_tracker(&self) -> Option<&BackgroundSettlementTracker> {
341 self.settlement_tracker.as_ref()
342 }
343
344 pub const fn resource(&self) -> &wire::ResourceInfo {
346 &self.resource
347 }
348
349 #[must_use]
355 pub fn hooks(&self) -> Option<&Arc<dyn DynPaygateHooks>> {
356 self.hooks.as_ref()
357 }
358
359 #[must_use]
369 #[allow(
370 clippy::expect_used,
371 reason = "infallible JSON/HTTP construction; panic indicates a bug"
372 )]
373 pub fn error_response(&self, err: PaygateError) -> Response {
374 match err {
375 PaygateError::Verification(ve) => {
376 let (status, payment_required) = {
377 let status = inferred_status(&ve);
380 let payment_required = wire::PaymentRequired::new(self.resource.clone())
381 .with_error(ve.to_string())
382 .with_accepts(
383 self.accepts
384 .iter()
385 .map(|pt| pt.requirements.clone())
386 .collect(),
387 );
388 (status, payment_required)
389 };
390 let body_bytes =
391 serde_json::to_vec(&payment_required).expect("serialization failed");
392 let header_value =
393 HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
394 .expect("invalid header value");
395
396 let mut response = Response::builder()
397 .status(status)
398 .header("Payment-Required", header_value)
399 .header("Content-Type", "application/json")
400 .body(Body::from(body_bytes))
401 .expect("failed to construct response");
402 super::cors::ensure_expose_headers(response.headers_mut());
405 response
406 }
407 PaygateError::Settlement(failure) => {
408 #[cfg(feature = "telemetry")]
409 tracing::error!(failure = ?failure, "Settlement failed");
410 let body_bytes = serde_json::to_vec(&*failure).expect("serialization failed");
411 let header_value = failure
412 .encode_base64_any()
413 .and_then(|b64| HeaderValue::from_bytes(b64.as_ref()).ok());
414
415 let mut builder = Response::builder()
416 .status(StatusCode::PAYMENT_REQUIRED)
417 .header("Content-Type", "application/json");
418 if let Some(header_value) = header_value {
419 builder = builder.header("Payment-Response", header_value);
420 }
421 let mut response = builder
422 .body(Body::from(body_bytes))
423 .expect("failed to construct response");
424 super::cors::ensure_expose_headers(response.headers_mut());
425 response
426 }
427 PaygateError::SettlementAborted(ref detail) => {
428 #[cfg(feature = "telemetry")]
429 tracing::error!(details = %detail, "Settlement aborted");
430 let body = json!({
431 "error": "settlement aborted",
432 "details": detail,
433 })
434 .to_string();
435
436 let mut response = Response::builder()
437 .status(StatusCode::PAYMENT_REQUIRED)
438 .header("Content-Type", "application/json")
439 .body(Body::from(body))
440 .expect("failed to construct response");
441 super::cors::ensure_expose_headers(response.headers_mut());
442 response
443 }
444 }
445 }
446}
447
448impl<TFacilitator> Paygate<TFacilitator>
449where
450 TFacilitator: Facilitator + Sync,
451{
452 pub async fn enrich_accepts(&mut self) {
454 let capabilities = self.facilitator.supported().await.unwrap_or_default();
455 let accepts: Vec<_> = self
456 .accepts
457 .iter()
458 .cloned()
459 .map(|mut pt| {
460 pt.enrich(&capabilities);
461 pt
462 })
463 .collect();
464 self.accepts = accepts.into();
465 }
466
467 #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
478 pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
479 let header_bytes = headers
480 .get(PAYMENT_HEADER)
481 .map(HeaderValue::as_bytes)
482 .ok_or(VerificationError::PaymentHeaderMissing)?;
483
484 let payload: PaymentPayload =
485 decode_payment_payload(header_bytes).ok_or(VerificationError::InvalidPaymentHeader)?;
486
487 let verify_request = build_verify_request(payload, &self.accepts)?;
488
489 let verify_response = self
490 .facilitator
491 .verify(verify_request.clone())
492 .await
493 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
494
495 if let wire::VerifyResponse::Invalid { reason, .. } = verify_response {
496 return Err(VerificationError::VerificationFailed(reason.to_string()).into());
497 }
498
499 Ok(VerifiedPayment {
500 settle_request: verify_request.into(),
501 })
502 }
503
504 #[cfg_attr(
516 feature = "telemetry",
517 instrument(name = "x402.handle_request", skip_all)
518 )]
519 pub async fn handle_request<
520 ReqBody,
521 ResBody,
522 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
523 >(
524 &self,
525 inner: S,
526 req: http::Request<ReqBody>,
527 ) -> Result<Response, PaygateError>
528 where
529 S::Response: IntoResponse,
530 S::Error: IntoResponse,
531 S::Future: Send,
532 {
533 let verified = self.verify_only(req.headers()).await?;
534
535 let response = match call_inner(inner, req).await {
536 Ok(r) => r,
537 Err(err) => return Ok(err.into_response()),
538 };
539
540 if response.status().is_client_error() || response.status().is_server_error() {
541 return Ok(response.into_response());
542 }
543
544 let mut response = response.into_response();
545 let override_amount = response
548 .extensions_mut()
549 .remove::<super::upto::UptoActualAmount>();
550
551 let settlement = verified
552 .settle_with_override(
553 &self.facilitator,
554 override_amount
555 .as_ref()
556 .map(super::upto::UptoActualAmount::as_str),
557 )
558 .await?;
559 let header_value = settlement_to_header(&settlement)?;
560
561 response
562 .headers_mut()
563 .insert("Payment-Response", header_value);
564 super::cors::ensure_expose_headers(response.headers_mut());
566 Ok(response)
567 }
568}
569
570impl<TFacilitator> Paygate<TFacilitator>
571where
572 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
573{
574 #[cfg_attr(
588 feature = "telemetry",
589 instrument(name = "x402.handle_request_concurrent", skip_all)
590 )]
591 pub async fn handle_request_concurrent<
592 ReqBody,
593 ResBody,
594 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
595 >(
596 &self,
597 inner: S,
598 req: http::Request<ReqBody>,
599 ) -> Result<Response, PaygateError>
600 where
601 S::Response: IntoResponse,
602 S::Error: IntoResponse,
603 S::Future: Send + 'static,
604 ReqBody: Send + 'static,
605 {
606 let verified = self.verify_only(req.headers()).await?;
607
608 let facilitator = self.facilitator.clone();
609 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
610
611 let response = match call_inner(inner, req).await {
612 Ok(r) => r,
613 Err(err) => {
614 drop(settle_handle);
615 return Ok(err.into_response());
616 }
617 };
618
619 if response.status().is_client_error() || response.status().is_server_error() {
620 drop(settle_handle);
621 return Ok(response.into_response());
622 }
623
624 let settlement = settle_handle
625 .await
626 .map_err(|e| PaygateError::SettlementAborted(format!("settle task panicked: {e}")))??;
627 let header_value = settlement_to_header(&settlement)?;
628
629 let mut res = response.into_response();
630 res.headers_mut().insert("Payment-Response", header_value);
631 super::cors::ensure_expose_headers(res.headers_mut());
632 Ok(res)
633 }
634
635 #[cfg_attr(
656 feature = "telemetry",
657 instrument(name = "x402.handle_request_background", skip_all)
658 )]
659 pub async fn handle_request_background<
660 ReqBody,
661 ResBody,
662 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
663 >(
664 &self,
665 inner: S,
666 req: http::Request<ReqBody>,
667 ) -> Result<Response, PaygateError>
668 where
669 S::Response: IntoResponse,
670 S::Error: IntoResponse,
671 S::Future: Send + 'static,
672 ReqBody: Send + 'static,
673 {
674 let verified = self.verify_only(req.headers()).await?;
675
676 let facilitator = self.facilitator.clone();
688 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
689 let tracker_guard = self
693 .settlement_tracker
694 .as_ref()
695 .map(BackgroundSettlementTracker::start);
696 drop(tokio::spawn(supervise_background_settle(
700 settle_handle,
701 tracker_guard,
702 )));
703
704 match call_inner(inner, req).await {
705 Ok(r) => Ok(r.into_response()),
706 Err(err) => Ok(err.into_response()),
707 }
708 }
709}
710
711#[derive(Debug)]
717pub struct VerifiedPayment {
718 settle_request: wire::SettleRequest,
719}
720
721impl VerifiedPayment {
722 pub async fn settle<F: Facilitator>(
729 self,
730 facilitator: &F,
731 ) -> Result<wire::SettleResponse, PaygateError> {
732 self.settle_with_override(facilitator, None).await
733 }
734
735 pub async fn settle_with_override<F: Facilitator>(
749 mut self,
750 facilitator: &F,
751 actual_amount: Option<&str>,
752 ) -> Result<wire::SettleResponse, PaygateError> {
753 if let Some(amount) = actual_amount {
754 self.settle_request
755 .set_settlement_amount(amount)
756 .map_err(|e| {
757 PaygateError::SettlementAborted(format!("upto amount override failed: {e}"))
758 })?;
759 }
760 let settlement = facilitator
761 .settle(self.settle_request)
762 .await
763 .map_err(|e| PaygateError::SettlementAborted(format!("{e}")))?;
764
765 if matches!(settlement, wire::SettleResponse::Failure { .. }) {
766 return Err(PaygateError::Settlement(Box::new(settlement)));
767 }
768
769 Ok(settlement)
770 }
771
772 #[must_use]
774 pub const fn settle_request(&self) -> &wire::SettleRequest {
775 &self.settle_request
776 }
777}
778
779#[derive(Clone, Debug)]
792pub struct BackgroundSettlementTracker {
793 inner: Arc<TrackerInner>,
794}
795
796#[derive(Debug)]
797struct TrackerInner {
798 in_flight: AtomicUsize,
799 drained: Notify,
800}
801
802impl Default for BackgroundSettlementTracker {
803 fn default() -> Self {
804 Self::new()
805 }
806}
807
808impl BackgroundSettlementTracker {
809 #[must_use]
811 pub fn new() -> Self {
812 Self {
813 inner: Arc::new(TrackerInner {
814 in_flight: AtomicUsize::new(0),
815 drained: Notify::new(),
816 }),
817 }
818 }
819
820 #[must_use]
823 pub fn in_flight(&self) -> usize {
824 self.inner.in_flight.load(Ordering::SeqCst)
825 }
826
827 fn start(&self) -> SettlementInFlightGuard {
831 let _previous = self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
832 SettlementInFlightGuard {
833 inner: Arc::clone(&self.inner),
834 }
835 }
836
837 pub async fn wait_for_drain(&self, timeout: Duration) -> Result<(), usize> {
847 if self.in_flight() == 0 {
848 return Ok(());
849 }
850 let deadline = tokio::time::Instant::now() + timeout;
851 loop {
852 let notified = self.inner.drained.notified();
853 tokio::pin!(notified);
854 tokio::select! {
855 () = &mut notified => {}
856 () = tokio::time::sleep_until(deadline) => {
857 let remaining = self.in_flight();
858 return if remaining == 0 { Ok(()) } else { Err(remaining) };
859 }
860 }
861 if self.in_flight() == 0 {
862 return Ok(());
863 }
864 }
865 }
866}
867
868#[derive(Debug)]
875pub(crate) struct SettlementInFlightGuard {
876 inner: Arc<TrackerInner>,
877}
878
879impl Drop for SettlementInFlightGuard {
880 fn drop(&mut self) {
881 let previous = self.inner.in_flight.fetch_sub(1, Ordering::SeqCst);
882 if previous == 1 {
883 self.inner.drained.notify_waiters();
885 }
886 }
887}
888
889async fn supervise_background_settle(
905 handle: tokio::task::JoinHandle<Result<wire::SettleResponse, PaygateError>>,
906 _tracker: Option<SettlementInFlightGuard>,
911) {
912 let outcome = handle.await;
913 log_background_settle_outcome(outcome);
914}
915
916fn log_background_settle_outcome(
921 outcome: Result<Result<wire::SettleResponse, PaygateError>, tokio::task::JoinError>,
922) {
923 match outcome {
924 Ok(Ok(_settlement)) => {
925 #[cfg(feature = "telemetry")]
926 tracing::debug!("background settlement completed");
927 record_background_settle_metric("ok");
928 }
929 Ok(Err(err)) => {
930 log_background_settle_error(&err);
931 record_background_settle_metric("error");
932 }
933 Err(join_err) => {
934 let label = if join_err.is_panic() {
935 "panic"
936 } else {
937 "cancelled"
938 };
939 log_background_settle_join_error(&join_err);
940 record_background_settle_metric(label);
941 }
942 }
943}
944
945#[cfg(feature = "metrics")]
946fn record_background_settle_metric(result: &'static str) {
947 ::metrics::counter!(
948 r402_core::metrics::PAYGATE_BACKGROUND_SETTLE_TOTAL,
949 "result" => result,
950 )
951 .increment(1);
952}
953#[cfg(not(feature = "metrics"))]
954fn record_background_settle_metric(_result: &'static str) {}
955
956#[cfg(feature = "telemetry")]
957fn log_background_settle_error(err: &PaygateError) {
958 tracing::error!(error = %err, "background settlement returned error");
959}
960#[cfg(not(feature = "telemetry"))]
961fn log_background_settle_error(_err: &PaygateError) {}
962
963#[cfg(feature = "telemetry")]
964fn log_background_settle_join_error(join_err: &tokio::task::JoinError) {
965 if join_err.is_panic() {
966 tracing::error!(error = %join_err, "background settlement task panicked");
967 } else {
968 tracing::warn!(error = %join_err, "background settlement task cancelled");
969 }
970}
971#[cfg(not(feature = "telemetry"))]
972fn log_background_settle_join_error(_join_err: &tokio::task::JoinError) {}
973
974pub fn settlement_to_header(
981 settlement: &wire::SettleResponse,
982) -> Result<HeaderValue, PaygateError> {
983 let encoded = settlement.encode_base64().ok_or_else(|| {
984 PaygateError::SettlementAborted("cannot encode error settlement".to_owned())
985 })?;
986 HeaderValue::from_bytes(encoded.as_ref())
987 .map_err(|e| PaygateError::SettlementAborted(e.to_string()))
988}
989
990async fn call_inner<
992 ReqBody,
993 ResBody,
994 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
995>(
996 mut inner: S,
997 req: http::Request<ReqBody>,
998) -> Result<http::Response<ResBody>, S::Error>
999where
1000 S::Future: Send,
1001{
1002 #[cfg(feature = "telemetry")]
1003 {
1004 inner
1005 .call(req)
1006 .instrument(tracing::info_span!("inner"))
1007 .await
1008 }
1009 #[cfg(not(feature = "telemetry"))]
1010 {
1011 inner.call(req).await
1012 }
1013}
1014
1015fn decode_payment_payload<T: serde::de::DeserializeOwned>(header_bytes: &[u8]) -> Option<T> {
1017 let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
1018 serde_json::from_slice(decoded.as_ref()).ok()
1019}
1020
1021fn inferred_status(ve: &VerificationError) -> StatusCode {
1030 if let VerificationError::VerificationFailed(message) = ve
1031 && message.contains("permit2_allowance_required")
1032 {
1033 return StatusCode::PRECONDITION_FAILED;
1034 }
1035 StatusCode::PAYMENT_REQUIRED
1036}
1037
1038fn build_verify_request(
1039 payload: PaymentPayload,
1040 accepts: &[wire::PriceTag],
1041) -> Result<wire::VerifyRequest, VerificationError> {
1042 let selected = accepts
1043 .iter()
1044 .find(|pt| **pt == payload.accepted)
1045 .ok_or(VerificationError::NoPaymentMatching)?;
1046
1047 let verify: wire::TypedVerifyRequest<2, PaymentPayload, wire::PaymentRequirements> =
1048 wire::TypedVerifyRequest {
1049 x402_version: wire::V2,
1050 payment_payload: payload,
1051 payment_requirements: selected.requirements.clone(),
1052 };
1053
1054 let json = serde_json::to_value(&verify)
1055 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
1056
1057 Ok(wire::VerifyRequest::from(json))
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 #[tokio::test]
1065 async fn empty_tracker_drains_immediately() {
1066 let tracker = BackgroundSettlementTracker::new();
1067 assert_eq!(tracker.in_flight(), 0);
1068 tracker.wait_for_drain(Duration::ZERO).await.unwrap();
1071 }
1072
1073 #[tokio::test]
1074 async fn drain_waits_for_guard_drop() {
1075 let tracker = BackgroundSettlementTracker::new();
1076 let guard = tracker.start();
1077 assert_eq!(tracker.in_flight(), 1);
1078
1079 let tracker_clone = tracker.clone();
1082 let drop_task = tokio::spawn(async move {
1083 tokio::time::sleep(Duration::from_millis(10)).await;
1084 drop(guard);
1085 assert_eq!(tracker_clone.in_flight(), 0);
1086 });
1087
1088 tracker
1089 .wait_for_drain(Duration::from_secs(1))
1090 .await
1091 .expect("drain should complete after the guard drops");
1092 drop_task.await.unwrap();
1093 }
1094
1095 #[tokio::test]
1096 async fn drain_times_out_when_guards_outlive_deadline() {
1097 let tracker = BackgroundSettlementTracker::new();
1098 let _guard = tracker.start();
1099
1100 let result = tracker.wait_for_drain(Duration::from_millis(20)).await;
1101 assert_eq!(result, Err(1), "deadline elapses with the guard alive");
1102 }
1103
1104 #[tokio::test]
1105 async fn nested_guards_decrement_in_order() {
1106 let tracker = BackgroundSettlementTracker::new();
1107 let g1 = tracker.start();
1108 let g2 = tracker.start();
1109 let g3 = tracker.start();
1110 assert_eq!(tracker.in_flight(), 3);
1111 drop(g2);
1112 assert_eq!(tracker.in_flight(), 2);
1113 drop(g1);
1114 assert_eq!(tracker.in_flight(), 1);
1115 drop(g3);
1116 assert_eq!(tracker.in_flight(), 0);
1117 }
1118}