alux_http_conformance/lifecycle.rs
1//! What opening and closing one bound surface must do, whoever serves it.
2//!
3//! The scenario names no framework. It opens a server at an address, puts a connection on it,
4//! closes it, and opens another at the same address, which is the whole contract: closing resolves
5//! only once the address is released, so the address is free for whatever comes next.
6
7use alux_ext::ext;
8use alux_http::{HttpBind, HttpServerAlg, HttpServerSetup};
9use core::error::Error;
10use core::fmt::{self, Display, Formatter};
11use core::str;
12use core::time::Duration;
13use std::io;
14use std::net::{SocketAddr, TcpListener};
15use std::time::Instant;
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::net::TcpStream;
18use tokio::task::LocalSet;
19
20/// Carries why a lifecycle scenario was not satisfied.
21pub type LifecycleError = Box<dyn Error + Send + Sync>;
22
23/// Bounds how long closing may take before the scenario calls it a close that never returns.
24///
25/// Every interpretation bounds its own drain well inside this. It is here to fail a hang as a test
26/// failure rather than as a suite that never finishes.
27pub const LIMIT: Duration = Duration::from_secs(20);
28
29/// How long the scenario waits for a server to take a request it has been sent.
30///
31/// Writing a request only puts it in the socket. This is the moment given to the server to read it
32/// and start answering, so what the close finds is a connection being served rather than one about
33/// to be. Nothing the scenario states depends on the wait: a server that has not started yet simply
34/// has less to drain.
35///
36/// Every time the scenario reports is counted from the close rather than from the request, so this
37/// is the offset the endpoints are built to cancel. [`crate::PAUSE`] and [`crate::SLOW`] carry it,
38/// which is what lets a measurement be read against the drain without arithmetic.
39pub const SETTLE: Duration = Duration::from_millis(250);
40
41/// Holds a concrete server to what opening and closing one bound address must do.
42#[ext(name = ExpectLifecycleExt)]
43pub impl<This> This
44where
45 This: HttpServerAlg,
46 This::Error: Into<LifecycleError>,
47{
48 /// Holds this server to the contract when the connection it served has finished.
49 ///
50 /// One caller asks and is answered, and only then is the server closed. This is the ordinary
51 /// case: nothing is in flight, so no interpretation has anything to drain.
52 ///
53 /// # Errors
54 ///
55 /// States the first thing the server did not do: an open or close that failed, a close that
56 /// did not return, or a request the reopened server did not answer.
57 async fn expect_reopening<Compile>(mut self, mut compile: Compile) -> Result<(), LifecycleError>
58 where
59 Compile: FnMut() -> Self::Program,
60 {
61 // Several interpretations serve from tasks that are not `Send`, so every scenario runs on a
62 // local task set. It is theirs rather than the caller's, which is why it is stated here.
63 LocalSet::new()
64 .run_until(async move {
65 let bind = free_address()?;
66 let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
67
68 answered(bind).await?;
69 self.expect_closing(&mut open).await?;
70
71 // The address was released, so it can be taken again. Opening is the assertion.
72 let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
73
74 answered(bind).await?;
75 self.expect_closing(&mut reopened).await
76 })
77 .await
78 }
79
80 /// Holds this server to the contract when the request in flight finishes before the drain.
81 ///
82 /// A caller asks for something taking [`crate::PAUSE`], which is less than any drain an
83 /// interpretation states, and the server is closed while that answer is still being produced.
84 /// Closing gracefully means this caller is answered rather than cut off, so the answer must
85 /// arrive on the connection it was asked on, after the close has already returned.
86 ///
87 /// # Errors
88 ///
89 /// States the first thing the server did not do: an open or close that failed, an answer the
90 /// closing server never produced, or a request the reopened server did not answer.
91 async fn expect_answering_what_is_in_flight<Compile>(mut self, mut compile: Compile) -> Result<(), LifecycleError>
92 where
93 Compile: FnMut() -> Self::Program,
94 {
95 LocalSet::new()
96 .run_until(async move {
97 let bind = free_address()?;
98 let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
99 let mut held = hold(bind, "/pause").await?;
100
101 // Both at once, which is the whole statement: the caller is waiting for its
102 // answer at the same time as the server is being closed, and neither is allowed to
103 // be sequenced after the other. Reading only once closing had returned would state
104 // nothing, since by then whatever was going to happen already had.
105 let (closed, delivered) = tokio::join!(self.expect_closing(&mut open), read_answer(&mut held));
106 closed?;
107 delivered?;
108 drop(held);
109
110 let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
111
112 answered(bind).await?;
113 self.expect_closing(&mut reopened).await
114 })
115 .await
116 }
117
118 /// Holds this server to the contract when the request in flight outlives the drain.
119 ///
120 /// The other side of what a bound means. A caller asks for something taking [`crate::SLOW`],
121 /// which is longer than any drain, and the server is closed while that answer is still being
122 /// produced. Closing gracefully is not closing eventually: what is still being served when the
123 /// drain runs out is ended, so this caller must be left without an answer rather than handed
124 /// one by a server that no longer exists.
125 ///
126 /// # Errors
127 ///
128 /// States the first thing the server did not do: an open or close that failed, an answer that
129 /// arrived anyway, or a request the reopened server did not answer.
130 async fn expect_ending_what_outlives_the_drain<Compile>(
131 mut self,
132 mut compile: Compile,
133 ) -> Result<(), LifecycleError>
134 where
135 Compile: FnMut() -> Self::Program,
136 {
137 LocalSet::new()
138 .run_until(async move {
139 let bind = free_address()?;
140 let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
141 let mut held = hold(bind, "/slow").await?;
142
143 let (over, ended) = tokio::join!(self.expect_ending(&mut open), read_ending(&mut held));
144 over?;
145 ended?;
146 drop(held);
147
148 let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
149
150 answered(bind).await?;
151 self.expect_closing(&mut reopened).await
152 })
153 .await
154 }
155
156 /// Measures what closing and then ending do to one request in flight, rather than stating it.
157 ///
158 /// The same shape as the scenarios above, reporting what it saw instead of holding the server
159 /// to it. `asking_for` chooses which case is measured: `/pause` for a request that finishes
160 /// inside the drain, `/slow` for one that does not.
161 ///
162 /// Closing and ending are measured in that order on the one server, which is how a caller
163 /// wanting both would ask: take the address back, then wait for the rest.
164 ///
165 /// # Errors
166 ///
167 /// States an open, close or end that failed. What the request in flight did is measured, never
168 /// an error, since either outcome is a result worth reporting.
169 async fn measure_closing<Compile>(
170 mut self,
171 mut compile: Compile,
172 asking_for: &'static str,
173 ) -> Result<Measured, LifecycleError>
174 where
175 Compile: FnMut() -> Self::Program,
176 {
177 LocalSet::new()
178 .run_until(async move {
179 let bind = free_address()?;
180 let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
181 let mut held = hold(bind, asking_for).await?;
182
183 let started = Instant::now();
184 let (times, delivered) = tokio::join!(
185 async {
186 let closed = self.close(&mut open).await;
187 let at_close = started.elapsed();
188 let ended = self.end(&mut open).await;
189
190 (at_close, started.elapsed(), closed.and(ended))
191 },
192 read_outcome(&mut held, started)
193 );
194 times.2.map_err(Into::into)?;
195 drop(held);
196
197 let reusable = self.open(HttpServerSetup::new(bind, compile())).await;
198 let measured = Measured {
199 asked: asking_for,
200 closed: times.0,
201 ended: times.1,
202 in_flight: delivered.0,
203 at: delivered.1,
204 reusable: reusable.is_ok(),
205 };
206 if let Ok(mut reusable) = reusable {
207 self.close(&mut reusable).await.map_err(Into::into)?;
208 }
209
210 Ok(measured)
211 })
212 .await
213 }
214
215 /// Closes one open server and states that closing returned inside [`LIMIT`].
216 ///
217 /// # Errors
218 ///
219 /// States a close that failed, and a close that did not return in time.
220 async fn expect_closing(&mut self, open: &mut Self::Open) -> Result<(), LifecycleError> {
221 let closing = Instant::now();
222 let closed = tokio::time::timeout(LIMIT, self.close(open))
223 .await
224 .map_err(|_| io::Error::other(format!("closing did not return inside {LIMIT:?}")))?;
225 closed.map_err(Into::into)?;
226 bounded_by_the_server(closing, "closing")
227 }
228
229 /// Ends one open server and states that ending returned inside [`LIMIT`].
230 ///
231 /// # Errors
232 ///
233 /// States an end that failed, and an end that did not return in time.
234 async fn expect_ending(&mut self, open: &mut Self::Open) -> Result<(), LifecycleError> {
235 let ending = Instant::now();
236 let ended = tokio::time::timeout(LIMIT, self.end(open))
237 .await
238 .map_err(|_| io::Error::other(format!("ending did not return inside {LIMIT:?}")))?;
239 ended.map_err(Into::into)?;
240 bounded_by_the_server(ending, "ending")
241 }
242
243 /// Holds this server to the contract when a connection is still open as it closes.
244 ///
245 /// A caller connects, is answered, and then asks for something that takes [`crate::SLOW`] to
246 /// answer, which is longer than any drain an interpretation states. So the connection outlives
247 /// the close rather than the close waiting it out: whoever drains hits its own bound and stops
248 /// anyway. Closing must still return, and the address must still be free for the server opened
249 /// after it.
250 ///
251 /// # Errors
252 ///
253 /// States the first thing the server did not do: an open or close that failed, a close the
254 /// held connection kept from returning, or a request the reopened server did not answer.
255 async fn expect_reopening_while_a_connection_is_held<Compile>(
256 mut self,
257 mut compile: Compile,
258 ) -> Result<(), LifecycleError>
259 where
260 Compile: FnMut() -> Self::Program,
261 {
262 LocalSet::new()
263 .run_until(async move {
264 let bind = free_address()?;
265 let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
266 let held = hold(bind, "/slow").await?;
267
268 self.expect_closing(&mut open).await?;
269
270 let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
271
272 // A new connection, because what becomes of the held one differs: an
273 // interpretation that only drops its listener answers it in its own time, one that
274 // shuts its framework down ends it unanswered. Neither is what this states, which
275 // is that the held connection did not keep the address from being taken again.
276 answered(bind).await?;
277 drop(held);
278
279 self.expect_closing(&mut reopened).await
280 })
281 .await
282 }
283}
284
285/// What became of a request that was in flight when its server was closed.
286///
287/// Answered and ended are not the only two. A close can land between the head of an answer and the
288/// end of its body, leaving the caller a status it can read and an answer it cannot use, so that is
289/// stated as what it is rather than counted as either.
290#[derive(Debug, Clone, Copy, Eq, PartialEq)]
291pub enum InFlight {
292 /// The server answered in full, carrying this status.
293 Answered(u16),
294 /// The head arrived carrying this status, and the body was cut off before it was complete.
295 CutOff(u16),
296 /// The connection ended with nothing on it, so closing ran out of drain first.
297 Ended,
298 /// Neither happened inside [`LIMIT`], so the measurement states nothing.
299 Unfinished,
300}
301
302impl InFlight {
303 /// Whether the caller was left with an answer it can use.
304 pub const fn is_answered(self) -> bool {
305 matches!(self, Self::Answered(_))
306 }
307}
308
309impl Display for InFlight {
310 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
311 match self {
312 Self::Answered(status) => write!(f, "answered {status}"),
313 Self::CutOff(status) => write!(f, "{status} cut off mid-answer"),
314 Self::Ended => f.write_str("ended unanswered"),
315 Self::Unfinished => f.write_str("still waiting"),
316 }
317 }
318}
319
320/// Reads everything a server sends on a connection, and says what it amounted to.
321///
322/// Answers when the first byte arrived as well as what arrived, because the two state different
323/// things: one is when the server got to this request, the other whether it finished it.
324async fn read_outcome(connection: &mut TcpStream, started: Instant) -> (InFlight, Duration) {
325 let mut answer = Vec::new();
326 let mut first = None;
327 let read = tokio::time::timeout(LIMIT, async {
328 let mut arriving = [0; 1024];
329 // Read on rather than once: whether the body is whole is the whole question, and one read
330 // can carry a head whose body never follows.
331 while let Ok(read) = connection.read(&mut arriving).await {
332 if read == 0 {
333 break;
334 }
335 first.get_or_insert_with(|| started.elapsed());
336 answer.extend_from_slice(&arriving[..read]);
337 // Stop at the end of one whole answer rather than at the end of the connection, which
338 // on a keep-alive connection is not coming.
339 if states_a_whole_answer(&answer) {
340 break;
341 }
342 }
343 })
344 .await;
345
346 if read.is_err() {
347 return (InFlight::Unfinished, started.elapsed());
348 }
349
350 (read_as_answer(&answer), first.unwrap_or_else(|| started.elapsed()))
351}
352
353/// Reads bytes as the answer they amount to, whole or cut off.
354fn read_as_answer(answer: &[u8]) -> InFlight {
355 let Some(status) = stated_status(answer) else {
356 return InFlight::Ended;
357 };
358 if states_a_whole_answer(answer) {
359 return InFlight::Answered(status);
360 }
361
362 // An answer stating no length of its own ends where the connection does, so reading this far
363 // is reading all of it. Anything else stated a length it did not reach.
364 match answer.windows(4).position(|four| four == b"\r\n\r\n") {
365 Some(head) => {
366 let head = String::from_utf8_lossy(&answer[..head]).to_lowercase();
367 if stated_length(&head).is_none() && !head.contains("transfer-encoding: chunked") {
368 InFlight::Answered(status)
369 } else {
370 InFlight::CutOff(status)
371 }
372 }
373 // Not even the head arrived whole.
374 None => InFlight::CutOff(status),
375 }
376}
377
378/// Whether these bytes carry one answer, all of it.
379///
380/// Only where the answer stated how long it would be. One that did not is whole when the connection
381/// ends, which these bytes cannot say.
382fn states_a_whole_answer(answer: &[u8]) -> bool {
383 let Some(head) = answer.windows(4).position(|four| four == b"\r\n\r\n") else {
384 return false;
385 };
386
387 let (head, body) = answer.split_at(head + 4);
388 let head = String::from_utf8_lossy(head).to_lowercase();
389 match stated_length(&head) {
390 Some(stated) => body.len() >= stated,
391 None => head.contains("transfer-encoding: chunked") && body.ends_with(b"0\r\n\r\n"),
392 }
393}
394
395/// Reads the status an answer carries, where it carries one at all.
396fn stated_status(answer: &[u8]) -> Option<u16> {
397 let line = answer.split(|byte| *byte == b'\r').next()?;
398
399 str::from_utf8(line).ok()?.split_whitespace().nth(1)?.parse().ok()
400}
401
402/// Reads how long an answer says its body is, where it says at all.
403fn stated_length(head: &str) -> Option<usize> {
404 head.lines().find_map(|line| line.strip_prefix("content-length:")?.trim().parse().ok())
405}
406
407/// One measurement of what closing did, which reads as a row of the table it is gathered for.
408#[derive(Debug, Clone, Copy)]
409pub struct Measured {
410 /// What the held connection asked for.
411 pub asked: &'static str,
412 /// How long closing took to return.
413 pub closed: Duration,
414 /// How long until ending returned as well, counted from the same start.
415 pub ended: Duration,
416 /// What became of the request in flight.
417 pub in_flight: InFlight,
418 /// When that became known, measured from when closing started.
419 pub at: Duration,
420 /// Whether the address could be taken again once closing had returned.
421 pub reusable: bool,
422}
423
424impl Display for Measured {
425 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
426 let Self { asked, closed, ended, in_flight, at, reusable } = self;
427 // Seconds throughout rather than whatever unit each duration reads best in, so a column of
428 // these can be compared down the page.
429 let (closed, ended, at) = (closed.as_secs_f64(), ended.as_secs_f64(), at.as_secs_f64());
430
431 write!(f, "{asked} | {in_flight} at {at:.4}s")?;
432 write!(f, " | close {closed:.4}s | end {ended:.4}s | reusable {reusable}")
433 }
434}
435
436/// Takes the mean of however many durations, which is nothing where there are none.
437fn meaned(durations: impl ExactSizeIterator<Item = Duration>) -> Duration {
438 let Ok(rounds) = u32::try_from(durations.len()) else {
439 return Duration::ZERO;
440 };
441
442 durations.sum::<Duration>().checked_div(rounds).unwrap_or_default()
443}
444
445/// Several measurements of the same thing, taken together.
446///
447/// One measurement of a close is one sample of a machine, so a comparison worth reading is a mean.
448/// The range comes with it, because a mean says nothing on its own about whether the rounds agreed.
449#[derive(Debug, Clone, Copy)]
450pub struct Averaged {
451 /// What the held connection asked for.
452 pub asked: &'static str,
453 /// How many measurements this is the mean of.
454 pub rounds: usize,
455 /// How long closing took to return, meaned.
456 pub closed: Duration,
457 /// How long until ending returned as well, meaned.
458 pub ended: Duration,
459 /// The spread between the slowest and fastest close, which says how much the mean is worth.
460 pub range: Duration,
461 /// What became of the request in flight, which every round agreed on.
462 pub in_flight: InFlight,
463 /// When that became known, meaned.
464 pub at: Duration,
465 /// Whether the address came back, which every round agreed on.
466 pub reusable: bool,
467}
468
469impl Averaged {
470 /// Takes the mean of several measurements of the same thing.
471 ///
472 /// # Errors
473 ///
474 /// States rounds with nothing in them, and rounds disagreeing about what became of the request
475 /// or about whether the address came back. A mean over those would hide the thing worth
476 /// knowing, which is that a provider did not do the same thing twice.
477 pub fn over(rounds: &[Measured]) -> Result<Self, LifecycleError> {
478 let [first, rest @ ..] = rounds else {
479 return Err(io::Error::other("averaging nothing").into());
480 };
481 if let Some(differs) =
482 rest.iter().find(|round| round.in_flight != first.in_flight || round.reusable != first.reusable)
483 {
484 let (asked, first, differs) = (first.asked, first.in_flight, differs.in_flight);
485 return Err(io::Error::other(format!("{asked} was {first} in one round and {differs} in another")).into());
486 }
487
488 let closed = rounds.iter().map(|round| round.closed);
489 let slowest = closed.clone().max().unwrap_or_default();
490 let quickest = closed.clone().min().unwrap_or_default();
491
492 Ok(Self {
493 asked: first.asked,
494 rounds: rounds.len(),
495 closed: meaned(closed),
496 ended: meaned(rounds.iter().map(|round| round.ended)),
497 range: slowest.saturating_sub(quickest),
498 in_flight: first.in_flight,
499 at: meaned(rounds.iter().map(|round| round.at)),
500 reusable: first.reusable,
501 })
502 }
503}
504
505impl Display for Averaged {
506 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
507 let Self { asked, rounds, closed, ended, range, in_flight, at, reusable } = self;
508 let (closed, ended) = (closed.as_secs_f64(), ended.as_secs_f64());
509 let (range, at) = (range.as_secs_f64(), at.as_secs_f64());
510
511 write!(f, "{asked} | {in_flight} at {at:.4}s | close {closed:.4}s | end {ended:.4}s")?;
512 write!(f, " | reusable {reusable} | range {range:.4}s over {rounds}")
513 }
514}
515
516/// States that what took this long was bounded by the server rather than by [`LIMIT`].
517///
518/// Awaiting inside the limit is not the same as being bounded by the interpretation: one that took
519/// the whole limit was bounded by the scenario, and says nothing about its own drain.
520fn bounded_by_the_server(since: Instant, what: &str) -> Result<(), LifecycleError> {
521 if since.elapsed() >= LIMIT {
522 return Err(io::Error::other(format!("{what} was bounded by the scenario rather than by the server")).into());
523 }
524
525 Ok(())
526}
527
528/// Names an address nothing is bound to.
529pub(crate) fn free_address() -> Result<HttpBind, LifecycleError> {
530 // Binding and dropping states the address as free; the kernel picked it, so nothing else here
531 // has a reason to hold it.
532 let probe = TcpListener::bind("127.0.0.1:0")?;
533
534 Ok(HttpBind::new(probe.local_addr()?))
535}
536
537/// Asks for the readings and states that the server answered them.
538pub(crate) async fn answered(bind: HttpBind) -> Result<(), LifecycleError> {
539 let address = bind.address();
540 let mut connection = TcpStream::connect(address).await?;
541 connection.write_all(asking(address, "/items").as_bytes()).await?;
542
543 match read_outcome(&mut connection, Instant::now()).await.0 {
544 InFlight::Answered(200) => Ok(()),
545 read => Err(io::Error::other(format!("the server at {address} {read}")).into()),
546 }
547}
548
549/// Takes a connection the server is producing an answer on, and keeps it.
550///
551/// Two requests, because both matter. The first is asked and answered, which states that this
552/// connection was accepted rather than left waiting in the backlog. The second asks for the slow
553/// answer and is not read, so the connection is one the server is still working on. An idle
554/// keep-alive connection would not do: a graceful shutdown closes those at once and drains nothing.
555pub(crate) async fn hold(bind: HttpBind, asking_for: &str) -> Result<TcpStream, LifecycleError> {
556 let address = bind.address();
557 let mut connection = TcpStream::connect(address).await?;
558 connection.write_all(asking(address, "/items").as_bytes()).await?;
559
560 read_answer(&mut connection).await?;
561
562 connection.write_all(asking(address, asking_for).as_bytes()).await?;
563 tokio::time::sleep(SETTLE).await;
564
565 Ok(connection)
566}
567
568/// Reads one answer from a connection, and states that the server answered it in full.
569///
570/// In full rather than merely begun: a head whose body was cut off leaves the caller a status and
571/// nothing it can use, which is not what being answered means.
572async fn read_answer(connection: &mut TcpStream) -> Result<(), LifecycleError> {
573 match read_outcome(connection, Instant::now()).await.0 {
574 InFlight::Answered(200) => Ok(()),
575 read => Err(io::Error::other(format!("the closing server {read}")).into()),
576 }
577}
578
579/// Reads the end of a connection, and states that no usable answer arrived on it.
580///
581/// A head cut off mid-body counts as no answer, because that is what it leaves the caller with.
582/// What must not happen is a whole answer from a server that has already closed.
583async fn read_ending(connection: &mut TcpStream) -> Result<(), LifecycleError> {
584 match read_outcome(connection, Instant::now()).await.0 {
585 InFlight::Ended | InFlight::CutOff(_) => Ok(()),
586 read => Err(io::Error::other(format!("the closed server {read} anyway")).into()),
587 }
588}
589
590/// States one request, left open so the connection carries whatever comes next.
591fn asking(address: SocketAddr, path: &str) -> String {
592 format!("GET {path} HTTP/1.1\r\nHost: {address}\r\n\r\n")
593}