polyoxide_core/health.rs
1//! One health ping, on the send loop.
2
3use std::sync::Mutex;
4use std::time::{Duration, Instant};
5
6use reqwest::{Method, Response};
7
8use crate::client::HttpClient;
9use crate::error::ApiError;
10use crate::hooks::{Authenticator, Cost, DynAuthenticator, RequestParts};
11use crate::request::RequestError;
12
13/// A successful [`HttpClient::health`] ping.
14#[derive(Debug)]
15#[non_exhaustive]
16pub struct Pong {
17 /// The round trip of the attempt that answered, timed from just before it
18 /// was sent. It leaves out the waits for a permit, the throttle and a
19 /// retry's backoff, so a ping that was retried reports its last attempt.
20 pub round_trip: Duration,
21 /// The 2xx response, its body unread, for a venue that checks it.
22 pub response: Response,
23}
24
25/// Notes when an attempt is signed, which is just before it is sent.
26#[derive(Default)]
27struct Stopwatch(Mutex<Option<Instant>>);
28
29impl Stopwatch {
30 /// Time since the last attempt was signed.
31 fn elapsed(&self) -> Duration {
32 self.0
33 .lock()
34 .unwrap_or_else(|poisoned| poisoned.into_inner())
35 .map_or(Duration::ZERO, |sent| sent.elapsed())
36 }
37}
38
39impl Authenticator for Stopwatch {
40 async fn sign(&self, _parts: &mut RequestParts, _attempt: u32) -> Result<(), ApiError> {
41 *self
42 .0
43 .lock()
44 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(Instant::now());
45 Ok(())
46 }
47}
48
49impl HttpClient {
50 /// Ping `path` with one `GET` on [`send`](Self::send), charging `costs`.
51 ///
52 /// The ping takes a concurrency permit and the throttle, and is retried
53 /// and held by the client's policy, as every other request is. Its
54 /// [`Pong::round_trip`] is that of the attempt that answered, so every
55 /// venue's ping latency means the same thing.
56 ///
57 /// # Errors
58 ///
59 /// A final non-2xx response is [`ApiError::Response`] as `E`, and so is a
60 /// failure before one (a refused cost, a transport error).
61 pub async fn health<E: RequestError>(&self, path: &str, costs: &[Cost]) -> Result<Pong, E> {
62 let stopwatch = Stopwatch::default();
63 let response = self
64 .send(
65 RequestParts::new(Method::GET, path),
66 costs,
67 Some(DynAuthenticator::from_ref(&stopwatch)),
68 )
69 .await?;
70 let round_trip = stopwatch.elapsed();
71
72 // Only a policy that is `Done` with a failed response gets here.
73 if !response.status().is_success() {
74 return Err(ApiError::from_response(response).await.into());
75 }
76
77 Ok(Pong {
78 round_trip,
79 response,
80 })
81 }
82}