Skip to main content

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}