hclient_rt/caps.rs
1use std::error::Error as StdError;
2use std::fmt::Display;
3use std::future::Future;
4use std::net::{IpAddr, SocketAddr};
5use std::time::Duration;
6
7/// The shape is deliberately copied from `hyper::rt::Executor`: generic
8/// over the future, zero bounds in the declaration. `Send` is added by the
9/// `impl`, not the trait, so single-threaded runtimes can implement it
10/// honestly.
11pub trait Spawn<F: Future<Output = ()>> {
12 fn spawn(&self, f: F);
13}
14
15/// Socket options are applied in hclient **once**, on the `socket2::Socket`,
16/// and the runtime only adopts the descriptor (`TcpAdoptStd`). Otherwise
17/// every runtime crate would rewrite this whole rigmarole again.
18///
19/// # `default()` is all-off, and that was re-decided rather than inherited
20///
21/// Nagle's algorithm costs the head of a `Native` TLS exchange **41 ms** on
22/// loopback — measured from the server's side of the wire in
23/// `hclient-native`'s `tests/nagle_cost.rs`, and 0.9 ms with `nodelay` set.
24/// Every field here stays `false`/`None` anyway, for two reasons that are
25/// not caution:
26///
27/// - **This is a socket seam, and it does not know who is writing.** The
28/// 41 ms is the write-write-read pattern of a request over TLS meeting a
29/// peer's delayed ACK. A protocol that streams one way is exactly the one
30/// Nagle helps, and a default here would impose one caller's protocol on
31/// every other caller of the trait.
32/// - **A set option is a refusal, not a preference.**
33/// [`TcpOpts::reject_unsupported`] fails the connect on a runtime whose
34/// [`TcpConnect::APPLIES`] does not cover it, and that default is `NONE`.
35/// Turning a field on here would turn every connect on a backend that
36/// forgot to declare `APPLIES` into an `Unsupported` error for an option
37/// its caller never mentioned — a performance fix aimed straight at the
38/// implementors the `NONE` default was written to protect.
39///
40/// So the opinion lives where the protocol is: `hclient_native::Native::new`
41/// asks for `nodelay`, and asks only where the runtime declares it applies
42/// it.
43#[derive(Debug, Clone, Default)]
44pub struct TcpOpts {
45 /// `TCP_NODELAY` — Nagle's algorithm off. See the type's own doc for
46 /// why `default()` leaves it `false` and who turns it on.
47 pub nodelay: bool,
48 /// `TCP_KEEPIDLE` — how long a connection may be idle before the
49 /// first probe.
50 ///
51 /// **One setting in three parts, with
52 /// [`keepalive_interval`](Self::keepalive_interval) and
53 /// [`keepalive_retries`](Self::keepalive_retries).** Setting *any* of
54 /// the three turns `SO_KEEPALIVE` on; each part left `None` keeps the
55 /// operating system's value for it. That is `socket2::TcpKeepalive`'s own shape
56 /// and it is stated here because the field names do not say it: a
57 /// caller who sets only the interval has switched keepalive on, with
58 /// the OS's idle time.
59 pub keepalive: Option<Duration>,
60 /// `TCP_KEEPINTVL` — the gap between probes once they have started.
61 ///
62 /// Worth setting with [`keepalive`](Self::keepalive) rather than
63 /// instead of it: the idle time decides *when a dead peer starts being
64 /// noticed* and this decides *how fast the noticing then goes*, and
65 /// Linux's defaults are 7200 s and 75 s, so an untouched idle time
66 /// makes the interval nearly irrelevant.
67 pub keepalive_interval: Option<Duration>,
68 /// `TCP_KEEPCNT` — how many unanswered probes end the connection.
69 pub keepalive_retries: Option<u32>,
70 /// `SO_BINDTODEVICE` — the interface this socket must use, by name.
71 ///
72 /// Not [`local_address`](Self::local_address) under another name: an
73 /// address binds the *source address*, and the kernel still routes by
74 /// its table, so a request can leave through a different interface
75 /// that happens to hold the same address. This binds the **interface**,
76 /// which is what a caller on a multi-homed host or inside a VRF
77 /// actually means. Linux, Android and Fuchsia only — see
78 /// [`TcpOptsSupport`], which is where a runtime says so per target.
79 ///
80 /// A `String` rather than a `&'static str` because an interface name
81 /// is configuration a caller reads at run time, and rather than bytes
82 /// because every interface name on every platform that has this option
83 /// is ASCII.
84 pub bind_device: Option<String>,
85 /// `TCP_USER_TIMEOUT` — how long transmitted data may stay
86 /// unacknowledged before the connection is dropped.
87 ///
88 /// **The one option here that catches a peer which vanished
89 /// mid-transfer**, where keepalive only catches an *idle* one: probes
90 /// are sent when nothing is in flight, so a connection with unsent
91 /// acknowledgements sits in retransmission for minutes with keepalive
92 /// never firing. Linux, Android, Fuchsia and Cygwin only.
93 ///
94 /// It overlaps `Timeouts::between_bytes` and does not replace it: this
95 /// is the kernel's, applies to a socket rather than to an exchange, and
96 /// is the only one of the two that a build with no `Client` above it
97 /// can reach.
98 pub user_timeout: Option<Duration>,
99 pub local_address: Option<IpAddr>,
100 pub send_buffer_size: Option<usize>,
101 pub recv_buffer_size: Option<usize>,
102 pub reuse_address: bool,
103}
104
105/// Which of [`TcpOpts`]' six fields a runtime can actually apply.
106///
107/// One `bool` per field of `TcpOpts`, not a count and not a bitflags crate:
108/// the error a caller gets has to name the option it asked for, and a
109/// field-per-field mirror is the only shape that can.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct TcpOptsSupport {
112 pub nodelay: bool,
113 pub keepalive: bool,
114 pub keepalive_interval: bool,
115 pub keepalive_retries: bool,
116 /// `SO_BINDTODEVICE`, which exists on Linux, Android and Fuchsia and
117 /// nowhere else — so a runtime that sets this **must** decide it per
118 /// target rather than in one constant. `TcpOptsSupport::ALL` is still
119 /// literally every field, and is therefore no longer a value any real
120 /// runtime can claim on every platform it builds for.
121 pub bind_device: bool,
122 /// `TCP_USER_TIMEOUT`, Linux/Android/Fuchsia/Cygwin — the same
123 /// per-target rule as [`bind_device`](Self::bind_device).
124 pub user_timeout: bool,
125 pub local_address: bool,
126 pub send_buffer_size: bool,
127 pub recv_buffer_size: bool,
128 pub reuse_address: bool,
129}
130
131impl TcpOptsSupport {
132 /// Everything applied — what a runtime that hands the whole set to a
133 /// `socket2::Socket` says. Both shipped runtimes do exactly that.
134 pub const ALL: Self = Self {
135 nodelay: true,
136 keepalive: true,
137 keepalive_interval: true,
138 keepalive_retries: true,
139 bind_device: true,
140 user_timeout: true,
141 local_address: true,
142 send_buffer_size: true,
143 recv_buffer_size: true,
144 reuse_address: true,
145 };
146 /// Nothing applied — the default for [`TcpConnect::APPLIES`], and the
147 /// conservative base a runtime turns individual fields on from.
148 pub const NONE: Self = Self {
149 nodelay: false,
150 keepalive: false,
151 keepalive_interval: false,
152 keepalive_retries: false,
153 bind_device: false,
154 user_timeout: false,
155 local_address: false,
156 send_buffer_size: false,
157 recv_buffer_size: false,
158 reuse_address: false,
159 };
160}
161
162/// The caller set socket options this runtime cannot apply.
163///
164/// Carried inside an [`std::io::Error`] with
165/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported) by
166/// [`TcpOpts::reject_unsupported`], and reachable again through
167/// `io::Error::get_ref().downcast_ref()`.
168///
169/// `Display` names **every** offending option, not just the first: a caller
170/// who set two unappliable options and fixed the one the message mentioned
171/// would otherwise get a second, identical-looking failure.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct UnsupportedTcpOpts {
174 /// `true` where the caller asked for an option the runtime does not
175 /// apply — i.e. set in [`TcpOpts`] and absent from
176 /// [`TcpConnect::APPLIES`].
177 missing: TcpOptsSupport,
178}
179
180impl UnsupportedTcpOpts {
181 /// The offending option names, in [`TcpOpts`]' own field order.
182 pub fn names(&self) -> impl Iterator<Item = &'static str> {
183 let m = self.missing;
184 [
185 ("nodelay", m.nodelay),
186 ("keepalive", m.keepalive),
187 ("keepalive_interval", m.keepalive_interval),
188 ("keepalive_retries", m.keepalive_retries),
189 ("bind_device", m.bind_device),
190 ("user_timeout", m.user_timeout),
191 ("local_address", m.local_address),
192 ("send_buffer_size", m.send_buffer_size),
193 ("recv_buffer_size", m.recv_buffer_size),
194 ("reuse_address", m.reuse_address),
195 ]
196 .into_iter()
197 .filter_map(|(name, missing)| missing.then_some(name))
198 }
199}
200
201// Hand-written rather than `thiserror`: the message is a computed list, so
202// the derive would buy nothing, and this way the names are written straight
203// into the formatter instead of through an intermediate `String`.
204impl Display for UnsupportedTcpOpts {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 f.write_str(
207 "this runtime cannot apply these TCP socket options, and does not ignore them:",
208 )?;
209 for (i, name) in self.names().enumerate() {
210 f.write_str(if i > 0 { ", " } else { " " })?;
211 f.write_str(name)?;
212 }
213 // Where the claim came from, because half the readers of this
214 // message are on the wrong side of it. `TcpConnect::APPLIES`
215 // defaults to `NONE`, so a runtime that *does* apply an option and
216 // forgot the line refuses it here — which has happened once in
217 // this workspace already (`TokioHandle`, found by measurement).
218 // Naming the option alone sends that author looking at their
219 // `connect` body, where the code is correct and the bug is not.
220 f.write_str(" (a runtime that does apply one declares it in TcpConnect::APPLIES)")
221 }
222}
223
224impl StdError for UnsupportedTcpOpts {}
225
226impl TcpOpts {
227 /// Fail when the caller set an option `can` says this runtime does not
228 /// apply — the one sanctioned answer to an option a runtime cannot
229 /// honour, since silently ignoring it is not one.
230 ///
231 /// Only fields that are actually *set* can offend: [`TcpOpts::default`]
232 /// is all-off, so even a runtime with [`TcpOptsSupport::NONE`] still
233 /// serves every caller that never asked for anything.
234 ///
235 /// A runtime whose [`TcpConnect::APPLIES`] is [`TcpOptsSupport::ALL`]
236 /// need not call this at all — the call is a no-op by construction,
237 /// which `reject_unsupported_is_a_no_op_against_all` pins.
238 pub fn reject_unsupported(&self, can: TcpOptsSupport) -> std::io::Result<()> {
239 let missing = TcpOptsSupport {
240 nodelay: self.nodelay && !can.nodelay,
241 keepalive: self.keepalive.is_some() && !can.keepalive,
242 keepalive_interval: self.keepalive_interval.is_some() && !can.keepalive_interval,
243 keepalive_retries: self.keepalive_retries.is_some() && !can.keepalive_retries,
244 bind_device: self.bind_device.is_some() && !can.bind_device,
245 user_timeout: self.user_timeout.is_some() && !can.user_timeout,
246 local_address: self.local_address.is_some() && !can.local_address,
247 send_buffer_size: self.send_buffer_size.is_some() && !can.send_buffer_size,
248 recv_buffer_size: self.recv_buffer_size.is_some() && !can.recv_buffer_size,
249 reuse_address: self.reuse_address && !can.reuse_address,
250 };
251 if missing == TcpOptsSupport::NONE {
252 return Ok(());
253 }
254 Err(std::io::Error::new(
255 std::io::ErrorKind::Unsupported,
256 UnsupportedTcpOpts { missing },
257 ))
258 }
259}
260
261pub trait TcpConnect {
262 type Stream: hyper::rt::Read + hyper::rt::Write + Unpin;
263
264 /// Which [`TcpOpts`] fields this runtime actually applies.
265 ///
266 /// # Why the default is `NONE` and not `ALL`
267 ///
268 /// A default is a claim made by silence, and it must never be stronger
269 /// than the truth — the rule written down on
270 /// [`CancelSupport::None`](hclient_core::CancelSupport::None) and
271 /// learned from `RedirectSupport::Transparent`. `ALL` would make a
272 /// backend that forgot the line claim it applies every option; `NONE`
273 /// makes it understate itself, so the worst case is one refused connect
274 /// too many rather than an option dropped on the floor without a trace.
275 const APPLIES: TcpOptsSupport = TcpOptsSupport::NONE;
276
277 /// # The options are not optional
278 ///
279 /// A runtime that cannot apply an option the caller set **must fail
280 /// this call** — [`TcpOpts::reject_unsupported`] is the shared way to
281 /// do it, and the error it builds names the option. Ignoring it is not
282 /// an available answer: `connect` returns `io::Result<Self::Stream>`
283 /// and nothing else, so an option quietly dropped here is dropped
284 /// without a trace anywhere in the stack.
285 ///
286 /// On platforms with file descriptors the whole set is applied outside
287 /// the runtime, on a `socket2::Socket`, and the runtime only adopts the
288 /// finished socket ([`TcpAdoptStd`]) — which is why both shipped
289 /// runtimes declare [`TcpOptsSupport::ALL`] and never have to refuse
290 /// anything.
291 fn connect(
292 &self,
293 addr: SocketAddr,
294 opts: &TcpOpts,
295 ) -> impl Future<Output = std::io::Result<Self::Stream>>;
296
297 /// Whether [`connect_unix`](Self::connect_unix) does anything.
298 ///
299 /// [`APPLIES`](Self::APPLIES)' shape, and defaulted the same way and
300 /// for the same reason: a claim made by silence must never be stronger
301 /// than the truth. A runtime that says nothing here refuses the
302 /// setting, where one that over-claimed would fail every connect at
303 /// the socket instead of at the call that asked.
304 ///
305 /// It is a `const` rather than something the connect discovers,
306 /// because the answer is a property of the runtime and the target and
307 /// a caller should learn it at configuration rather than on the wire —
308 /// which is what lets `hclient_native::Native::unix_socket` refuse.
309 const SUPPORTS_UNIX: bool = false;
310
311 /// Connect to a Unix-domain socket at `path`.
312 ///
313 /// # Why it is here rather than on a seam of its own
314 ///
315 /// Because a seam of its own could not be reached. `Native`'s IO type
316 /// **is** [`Self::Stream`], so a second trait would have to produce
317 /// the same associated type — at which point it is this trait with an
318 /// extra method — and putting `R: UnixConnect` on `Native` would tax
319 /// every runtime that has no file descriptors. The `fn`-pointer trick
320 /// that keeps `Spawn` off `Native`'s signature does not work here:
321 /// `spawn` returns `()` where this returns a future, and boxing it
322 /// would drop auto traits (spec amendment C1).
323 ///
324 /// So it is a defaulted method on the seam that already exists —
325 /// `TlsConnect::reports_alpn`'s shape, `applies_ech`'s and
326 /// `TlsIdentity::presents_client_certs`': a constant defaulted to the
327 /// understating value, read by the layer above to decide whether to
328 /// **ask**.
329 ///
330 /// # No `TcpOpts`
331 ///
332 /// Not an omission: every field of [`TcpOpts`] is a TCP or IP socket
333 /// option, and `AF_UNIX` has none of them — no Nagle, no keepalive, no
334 /// source address, no interface. A parameter that could only ever be
335 /// ignored is worse than no parameter.
336 ///
337 /// The default is a refusal rather than a panic, and the error carries
338 /// [`std::io::ErrorKind::Unsupported`] so a caller who reached it
339 /// through some path that skipped
340 /// [`SUPPORTS_UNIX`](Self::SUPPORTS_UNIX) still gets an answer rather
341 /// than an abort.
342 fn connect_unix(
343 &self,
344 path: &std::path::Path,
345 ) -> impl Future<Output = std::io::Result<Self::Stream>> {
346 let _ = path;
347 async {
348 Err(std::io::Error::new(
349 std::io::ErrorKind::Unsupported,
350 UnixSocketsUnsupported,
351 ))
352 }
353 }
354}
355
356/// A runtime that declares no Unix-domain support was asked for a
357/// connection to one.
358///
359/// Reachable only past [`TcpConnect::SUPPORTS_UNIX`], which
360/// `hclient_native::Native::unix_socket` checks at the call that
361/// configures it — so a caller normally meets the refusal where they
362/// wrote the path, not on the wire.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
364#[error("this runtime does not connect to Unix-domain sockets")]
365pub struct UnixSocketsUnsupported;
366
367/// On platforms with file descriptors, the whole set of socket options is
368/// applied outside the runtime, and the runtime only adopts the finished
369/// socket.
370pub trait TcpAdoptStd: TcpConnect {
371 fn adopt(&self, std: std::net::TcpStream) -> std::io::Result<Self::Stream>;
372}
373
374/// A separate trait, not a method: `getaddrinfo` blocks, and wasm and
375/// embedded have no blocking pool at all. The absence of the capability
376/// must be a compile error, not an `unimplemented!()` in the runtime.
377///
378/// **The one place in the whole project where we declare `Send` ourselves**,
379/// and here it's honest: both `tokio::task::spawn_blocking` and
380/// `blocking::unblock` require `Send + 'static`, and the `Blocking`
381/// capability doesn't exist on wasm at all — there's nothing for it to
382/// infect. The justification is `amendment-C5` (`docs/exceptions.md`), an
383/// amendment separate from C1/C2: those two are about erasing auto-traits in `dyn
384/// Trait` on the `Client -> Transport` path, whereas here the bound is
385/// declared directly in the signature of a capability trait that simply
386/// doesn't exist on wasm.
387///
388/// The bounds live in `where`, not in the generic parameter list `fn
389/// run<T: Send + …>`, so each one can carry its own `send-bound-exception`
390/// marker on its own line: the CI `no-declared-send` job matches bound
391/// declarations line by line, and a single shared comment after the
392/// generic list wouldn't cover it.
393///
394/// Two distinct failure modes of `f` are not conflated into one channel:
395///
396/// - A panic in `f` is a bug in the calling code. It must be re-raised as a
397/// panic (`std::panic::resume_unwind`, with the original payload), not
398/// quietly turned into a value that can be `?`-propagated — otherwise the
399/// implementation hides a defect in the caller's code behind a `Result`.
400/// - The background thread pool going away (for example, the runtime
401/// shutting down while a task is still queued and hasn't started
402/// running) is not a bug in the calling code, but an ordinary runtime
403/// lifecycle event. The implementation must return [`Cancelled`], not
404/// panic: a library panicking on a normal (if rare) runtime-shutdown
405/// scenario would contradict the rest of the project ("no silent
406/// no-ops... typed error, never a discarded value" — the same principle
407/// applied here, just to failure instead of success).
408pub trait Blocking {
409 fn run<T, F>(&self, f: F) -> impl Future<Output = Result<T, Cancelled>>
410 where
411 T: Send + 'static, // send-bound-exception: amendment-C5
412 F: FnOnce() -> T + Send + 'static; // send-bound-exception: amendment-C5
413}
414
415/// The background thread pool that `Blocking::run` was supposed to run on
416/// went away before the task got to start — for example, the runtime is
417/// shutting down while the task is still queued. No payload: this is not a
418/// failure of `f` (`f` never ran at all), but a signal from the runtime
419/// that there will be no result.
420///
421/// A panic in `f`, by contrast, does NOT become `Cancelled` — it is
422/// re-raised as a panic by the `Blocking` implementation, see the trait's
423/// doc comment.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
425#[error("blocking task pool went away before the work started")]
426pub struct Cancelled;
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use std::pin::Pin;
432 use std::task::Context;
433 use std::task::Poll;
434
435 #[test]
436 fn tcp_opts_default_is_conservative() {
437 // All SIX fields, not four: a hand-written `Default` that set
438 // `send_buffer_size`/`recv_buffer_size` to `Some(1 << 20)` would
439 // pass this test unnoticed if only the other four were checked
440 // if only the other four were checked. `#[derive(Default)]` gives
441 // `None` by construction, but the test's name promises the
442 // whole struct — so the test must check the whole struct.
443 let o = TcpOpts::default();
444 // "The user turns nodelay on, not us" is how this line read until
445 // the 41 ms was measured, and it was half right: the user, or the
446 // transport that knows what protocol is about to be spoken —
447 // `hclient_native::Native::new`, which asks for it and only where
448 // `TcpConnect::APPLIES` says the runtime applies it. Not this
449 // seam, which cannot know either thing, and where a `true` would
450 // become a refused connect on every backend that left `APPLIES`
451 // at its default. See the type's doc.
452 assert!(!o.nodelay, "the seam has no opinion about who is writing");
453 assert!(o.keepalive.is_none());
454 assert!(o.local_address.is_none());
455 assert!(o.send_buffer_size.is_none());
456 assert!(o.recv_buffer_size.is_none());
457 assert!(!o.reuse_address);
458 }
459
460 /// Every field of `TcpOpts` set to something a runtime would have to
461 /// act on, paired with the `TcpOptsSupport` field that covers it.
462 ///
463 /// Named for a count until the count changed, which is why it is not
464 /// named for one any more: the pairing is what the tests below read,
465 /// and a name carrying a number goes stale the first time the struct
466 /// grows.
467 fn every_field_set() -> TcpOpts {
468 TcpOpts {
469 nodelay: true,
470 keepalive: Some(Duration::from_secs(30)),
471 keepalive_interval: Some(Duration::from_secs(5)),
472 keepalive_retries: Some(3),
473 bind_device: Some("lo".to_owned()),
474 user_timeout: Some(Duration::from_secs(20)),
475 local_address: Some(IpAddr::from([127, 0, 0, 1])),
476 send_buffer_size: Some(4096),
477 recv_buffer_size: Some(4096),
478 reuse_address: true,
479 }
480 }
481
482 /// Every option name, in `TcpOpts`' own field order — which is the
483 /// order `UnsupportedTcpOpts::names` walks, so this list going stale
484 /// is the same failure as that one going stale.
485 ///
486 /// The length is inferred rather than written: it was `[&str; 6]`, and
487 /// a number in a type is one more thing to remember when the struct
488 /// grows. It grew.
489 const NAMES: &[&str] = &[
490 "nodelay",
491 "keepalive",
492 "keepalive_interval",
493 "keepalive_retries",
494 "bind_device",
495 "user_timeout",
496 "local_address",
497 "send_buffer_size",
498 "recv_buffer_size",
499 "reuse_address",
500 ];
501
502 /// `TcpOptsSupport::ALL` with exactly one field turned off, in the same
503 /// order as `NAMES` — so a test can walk both together and check that
504 /// the error names the one option that was withheld.
505 fn all_but(i: usize) -> TcpOptsSupport {
506 let mut can = TcpOptsSupport::ALL;
507 match i {
508 0 => can.nodelay = false,
509 1 => can.keepalive = false,
510 2 => can.keepalive_interval = false,
511 3 => can.keepalive_retries = false,
512 4 => can.bind_device = false,
513 5 => can.user_timeout = false,
514 6 => can.local_address = false,
515 7 => can.send_buffer_size = false,
516 8 => can.recv_buffer_size = false,
517 9 => can.reuse_address = false,
518 _ => unreachable!("one arm per NAMES entry"),
519 }
520 can
521 }
522
523 #[test]
524 fn reject_unsupported_is_a_no_op_against_all() {
525 // The claim `TcpConnect::APPLIES`' doc makes about the two shipped
526 // runtimes: they apply the whole set, so the check they don't call
527 // could not have refused anything anyway.
528 assert!(
529 every_field_set()
530 .reject_unsupported(TcpOptsSupport::ALL)
531 .is_ok()
532 );
533 }
534
535 #[test]
536 fn a_runtime_that_applies_nothing_still_serves_a_caller_that_asked_for_nothing() {
537 // Why `TcpOptsSupport::NONE` is a usable default and not a brick
538 // wall: `TcpOpts::default()` sets nothing, and that is what
539 // `Native` passes unless the caller called `tcp_opts`.
540 assert!(
541 TcpOpts::default()
542 .reject_unsupported(TcpOptsSupport::NONE)
543 .is_ok()
544 );
545 }
546
547 #[test]
548 fn each_unappliable_option_is_named_on_its_own() {
549 // One case per option, not one case: an implementation that named
550 // a fixed option, or the first one it found, would pass a test
551 // that only ever withheld `nodelay`.
552 //
553 // **Compared as data and not as substrings of the message**, which
554 // is what the neighbour above already does and this one did not.
555 // It worked while no two names shared a prefix; `keepalive` and
556 // `keepalive_interval` ended that, and the failure was the test
557 // reporting that a withheld `keepalive_interval` had *also* named
558 // `keepalive` — which the message never did.
559 for (i, name) in NAMES.iter().enumerate() {
560 let err = every_field_set()
561 .reject_unsupported(all_but(i))
562 .expect_err("the one option this runtime cannot apply was set");
563 let named: Vec<&str> = err
564 .get_ref()
565 .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
566 .expect("typed payload")
567 .names()
568 .collect();
569 assert_eq!(
570 named,
571 [*name],
572 "a withheld {name} must be the only option named"
573 );
574 // And the message really does carry it, since that is what a
575 // caller who does not downcast will read.
576 assert!(err.to_string().contains(name), "{err}");
577 }
578 }
579
580 #[test]
581 fn the_message_names_the_constant_an_implementor_would_have_to_change() {
582 // The other audience for this error is the backend author whose
583 // `connect` applies the option perfectly well and whose `APPLIES`
584 // line is missing — `TokioHandle`, in this workspace, found by
585 // measurement rather than by reading. The option's name sends
586 // them to their `connect` body; the constant's name sends them to
587 // the defect.
588 let err = every_field_set()
589 .reject_unsupported(all_but(0))
590 .expect_err("nodelay was withheld");
591 let msg = err.to_string();
592 assert!(msg.contains("TcpConnect::APPLIES"), "{msg}");
593 }
594
595 #[test]
596 fn all_offending_options_are_named_not_only_the_first() {
597 let err = every_field_set()
598 .reject_unsupported(TcpOptsSupport::NONE)
599 .expect_err("nothing can be applied and everything was asked for");
600 let msg = err.to_string();
601 for name in NAMES {
602 assert!(msg.contains(name), "{name} missing from: {msg}");
603 }
604 }
605
606 #[test]
607 fn the_error_is_unsupported_and_carries_a_typed_payload() {
608 // `ErrorKind::Unsupported` rather than `Other`, and the names
609 // reachable as data rather than only by parsing the message —
610 // otherwise a caller wanting to react per-option has to scrape
611 // `Display`.
612 // Indexed through `NAMES` rather than by a literal, so that a
613 // field inserted above this one moves the index and the expected
614 // name together. It was `all_but(2)` against `["local_address"]`
615 // and four fields arrived above it.
616 const I: usize = 6;
617 let err = every_field_set()
618 .reject_unsupported(all_but(I))
619 .expect_err("one option was withheld");
620 assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
621 let payload = err
622 .get_ref()
623 .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
624 .expect("the typed payload survives the trip through io::Error");
625 assert_eq!(payload.names().collect::<Vec<_>>(), [NAMES[I]]);
626 assert_eq!(NAMES[I], "local_address", "the index still names it");
627 }
628
629 #[test]
630 fn an_option_left_unset_is_not_an_offence_even_when_unsupported() {
631 // The check is about what the caller ASKED for, not about what the
632 // runtime lacks: a runtime that applies nothing owes nothing to a
633 // caller who set nothing. Without this distinction
634 // `TcpOptsSupport::NONE` would refuse every connect.
635 let opts = TcpOpts {
636 nodelay: true,
637 ..TcpOpts::default()
638 };
639 let err = opts
640 .reject_unsupported(TcpOptsSupport::NONE)
641 .expect_err("nodelay was set and cannot be applied");
642 let payload = err
643 .get_ref()
644 .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
645 .expect("typed payload");
646 assert_eq!(payload.names().collect::<Vec<_>>(), ["nodelay"], "{err}");
647 }
648
649 #[test]
650 fn a_runtime_that_declares_nothing_applies_nothing() {
651 // The default is a claim made by silence, and this is the only
652 // test that reads it. All three shipped runtimes declare
653 // `APPLIES` explicitly — tokio and smol `ALL`, embassy its own
654 // two-of-six — so flipping the default to `ALL` passes the whole
655 // workspace suite otherwise: 878/878, measured.
656 // The rule it protects is that a backend which forgets the line
657 // must understate itself, so the worst case is one refused
658 // connect too many rather than an option dropped on the floor
659 // without a trace.
660 struct Forgetful;
661 // Never constructed: it exists only so `Forgetful` can satisfy
662 // the associated type without a runtime behind it.
663 struct NeverIo;
664 impl hyper::rt::Read for NeverIo {
665 fn poll_read(
666 self: Pin<&mut Self>,
667 _: &mut Context<'_>,
668 _: hyper::rt::ReadBufCursor<'_>,
669 ) -> Poll<std::io::Result<()>> {
670 unreachable!("this runtime never connects")
671 }
672 }
673 impl hyper::rt::Write for NeverIo {
674 fn poll_write(
675 self: Pin<&mut Self>,
676 _: &mut Context<'_>,
677 _: &[u8],
678 ) -> Poll<std::io::Result<usize>> {
679 unreachable!("this runtime never connects")
680 }
681 fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<std::io::Result<()>> {
682 unreachable!("this runtime never connects")
683 }
684 fn poll_shutdown(
685 self: Pin<&mut Self>,
686 _: &mut Context<'_>,
687 ) -> Poll<std::io::Result<()>> {
688 unreachable!("this runtime never connects")
689 }
690 }
691 impl TcpConnect for Forgetful {
692 type Stream = NeverIo;
693 // No `APPLIES` line, deliberately — that absence is the
694 // subject of this test.
695 async fn connect(&self, _: SocketAddr, _: &TcpOpts) -> std::io::Result<NeverIo> {
696 unreachable!("this runtime never connects")
697 }
698 }
699
700 assert_eq!(
701 <Forgetful as TcpConnect>::APPLIES,
702 TcpOptsSupport::NONE,
703 "a runtime that declares nothing must not claim to apply anything"
704 );
705 // And the consequence, not only the constant: a caller who asks
706 // such a runtime for all six gets all six refused by name, rather
707 // than silently honoured on paper.
708 let err = every_field_set()
709 .reject_unsupported(<Forgetful as TcpConnect>::APPLIES)
710 .expect_err("a runtime that applies nothing must refuse everything asked of it");
711 let payload = err
712 .get_ref()
713 .and_then(|e| e.downcast_ref::<UnsupportedTcpOpts>())
714 .expect("typed payload");
715 assert_eq!(payload.names().collect::<Vec<_>>(), NAMES);
716 }
717
718 #[test]
719 fn spawn_is_generic_over_the_future_not_boxed() {
720 // The shape is copied from hyper::rt::Executor: generic over F,
721 // zero bounds in the declaration. Send is added by the impl, not
722 // the trait.
723 struct Immediate;
724 impl<F: std::future::Future<Output = ()>> Spawn<F> for Immediate {
725 fn spawn(&self, f: F) {
726 futures_executor::block_on(f)
727 }
728 }
729 let done = std::rc::Rc::new(std::cell::Cell::new(false));
730 let d = done.clone();
731 // !Send future — the trait allows this.
732 Immediate.spawn(async move { d.set(true) });
733 assert!(done.get());
734 }
735}