Skip to main content

ergon_io/
client.rs

1//! A pooled HTTP client; the implementation is configuration.
2//!
3//! | `HttpImpl` | What | Use it for |
4//! |---|---|---|
5//! | [`HttpImpl::Native`] (default) | ergon-io's HTTP/1.1 client: a pool of warm connections on a [`Driver`] | latency: on the same interrupt-driven path (`epoll-spin`), 5–12 µs faster per round trip than the source-built libcurl at the median, 1 connection; the p99 and max were lower on Azure, not on AWS (`io/benches/BENCHMARK.md`, "HTTP clients"; each pooled connection is one more socket the loop polls) |
6//! | [`HttpImpl::Curl`] (feature `curl`) | libcurl, linked in-process: one multi handle, reused easy handles | endpoints that need redirects, proxies, HTTP/2, other auth schemes or compression |
7//!
8//! - One API for both: [`HttpClient::request`] and
9//!   [`HttpClient::signed_request`] return a request id at once;
10//!   [`HttpClient::poll`] (one non-blocking pass) reports each result to
11//!   [`ClientHandler`]. Switching costs one predictable branch per call.
12//! - Native: each request goes to a connection with nothing in flight; none
13//!   free gives [`Error::Busy`] (no queue). A dropped connection is replaced
14//!   in the background after a random delay of up to 100 ms (connections
15//!   that drop together do not reconnect together), with backoff while
16//!   connects fail. [`HttpClient::poll_gated`] puts every connect through
17//!   [`ClientHandler::connect_gate`] (a venue's connect limit).
18//! - Curl: never blocks. Driven by `curl_multi_perform` on every poll, or by
19//!   `curl_multi_socket_action` on libcurl's own sockets ([`CurlDrive`]).
20//!   Takes GET, HEAD, OPTIONS and DELETE only, unless
21//!   [`HttpClientConfig::curl_allow_non_idempotent_methods`]: libcurl sends
22//!   a request again when its reused connection dies before the reply.
23//! - A failed request says whether the server can have seen it
24//!   ([`ClientHandler::on_error`], [`Sent`]).
25
26use std::str::FromStr;
27use std::sync::Arc;
28
29use crate::RxMeta;
30use crate::auth::Signer;
31use crate::backend::{Backend, Config};
32use crate::clock::monotonic_ns;
33use crate::driver::Driver;
34use crate::endpoint::{ConnId, Handler};
35use crate::error::{Error, Result};
36use crate::http::{HttpConn, HttpDial, HttpHandler, HttpOptions, Response};
37use crate::net::Target;
38use crate::outcome::Sent;
39
40/// Which HTTP implementation an [`HttpClient`] uses.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum HttpImpl {
43    /// ergon-io's HTTP/1.1 client on the [`Driver`].
44    Native,
45    /// libcurl (needs the `curl` feature).
46    Curl,
47}
48
49impl FromStr for HttpImpl {
50    type Err = Error;
51    fn from_str(s: &str) -> Result<Self> {
52        match s {
53            "native" => Ok(Self::Native),
54            "curl" => Ok(Self::Curl),
55            _ => Err(Error::Config(format!("unknown HTTP implementation {s:?}"))),
56        }
57    }
58}
59
60/// How the libcurl implementation is driven (both never block).
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum CurlDrive {
63    /// `curl_multi_perform` on every poll.
64    Perform,
65    /// `curl_multi_socket_action` on the sockets libcurl reports ready, from a
66    /// zero-timeout `epoll_wait` on libcurl's own sockets.
67    SocketAction,
68}
69
70/// [`HttpClient`] settings.
71#[derive(Debug, Clone)]
72pub struct HttpClientConfig {
73    /// The implementation.
74    pub implementation: HttpImpl,
75    /// Native: the loop backend for the pool's driver. Both implementations
76    /// take TCP keepalive, `TCP_USER_TIMEOUT` and the quick-ACK route check's
77    /// policy from it (`driver.quickack_check`, see [`crate::route`]):
78    /// libcurl's connections are checked from its pre-request callback
79    /// (libcurl 7.80 or later).
80    pub driver: Config,
81    /// Connections kept warm (native) or the most libcurl may use at once.
82    pub pool: usize,
83    /// Time from sending a request to its complete response, in milliseconds.
84    pub request_timeout_ms: u32,
85    /// Native: receive buffer per connection (must hold the largest response).
86    pub buf_size: usize,
87    /// Native: open connections on the driver's helper thread
88    /// (`Driver::connect_background`), keeping handshake CPU off the loop.
89    /// In a process that may use the polling CPU only (a pod with one CPU,
90    /// [`crate::placement`]) [`HttpClient::new`] then fails with
91    /// [`Error::Cpu`], unless `driver.inline_connect_fallback` is set: the
92    /// pool then opens connections on the loop, as with `false`.
93    pub background_connect: bool,
94    /// Curl: how libcurl is driven.
95    pub curl_drive: CurlDrive,
96    /// Curl: a CA bundle file instead of the system store. (Native trust comes
97    /// from the rustls configuration passed to [`HttpClient::new`].)
98    pub curl_ca_file: Option<String>,
99    /// Curl: accept POST, PUT, PATCH and every method other than GET, HEAD,
100    /// OPTIONS and DELETE. Off by default: libcurl sends a request again, up
101    /// to 5 times, when the reused connection it went out on dies before any
102    /// reply byte, so an order could run twice. Off, such a request fails
103    /// with [`Error::CurlResend`]. Send orders through the native client,
104    /// which never re-sends.
105    pub curl_allow_non_idempotent_methods: bool,
106    /// Native: every pool connect waits for [`ClientHandler::connect_gate`],
107    /// the first ones included, so `new` opens nothing and
108    /// [`HttpClient::poll`] refuses ([`Error::Config`]). For a venue that
109    /// limits connects per IP. Give the pool a budget bucket of its own:
110    /// venues' connect limits count WebSocket connects, not HTTP ones. Off
111    /// (default): `new` opens every connection at once.
112    pub connect_gate: bool,
113    /// Native: the seed of the reconnect delays' jitter; `0` picks one at
114    /// random. A fixed seed gives the same delays every run (tests).
115    pub reconnect_seed: u64,
116}
117
118impl HttpClientConfig {
119    /// Defaults: native, `recv-busy`, a pool of 2, background connects.
120    #[must_use]
121    pub const fn new(implementation: HttpImpl) -> Self {
122        Self {
123            implementation,
124            driver: Config::new(Backend::RecvBusy),
125            pool: 2,
126            request_timeout_ms: 5_000,
127            buf_size: 1 << 20,
128            background_connect: true,
129            curl_drive: CurlDrive::Perform,
130            curl_ca_file: None,
131            curl_allow_non_idempotent_methods: false,
132            connect_gate: false,
133            reconnect_seed: 0,
134        }
135    }
136}
137
138/// Receives the outcome of every request.
139pub trait ClientHandler {
140    /// The complete response to `request`; it borrows the client's buffers for
141    /// the duration of the call.
142    fn on_response(&mut self, request: u64, response: &Response<'_>);
143    /// `request` failed with no response (connection error, timeout,
144    /// libcurl error, an unreadable response). Every request ends in exactly
145    /// one of `on_response` or `on_error`.
146    ///
147    /// `sent`: [`Sent::No`] when the request cannot have reached the server
148    /// (safe to send again); [`Sent::Maybe`] when it may have run (reconcile
149    /// an order by its client order id, never send it again blindly). A
150    /// timeout is [`Error::Timeout`] (`"http request"`) on both
151    /// implementations.
152    fn on_error(&mut self, request: u64, sent: Sent, error: Error) {
153        let _ = (request, sent, error);
154    }
155    /// A pooled connection could not be opened (native implementation). The
156    /// pool keeps retrying, each time after a random delay below a limit
157    /// that starts at 100 ms and doubles per failure in a row, up to 5 s;
158    /// `poll` itself does not fail, and [`HttpClient::connect_failures`]
159    /// counts them.
160    ///
161    /// The default prints a warning to stderr, at most one every 10 s across
162    /// the process (with the number held back). A thread started with the
163    /// client, on the housekeeping CPUs, makes the write, so the polling
164    /// thread only formats the line and queues it. Implement it to route
165    /// failures to your own logging or alerting.
166    fn on_connect_error(&mut self, error: Error) {
167        crate::warn::connect_failure("HttpClient could not open a connection", &error);
168    }
169    /// May a pool connection open now? `Ok(())` to open it, or
170    /// `Err(wait_ns)`: that connection asks again after the wait, at most
171    /// 5 s later. A refusal is not a failed connect: the backoff does not
172    /// grow. Called only for a connection due to open: never on a pass with
173    /// nothing to open.
174    ///
175    /// Lives here, not in a closure passed to [`HttpClient::poll_gated`],
176    /// so the gate can share this handler's [`Budget`](crate::limit::Budget) (the same one
177    /// [`on_response`](ClientHandler::on_response) charges for requests):
178    ///
179    /// ```text
180    /// fn connect_gate(&mut self) -> Result<(), u64> {
181    ///     self.budget
182    ///         .try_charge(&[Charge::new(self.connects, 1)], Class::Normal, self.now)
183    ///         .map(drop)
184    ///         .map_err(|l| l.wait_ns(self.now))
185    /// }
186    /// ```
187    ///
188    /// The default passes every connect. Implement it for a venue's connect
189    /// limit, with a bucket of the pool's own: venues count WebSocket
190    /// connects, not HTTP ones.
191    ///
192    /// # Errors
193    /// The nanoseconds to wait before that connection asks again, when the
194    /// gate refuses it now.
195    fn connect_gate(&mut self) -> std::result::Result<(), u64> {
196        Ok(())
197    }
198}
199
200enum Imp {
201    Native(Box<Native>),
202    #[cfg(feature = "curl")]
203    Curl(Box<crate::curl_client::CurlClient>),
204}
205
206/// A pooled HTTP client; see the module documentation.
207pub struct HttpClient {
208    imp: Imp,
209    next_id: u64,
210}
211
212impl HttpClient {
213    /// A client for `target` (its headers, such as the API key, go with every
214    /// request). Native connections start opening at once (with
215    /// [`HttpClientConfig::connect_gate`], from the first `poll_gated`); a
216    /// connection that cannot be opened is reported to
217    /// [`ClientHandler::on_connect_error`] from `poll` and retried.
218    ///
219    /// # Errors
220    /// The implementation is not built in, the configuration is invalid
221    /// (headers, TLS config, `connect_gate` with libcurl), or the driver or
222    /// libcurl fails to initialise. [`Error::Cpu`]: background connects
223    /// with no CPU for the connect helper (see
224    /// [`HttpClientConfig::background_connect`]).
225    pub fn new(
226        cfg: &HttpClientConfig,
227        target: Target,
228        tls: Arc<rustls::ClientConfig>,
229    ) -> Result<Self> {
230        if cfg.pool == 0 {
231            return Err(Error::Config("pool must be at least 1".into()));
232        }
233        let imp = match cfg.implementation {
234            HttpImpl::Native => {
235                // The default `on_connect_error` warning's writer starts in
236                // `Driver::new`, on the CPUs it chose for helper threads,
237                // rather than on the polling thread at the first failure.
238                Imp::Native(Box::new(Native::new(cfg, target, tls)?))
239            }
240            #[cfg(feature = "curl")]
241            HttpImpl::Curl => {
242                let _ = tls;
243                if cfg.connect_gate {
244                    return Err(Error::Config(
245                        "connect_gate: libcurl opens its own connections; use the native client"
246                            .into(),
247                    ));
248                }
249                if cfg.driver.quickack_check == crate::route::QuickAckCheck::Warn {
250                    crate::warn::prepare();
251                }
252                Imp::Curl(Box::new(crate::curl_client::CurlClient::new(cfg, &target)?))
253            }
254            #[cfg(not(feature = "curl"))]
255            HttpImpl::Curl => {
256                return Err(Error::Unsupported(
257                    "the libcurl client needs the `curl` feature",
258                ));
259            }
260        };
261        Ok(Self { imp, next_id: 0 })
262    }
263
264    /// The implementation in use.
265    #[must_use]
266    pub const fn implementation(&self) -> HttpImpl {
267        match self.imp {
268            Imp::Native(_) => HttpImpl::Native,
269            #[cfg(feature = "curl")]
270            Imp::Curl(_) => HttpImpl::Curl,
271        }
272    }
273
274    /// Connections the native pool failed to open since it was created (each
275    /// also went to [`ClientHandler::on_connect_error`]); 0 for libcurl, which
276    /// reports connect failures as request errors.
277    #[must_use]
278    pub fn connect_failures(&self) -> u64 {
279        match &self.imp {
280            Imp::Native(n) => n.connect_failures,
281            #[cfg(feature = "curl")]
282            Imp::Curl(_) => 0,
283        }
284    }
285
286    /// What the quick-ACK route check (`driver.quickack_check`, see
287    /// [`crate::route`]) found for each connection: the pool's open
288    /// connections (native), or the connection each libcurl handle last
289    /// used. Allocates: for diagnostics, not the request path.
290    #[must_use]
291    pub fn route_checks(&self) -> Vec<crate::route::RouteCheck> {
292        match &self.imp {
293            Imp::Native(n) => n
294                .slots
295                .iter()
296                .filter(|s| s.open)
297                .filter_map(|s| s.id.and_then(|id| n.driver.get(id)))
298                .map(|c| c.route().clone())
299                .collect(),
300            #[cfg(feature = "curl")]
301            Imp::Curl(c) => c.route_checks(),
302        }
303    }
304
305    /// Requests that can be sent now without [`Error::Busy`]: idle open
306    /// connections (native) or free handles (libcurl).
307    #[must_use]
308    pub fn idle(&self) -> usize {
309        match &self.imp {
310            Imp::Native(n) => n.idle(),
311            #[cfg(feature = "curl")]
312            Imp::Curl(c) => c.idle(),
313        }
314    }
315
316    /// Sends a request (`path` may carry a query). Returns its id.
317    ///
318    /// # Errors
319    /// [`Error::Busy`] when no connection or handle is free, or an invalid
320    /// request.
321    pub fn request(
322        &mut self,
323        method: &str,
324        path: &str,
325        headers: &[(&str, &str)],
326        body: &[u8],
327    ) -> Result<u64> {
328        let id = self.next_id;
329        match &mut self.imp {
330            Imp::Native(n) => n.send(id, |c| c.request(method, path, headers, body))?,
331            #[cfg(feature = "curl")]
332            Imp::Curl(c) => c.request(id, method, path, None, headers, body)?,
333        }
334        self.next_id += 1;
335        Ok(id)
336    }
337
338    /// Sends a signed request: `params`, `timestamp` (this host's UTC, ms)
339    /// and `signature` in the query string, as [`HttpConn::signed_request`].
340    /// Returns its id.
341    ///
342    /// # Errors
343    /// As [`HttpClient::request`], or `params` needs percent-encoding.
344    pub fn signed_request(
345        &mut self,
346        method: &str,
347        path: &str,
348        params: &str,
349        headers: &[(&str, &str)],
350        signer: &Signer,
351    ) -> Result<u64> {
352        let now_ms = u64::try_from(crate::clock::realtime_ns() / 1_000_000).unwrap_or(0);
353        self.signed_request_at(method, path, params, headers, signer, now_ms)
354    }
355
356    /// [`HttpClient::signed_request`] with the caller's timestamp, in ms
357    /// since 1970: a clock of the caller's choice, or replay time.
358    ///
359    /// # Errors
360    /// As [`HttpClient::signed_request`], or `path` has a query (put it in
361    /// `params`).
362    pub fn signed_request_at(
363        &mut self,
364        method: &str,
365        path: &str,
366        params: &str,
367        headers: &[(&str, &str)],
368        signer: &Signer,
369        timestamp_ms: u64,
370    ) -> Result<u64> {
371        let id = self.next_id;
372        match &mut self.imp {
373            Imp::Native(n) => {
374                n.send(id, |c| {
375                    c.signed_request_at(method, path, params, headers, signer, timestamp_ms)
376                })?;
377            }
378            #[cfg(feature = "curl")]
379            Imp::Curl(c) => {
380                crate::auth::validate_params(params)?;
381                if path.contains('?') {
382                    return Err(Error::Http("put query parameters in `params`".into()));
383                }
384                let mut q = Vec::with_capacity(params.len() + 128);
385                crate::auth::append_signed_params(&mut q, params, timestamp_ms, signer);
386                let q = String::from_utf8(q).map_err(|_| Error::Http("non-UTF-8 query".into()))?;
387                c.request(id, method, path, Some(&q), headers, &[])?;
388            }
389        }
390        self.next_id += 1;
391        Ok(id)
392    }
393
394    /// One non-blocking iteration: moves bytes, reports finished requests, and
395    /// replaces dropped connections. Returns requests finished.
396    ///
397    /// # Errors
398    /// The driver or libcurl failed as a whole (per-request failures go to
399    /// [`ClientHandler::on_error`]). [`Error::Config`] for a pool with
400    /// [`HttpClientConfig::connect_gate`]: use [`HttpClient::poll_gated`].
401    pub fn poll<H: ClientHandler>(&mut self, handler: &mut H) -> Result<usize> {
402        match &mut self.imp {
403            Imp::Native(n) => {
404                if n.gated {
405                    return Err(Error::Config(
406                        "connect_gate is set: poll with poll_gated".into(),
407                    ));
408                }
409                n.poll(handler, false)
410            }
411            #[cfg(feature = "curl")]
412            Imp::Curl(c) => c.poll(handler),
413        }
414    }
415
416    /// [`HttpClient::poll`], but a pool connect starts only when
417    /// [`ClientHandler::connect_gate`] passes it: a venue's limit on
418    /// connects per IP. The gate is the handler's, so it can share the
419    /// handler's [`Budget`](crate::limit::Budget) (see [`ClientHandler::connect_gate`]).
420    ///
421    /// # Errors
422    /// As [`HttpClient::poll`]; [`Error::Unsupported`] for libcurl, which
423    /// opens its own connections.
424    pub fn poll_gated<H: ClientHandler>(&mut self, handler: &mut H) -> Result<usize> {
425        match &mut self.imp {
426            Imp::Native(n) => n.poll(handler, true),
427            #[cfg(feature = "curl")]
428            Imp::Curl(_) => Err(Error::Unsupported(
429                "the libcurl client opens its own connections: no connect gate",
430            )),
431        }
432    }
433}
434
435struct Slot {
436    id: Option<ConnId>,
437    open: bool,
438    /// The client's request id in flight, and the connection's own.
439    request: Option<(u64, u64)>,
440    /// No connect before this (`CLOCK_MONOTONIC`).
441    retry_at_ns: i64,
442    /// Failed connects in a row.
443    failures: u32,
444    /// Closed during the last driver pass: its reconnect is not scheduled
445    /// yet.
446    closed: bool,
447}
448
449/// The first reconnect's delay is random, below this; the limit doubles per
450/// failed connect in a row.
451const REOPEN_NS: f64 = 100e6;
452/// The longest reconnect delay, and the longest wait between asking the
453/// gate.
454const REOPEN_MAX_NS: i64 = 5_000_000_000;
455
456struct Native {
457    driver: Driver<HttpConn>,
458    target: Target,
459    tls: Arc<rustls::ClientConfig>,
460    opts: HttpOptions,
461    background: bool,
462    slots: Vec<Slot>,
463    next: usize,
464    connect_failures: u64,
465    /// Every connect goes through the caller's gate.
466    gated: bool,
467    /// The reconnect jitter's state.
468    rng: u64,
469}
470
471impl Native {
472    fn new(cfg: &HttpClientConfig, target: Target, tls: Arc<rustls::ClientConfig>) -> Result<Self> {
473        let mut n = Self {
474            driver: Driver::new(cfg.driver.clone())?,
475            target,
476            tls,
477            opts: HttpOptions {
478                request_timeout_ms: cfg.request_timeout_ms,
479                max_in_flight: 1,
480                buf_size: cfg.buf_size,
481            },
482            background: cfg.background_connect,
483            slots: (0..cfg.pool)
484                .map(|_| Slot {
485                    id: None,
486                    open: false,
487                    request: None,
488                    retry_at_ns: i64::MIN,
489                    failures: 0,
490                    closed: false,
491                })
492                .collect(),
493            next: 0,
494            connect_failures: 0,
495            gated: cfg.connect_gate,
496            rng: crate::reconnect::rng_state(cfg.reconnect_seed)?,
497        };
498        // Configuration errors (headers, TLS config) fail here; connect
499        // failures go to the handler from `poll`, which retries them.
500        HttpDial::new(n.target.clone(), n.tls.clone(), n.opts)?;
501        // No CPU for the driver's connect helper (a pod with one CPU, without
502        // `inline_connect_fallback`): fail now, as every open would.
503        if n.background {
504            n.driver.background_ready()?;
505        }
506        // With a gate, the first opens wait for it too.
507        if !n.gated {
508            for i in 0..n.slots.len() {
509                let _ = n.open(i);
510            }
511        }
512        Ok(n)
513    }
514
515    fn open(&mut self, i: usize) -> Result<()> {
516        let dial = HttpDial::new(self.target.clone(), self.tls.clone(), self.opts)?;
517        let id = if self.background {
518            self.driver.connect_background(dial)?
519        } else {
520            self.driver.connect(dial)?
521        };
522        self.slots[i].id = Some(id);
523        Ok(())
524    }
525
526    fn idle(&self) -> usize {
527        self.slots
528            .iter()
529            .filter(|s| s.open && s.request.is_none())
530            .count()
531    }
532
533    fn send(&mut self, id: u64, f: impl FnOnce(&mut HttpConn) -> Result<u64>) -> Result<()> {
534        let n = self.slots.len();
535        let Some(i) = (0..n)
536            .map(|k| (self.next + k) % n)
537            .find(|&i| self.slots[i].open && self.slots[i].request.is_none())
538        else {
539            return Err(Error::Busy("no idle connection in the pool"));
540        };
541        let mut conn = self.slots[i]
542            .id
543            .and_then(|c| self.driver.get_mut(c))
544            .ok_or(Error::Closed)?;
545        let on_conn = f(conn.inner())?;
546        self.slots[i].request = Some((id, on_conn));
547        self.next = (i + 1) % n;
548        Ok(())
549    }
550
551    fn poll<H: ClientHandler>(&mut self, handler: &mut H, gated: bool) -> Result<usize> {
552        let mut a = Adapter {
553            h: handler,
554            slots: &mut self.slots,
555            done: 0,
556            connect_failures: 0,
557        };
558        self.driver.poll(&mut a)?;
559        let done = a.done;
560        self.connect_failures += a.connect_failures;
561        if self.slots.iter().any(|s| s.id.is_none()) {
562            self.reopen(handler, gated);
563        }
564        Ok(done)
565    }
566
567    /// Replaces dropped connections: each after a random delay, the limit
568    /// doubling per failed connect in a row, and each through
569    /// [`ClientHandler::connect_gate`] when `gated`.
570    #[cold]
571    fn reopen<H: ClientHandler>(&mut self, handler: &mut H, gated: bool) {
572        let now = monotonic_ns();
573        let n = self.slots.len();
574        for i in 0..n {
575            if std::mem::take(&mut self.slots[i].closed) {
576                let delay = self.reopen_delay_ns(self.slots[i].failures);
577                self.slots[i].retry_at_ns = now.saturating_add(delay);
578            }
579        }
580        for i in 0..n {
581            if self.slots[i].id.is_some() || now < self.slots[i].retry_at_ns {
582                continue;
583            }
584            let gate = if gated {
585                handler.connect_gate()
586            } else {
587                Ok(())
588            };
589            if let Err(wait_ns) = gate {
590                let wait = i64::try_from(wait_ns)
591                    .unwrap_or(i64::MAX)
592                    .min(REOPEN_MAX_NS);
593                self.slots[i].retry_at_ns = now.saturating_add(wait);
594                continue;
595            }
596            // A refused or unroutable connect fails here at once: report it
597            // and retry later, as for one that fails while connecting.
598            if let Err(e) = self.open(i) {
599                self.connect_failures += 1;
600                let failures = self.slots[i].failures.saturating_add(1);
601                self.slots[i].failures = failures;
602                let delay = self.reopen_delay_ns(failures);
603                self.slots[i].retry_at_ns = now.saturating_add(delay);
604                handler.on_connect_error(e);
605            }
606        }
607    }
608
609    /// A random delay below 100 ms × 2^`failures`, at most 5 s ("full
610    /// jitter": connections that drop together spread out).
611    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
612    fn reopen_delay_ns(&mut self, failures: u32) -> i64 {
613        let limit = (REOPEN_NS * f64::from(1u32 << failures.min(6))).min(REOPEN_MAX_NS as f64);
614        (crate::reconnect::unit(&mut self.rng) * limit) as i64
615    }
616}
617
618/// Maps driver callbacks onto pool slots and client request ids.
619struct Adapter<'a, H> {
620    h: &'a mut H,
621    slots: &'a mut [Slot],
622    done: usize,
623    connect_failures: u64,
624}
625
626impl<H> Adapter<'_, H> {
627    fn slot(&mut self, id: ConnId) -> Option<&mut Slot> {
628        self.slots.iter_mut().find(|s| s.id == Some(id))
629    }
630}
631
632impl<H: ClientHandler> Handler for Adapter<'_, H> {
633    fn on_open(&mut self, id: ConnId) {
634        if let Some(s) = self.slot(id) {
635            s.open = true;
636            s.failures = 0;
637        }
638    }
639
640    fn on_closed(&mut self, id: ConnId, error: Error) {
641        let Some(s) = self.slot(id) else {
642            return;
643        };
644        s.id = None;
645        s.closed = true;
646        let was_open = std::mem::replace(&mut s.open, false);
647        if !was_open {
648            s.failures = s.failures.saturating_add(1);
649        }
650        if let Some((r, on_conn)) = s.request.take() {
651            self.done += 1;
652            // The connection names the first request the kernel never
653            // wholly took; anything else may have reached the server.
654            let (sent, error) = match error {
655                Error::Unanswered(u) => {
656                    let sent = match u.first_unsent {
657                        Some(first) if first <= on_conn => Sent::No,
658                        _ => Sent::Maybe,
659                    };
660                    (sent, u.cause)
661                }
662                e => (Sent::Maybe, e),
663            };
664            self.h.on_error(r, sent, error);
665        } else if !was_open {
666            // It never opened: a failed connect.
667            self.connect_failures += 1;
668            self.h.on_connect_error(error);
669        }
670    }
671}
672
673impl<H: ClientHandler> HttpHandler for Adapter<'_, H> {
674    fn on_response(&mut self, conn: ConnId, _request: u64, response: &Response<'_>, _: &RxMeta) {
675        if let Some((r, _)) = self.slot(conn).and_then(|s| s.request.take()) {
676            self.done += 1;
677            self.h.on_response(r, response);
678        }
679    }
680}