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}