1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
//! The observability seam: what a transport did, told to whoever asked.
//!
//! Today the answer to *"why was that request slow"* is "read the source".
//! A caller can see the response and its version and nothing else — not
//! whether a connection was made or reused, not what DNS cost, not why a
//! connection went away underneath them.
//!
//! # It reports; it does not steer
//!
//! [`Hooks::on`] returns `()`, and that is the whole of the contract.
//! There is no verdict a hook can hand back, so there is nothing for the
//! request path to branch on: this cannot grow into a second
//! [`Capabilities`](crate::Capabilities), where a caller's declaration
//! changes what the transport does. A hook that wants to change the
//! request has the request — that is `Client`'s business, one layer up,
//! and a return value here would move the decision to a place where
//! nobody could see it happen.
//!
//! # Zero cost when nobody is watching, and it is [`Hooks::WATCHING`]
//!
//! A hook is a type parameter, not a `Box<dyn Hooks>`, and the type a
//! caller who wants nothing gets is [`NoHooks`] — a zero-sized struct
//! whose `WATCHING` is `false`. Backends read that const **before** they
//! measure anything, so a build with no hook does not read a clock, does
//! not take an id from a counter, and has no branch left that a
//! monomorphised `NoHooks` cannot delete. The const is what makes that
//! structural: a runtime `if self.hooks.is_some()` would still cost a
//! branch, and would already have read the clock to have something to put
//! in it. `crates/hclient-native/tests/hooks_cost.rs` measures it from
//! outside — a runtime whose clock counts its own reads.
//!
//! It is deliberately all-or-nothing rather than one const per event. A
//! hook that wanted only [`Closed`] would then skip the connect timings,
//! and the seam would gain four booleans that every backend has to read
//! correctly for the timings to stay honest. One const, one rule.
//!
//! # A panicking hook
//!
//! A panic in [`Hooks::on`] propagates to whoever polled the request or
//! the response body. It is deliberately not caught:
//! `std::panic::catch_unwind` needs `UnwindSafe`, which would become a
//! bound on the caller's own type, and it does nothing at all under
//! `panic = "abort"` — so catching would be a promise that holds in some
//! builds and not others. What backends owe instead is that a panic can
//! only unwind *out*, never leave a lock poisoned or a process aborted:
//!
//! - **No hook is called with a lock held.** `hclient-native` emits from
//! `Transport::execute` and from its response body, never from inside
//! the connection pool's mutex. A hook that panics therefore cannot
//! poison it, and a hook that blocks cannot stall a request that is not
//! its own.
//! - **No hook is called from a `Drop` impl.** A panic there, during an
//! unwind already in progress, aborts the process — and an
//! observability seam that can abort a program is worse than no seam.
//! The cost is written down where it lands: a connection dropped rather
//! than finished (a cancelled request; one the pool evicts for age) gets
//! no [`Closed`] event. That is a hole, and it is the one this rule
//! buys.
//!
//! # Why `unversioned`
//!
//! The event set is derived from what a connection-owning backend can
//! observe, and backends still to come will have facts this vocabulary has
//! no word for — a QUIC connection migrating, a transfer a background
//! session finished after the process died. Freezing it would freeze one
//! backend's view. See this module's parent for what `unversioned`
//! promises.
use crateError;
use Duration;
use Display;
use SocketAddr;
use Arc;
use ;
/// Somewhere to send what a transport did.
///
/// Implemented by the application rather than by a backend — which makes
/// it the one trait in `unversioned` pointing the other way. A backend
/// *calls* it, and what it owes is written on [`Event`]'s variants.
///
/// **No `Send` bound, declared or implied** (P13, settled by construction
/// in `crates/hclient-core/tests/shape.rs`). A hook is stored in a
/// transport and called from inside a response body's `poll_frame`, which
/// is not the shape any other seam here has: the body outlives
/// `Transport::execute`, so it holds the hook rather than borrowing it,
/// and a `Send` declared anywhere on that path would shut every
/// single-threaded runtime out of observability. It is inferred instead —
/// a transport with a `Send` hook stays `Send`, one with an `Rc` inside
/// its hook does not, and both implement `Transport`.
/// The hook a caller who asked for nothing gets: `WATCHING` is `false`,
/// the type is zero-sized, and `on` has no body.
///
/// The default type parameter of `hclient_native::Native`, so the
/// no-hooks build is the one that needs no words to ask for.
;
/// So that a hook can be shared without the caller writing the
/// delegation. `WATCHING` is forwarded rather than defaulted, or an
/// `Arc<NoHooks>` would start watching by being wrapped.
/// The single-threaded half of the impl above, and not decoration: it is
/// the shape P13 asks about. A hook behind an `Rc` makes the transport
/// holding it `!Send`, and everything still compiles — see
/// `crates/hclient-core/tests/shape.rs`.
/// What a transport reports.
///
/// Not `#[non_exhaustive]`, for `Message`'s reason (see
/// [`Message`](crate::unversioned::Message)): nothing here is published,
/// so a new variant costs a rebase inside this workspace, and a compile
/// error is what an implementer of [`Hooks`] should get when the
/// vocabulary grows — rather than a silently ignored fact.
///
/// **There is deliberately no "request queued" variant.** The original
/// list for this work had one, and `hclient-native` has nothing to put in
/// it: a request that finds no live pooled connection dials a fresh one,
/// there is no per-origin connection limit to wait behind, and an h2
/// connection is checked out of the pool exclusively — one stream at a
/// time — so `SendRequest::poll_ready` never waits for a stream of ours.
/// A variant no code can emit is a capability that lies; this one belongs
/// here once a backend has a queue.
/// A `1xx` that arrived before the response.
///
/// # Why there is no `version` here, when [`Head`] has one
///
/// A `1xx` travels on a connection, and the connection's protocol was
/// already reported by the [`Connected`] or [`Reused`] that opened this
/// exchange — both of which carry a plain `Version` rather than an
/// `Option`, because only a transport that owns a connection emits either
/// and owning one means having negotiated its protocol. Repeating it here
/// would be a third place to be wrong about the same fact.
///
/// `Head::version` is an `Option` for the opposite reason: the two
/// backends that own no connection emit `Head` and nothing else, so for
/// them there is no `Connected` to have carried it. Neither of them can
/// emit this event at all.
/// Which connection an event is about.
///
/// Process-wide and monotonic, so a [`Closed`] can be matched to the
/// [`Connected`] that opened the same socket — which is the only reason
/// it exists. Without it a close event says "a connection ended" and a
/// caller holding several has no way to learn which.
///
/// [`ConnectionId::UNWATCHED`] is what an event carries when there was no
/// id to mint for it — because nobody is watching, or because there is no
/// connection. See that constant.
;
static NEXT_CONNECTION_ID: AtomicU64 = new;
/// A connection was established.
/// What each phase of a connect cost.
///
/// **The three phases are three measurements, not a decomposition.**
/// `dns + tcp + tls <= total` always holds — they are disjoint intervals
/// inside the whole — but the remainder is real time belonging to none of
/// them: RFC 8305 staggers connection attempts, so a winner that started
/// 250 ms into the race spent that stagger in no phase at all, and a
/// first attempt made through an HTTPS record's hints and failed is time
/// the second attempt's phases do not contain either.
///
/// Every duration is measured on the transport's own `Timer` — the same
/// clock its timeouts and its pool deadlines use, so a test under
/// `tokio::time::pause()` sees one consistent story rather than two.
/// A connection was taken from the pool instead of being made.
///
/// The counterpart of [`Connected`], and the fact a caller cannot
/// otherwise get at: two requests to one origin either cost one
/// connection or two, and only the transport knows which happened.
/// The response head arrived.
/// A connection ended.
/// Why a connection ended.
///
/// Three, because three is what the code can tell apart. It deliberately
/// does not include "the caller dropped it": that would have to be
/// reported from a `Drop` impl, and the module doc says why no hook is
/// ever called from one.