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 Ok(response)
565 }
566}
567
568impl<TFacilitator> Paygate<TFacilitator>
569where
570 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
571{
572 #[cfg_attr(
586 feature = "telemetry",
587 instrument(name = "x402.handle_request_concurrent", skip_all)
588 )]
589 pub async fn handle_request_concurrent<
590 ReqBody,
591 ResBody,
592 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
593 >(
594 &self,
595 inner: S,
596 req: http::Request<ReqBody>,
597 ) -> Result<Response, PaygateError>
598 where
599 S::Response: IntoResponse,
600 S::Error: IntoResponse,
601 S::Future: Send + 'static,
602 ReqBody: Send + 'static,
603 {
604 let verified = self.verify_only(req.headers()).await?;
605
606 let facilitator = self.facilitator.clone();
607 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
608
609 let response = match call_inner(inner, req).await {
610 Ok(r) => r,
611 Err(err) => {
612 drop(settle_handle);
613 return Ok(err.into_response());
614 }
615 };
616
617 if response.status().is_client_error() || response.status().is_server_error() {
618 drop(settle_handle);
619 return Ok(response.into_response());
620 }
621
622 let settlement = settle_handle
623 .await
624 .map_err(|e| PaygateError::SettlementAborted(format!("settle task panicked: {e}")))??;
625 let header_value = settlement_to_header(&settlement)?;
626
627 let mut res = response;
628 res.headers_mut().insert("Payment-Response", header_value);
629 Ok(res.into_response())
630 }
631
632 #[cfg_attr(
653 feature = "telemetry",
654 instrument(name = "x402.handle_request_background", skip_all)
655 )]
656 pub async fn handle_request_background<
657 ReqBody,
658 ResBody,
659 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
660 >(
661 &self,
662 inner: S,
663 req: http::Request<ReqBody>,
664 ) -> Result<Response, PaygateError>
665 where
666 S::Response: IntoResponse,
667 S::Error: IntoResponse,
668 S::Future: Send + 'static,
669 ReqBody: Send + 'static,
670 {
671 let verified = self.verify_only(req.headers()).await?;
672
673 let facilitator = self.facilitator.clone();
685 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
686 let tracker_guard = self
690 .settlement_tracker
691 .as_ref()
692 .map(BackgroundSettlementTracker::start);
693 drop(tokio::spawn(supervise_background_settle(
697 settle_handle,
698 tracker_guard,
699 )));
700
701 match call_inner(inner, req).await {
702 Ok(r) => Ok(r.into_response()),
703 Err(err) => Ok(err.into_response()),
704 }
705 }
706}
707
708#[derive(Debug)]
714pub struct VerifiedPayment {
715 settle_request: wire::SettleRequest,
716}
717
718impl VerifiedPayment {
719 pub async fn settle<F: Facilitator>(
726 self,
727 facilitator: &F,
728 ) -> Result<wire::SettleResponse, PaygateError> {
729 self.settle_with_override(facilitator, None).await
730 }
731
732 pub async fn settle_with_override<F: Facilitator>(
746 mut self,
747 facilitator: &F,
748 actual_amount: Option<&str>,
749 ) -> Result<wire::SettleResponse, PaygateError> {
750 if let Some(amount) = actual_amount {
751 self.settle_request
752 .set_settlement_amount(amount)
753 .map_err(|e| {
754 PaygateError::SettlementAborted(format!("upto amount override failed: {e}"))
755 })?;
756 }
757 let settlement = facilitator
758 .settle(self.settle_request)
759 .await
760 .map_err(|e| PaygateError::SettlementAborted(format!("{e}")))?;
761
762 if matches!(settlement, wire::SettleResponse::Failure { .. }) {
763 return Err(PaygateError::Settlement(Box::new(settlement)));
764 }
765
766 Ok(settlement)
767 }
768
769 #[must_use]
771 pub const fn settle_request(&self) -> &wire::SettleRequest {
772 &self.settle_request
773 }
774}
775
776#[derive(Clone, Debug)]
789pub struct BackgroundSettlementTracker {
790 inner: Arc<TrackerInner>,
791}
792
793#[derive(Debug)]
794struct TrackerInner {
795 in_flight: AtomicUsize,
796 drained: Notify,
797}
798
799impl Default for BackgroundSettlementTracker {
800 fn default() -> Self {
801 Self::new()
802 }
803}
804
805impl BackgroundSettlementTracker {
806 #[must_use]
808 pub fn new() -> Self {
809 Self {
810 inner: Arc::new(TrackerInner {
811 in_flight: AtomicUsize::new(0),
812 drained: Notify::new(),
813 }),
814 }
815 }
816
817 #[must_use]
820 pub fn in_flight(&self) -> usize {
821 self.inner.in_flight.load(Ordering::SeqCst)
822 }
823
824 fn start(&self) -> SettlementInFlightGuard {
828 let _previous = self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
829 SettlementInFlightGuard {
830 inner: Arc::clone(&self.inner),
831 }
832 }
833
834 pub async fn wait_for_drain(&self, timeout: Duration) -> Result<(), usize> {
844 if self.in_flight() == 0 {
845 return Ok(());
846 }
847 let deadline = tokio::time::Instant::now() + timeout;
848 loop {
849 let notified = self.inner.drained.notified();
850 tokio::pin!(notified);
851 tokio::select! {
852 () = &mut notified => {}
853 () = tokio::time::sleep_until(deadline) => {
854 let remaining = self.in_flight();
855 return if remaining == 0 { Ok(()) } else { Err(remaining) };
856 }
857 }
858 if self.in_flight() == 0 {
859 return Ok(());
860 }
861 }
862 }
863}
864
865#[derive(Debug)]
872pub(crate) struct SettlementInFlightGuard {
873 inner: Arc<TrackerInner>,
874}
875
876impl Drop for SettlementInFlightGuard {
877 fn drop(&mut self) {
878 let previous = self.inner.in_flight.fetch_sub(1, Ordering::SeqCst);
879 if previous == 1 {
880 self.inner.drained.notify_waiters();
882 }
883 }
884}
885
886async fn supervise_background_settle(
902 handle: tokio::task::JoinHandle<Result<wire::SettleResponse, PaygateError>>,
903 _tracker: Option<SettlementInFlightGuard>,
908) {
909 let outcome = handle.await;
910 log_background_settle_outcome(outcome);
911}
912
913fn log_background_settle_outcome(
918 outcome: Result<Result<wire::SettleResponse, PaygateError>, tokio::task::JoinError>,
919) {
920 match outcome {
921 Ok(Ok(_settlement)) => {
922 #[cfg(feature = "telemetry")]
923 tracing::debug!("background settlement completed");
924 record_background_settle_metric("ok");
925 }
926 Ok(Err(err)) => {
927 log_background_settle_error(&err);
928 record_background_settle_metric("error");
929 }
930 Err(join_err) => {
931 let label = if join_err.is_panic() {
932 "panic"
933 } else {
934 "cancelled"
935 };
936 log_background_settle_join_error(&join_err);
937 record_background_settle_metric(label);
938 }
939 }
940}
941
942#[cfg(feature = "metrics")]
943fn record_background_settle_metric(result: &'static str) {
944 ::metrics::counter!(
945 r402_core::metrics::PAYGATE_BACKGROUND_SETTLE_TOTAL,
946 "result" => result,
947 )
948 .increment(1);
949}
950#[cfg(not(feature = "metrics"))]
951fn record_background_settle_metric(_result: &'static str) {}
952
953#[cfg(feature = "telemetry")]
954fn log_background_settle_error(err: &PaygateError) {
955 tracing::error!(error = %err, "background settlement returned error");
956}
957#[cfg(not(feature = "telemetry"))]
958fn log_background_settle_error(_err: &PaygateError) {}
959
960#[cfg(feature = "telemetry")]
961fn log_background_settle_join_error(join_err: &tokio::task::JoinError) {
962 if join_err.is_panic() {
963 tracing::error!(error = %join_err, "background settlement task panicked");
964 } else {
965 tracing::warn!(error = %join_err, "background settlement task cancelled");
966 }
967}
968#[cfg(not(feature = "telemetry"))]
969fn log_background_settle_join_error(_join_err: &tokio::task::JoinError) {}
970
971pub fn settlement_to_header(
978 settlement: &wire::SettleResponse,
979) -> Result<HeaderValue, PaygateError> {
980 let encoded = settlement.encode_base64().ok_or_else(|| {
981 PaygateError::SettlementAborted("cannot encode error settlement".to_owned())
982 })?;
983 HeaderValue::from_bytes(encoded.as_ref())
984 .map_err(|e| PaygateError::SettlementAborted(e.to_string()))
985}
986
987async fn call_inner<
989 ReqBody,
990 ResBody,
991 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
992>(
993 mut inner: S,
994 req: http::Request<ReqBody>,
995) -> Result<http::Response<ResBody>, S::Error>
996where
997 S::Future: Send,
998{
999 #[cfg(feature = "telemetry")]
1000 {
1001 inner
1002 .call(req)
1003 .instrument(tracing::info_span!("inner"))
1004 .await
1005 }
1006 #[cfg(not(feature = "telemetry"))]
1007 {
1008 inner.call(req).await
1009 }
1010}
1011
1012fn decode_payment_payload<T: serde::de::DeserializeOwned>(header_bytes: &[u8]) -> Option<T> {
1014 let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
1015 serde_json::from_slice(decoded.as_ref()).ok()
1016}
1017
1018fn inferred_status(ve: &VerificationError) -> StatusCode {
1027 if let VerificationError::VerificationFailed(message) = ve
1028 && message.contains("permit2_allowance_required")
1029 {
1030 return StatusCode::PRECONDITION_FAILED;
1031 }
1032 StatusCode::PAYMENT_REQUIRED
1033}
1034
1035fn build_verify_request(
1036 payload: PaymentPayload,
1037 accepts: &[wire::PriceTag],
1038) -> Result<wire::VerifyRequest, VerificationError> {
1039 let selected = accepts
1040 .iter()
1041 .find(|pt| **pt == payload.accepted)
1042 .ok_or(VerificationError::NoPaymentMatching)?;
1043
1044 let verify: wire::TypedVerifyRequest<2, PaymentPayload, wire::PaymentRequirements> =
1045 wire::TypedVerifyRequest {
1046 x402_version: wire::V2,
1047 payment_payload: payload,
1048 payment_requirements: selected.requirements.clone(),
1049 };
1050
1051 let json = serde_json::to_value(&verify)
1052 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
1053
1054 Ok(wire::VerifyRequest::from(json))
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060
1061 #[tokio::test]
1062 async fn empty_tracker_drains_immediately() {
1063 let tracker = BackgroundSettlementTracker::new();
1064 assert_eq!(tracker.in_flight(), 0);
1065 tracker.wait_for_drain(Duration::ZERO).await.unwrap();
1068 }
1069
1070 #[tokio::test]
1071 async fn drain_waits_for_guard_drop() {
1072 let tracker = BackgroundSettlementTracker::new();
1073 let guard = tracker.start();
1074 assert_eq!(tracker.in_flight(), 1);
1075
1076 let tracker_clone = tracker.clone();
1079 let drop_task = tokio::spawn(async move {
1080 tokio::time::sleep(Duration::from_millis(10)).await;
1081 drop(guard);
1082 assert_eq!(tracker_clone.in_flight(), 0);
1083 });
1084
1085 tracker
1086 .wait_for_drain(Duration::from_secs(1))
1087 .await
1088 .expect("drain should complete after the guard drops");
1089 drop_task.await.unwrap();
1090 }
1091
1092 #[tokio::test]
1093 async fn drain_times_out_when_guards_outlive_deadline() {
1094 let tracker = BackgroundSettlementTracker::new();
1095 let _guard = tracker.start();
1096
1097 let result = tracker.wait_for_drain(Duration::from_millis(20)).await;
1098 assert_eq!(result, Err(1), "deadline elapses with the guard alive");
1099 }
1100
1101 #[tokio::test]
1102 async fn nested_guards_decrement_in_order() {
1103 let tracker = BackgroundSettlementTracker::new();
1104 let g1 = tracker.start();
1105 let g2 = tracker.start();
1106 let g3 = tracker.start();
1107 assert_eq!(tracker.in_flight(), 3);
1108 drop(g2);
1109 assert_eq!(tracker.in_flight(), 2);
1110 drop(g1);
1111 assert_eq!(tracker.in_flight(), 1);
1112 drop(g3);
1113 assert_eq!(tracker.in_flight(), 0);
1114 }
1115}