Skip to main content

polyoxide_core/
health.rs

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