LCOV - code coverage report
Current view: top level - http/server - server_connection_deadlines.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 116 116
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 19 19

            Line data    Source code
       1              : #include "server_connection.hpp"
       2              : #include "server_base.hpp"
       3              : #include "../../asio/sockets/socket.hpp"
       4              : #include "body_reader.hpp"
       5              : #include "../../util/logger.hpp"
       6              : #include <algorithm>
       7              : 
       8              : // The deadlines of a server connection, on its one watchdog (timeout_timer_): no timer, spawn or
       9              : // allocation of their own. The watchdog is armed at the earliest deadline that applies, and on
      10              : // expiry the connection works out which one passed: it acts for it, or arms the watchdog again
      11              : // for the next one (a head completed, activity moved the idle deadline, or a write went on,
      12              : // meanwhile)
      13              : 
      14              : namespace thinger::http {
      15              : 
      16       159293 : void server_connection::arm_deadline() {
      17       159293 :     timeout_timer_.expires_at(next_deadline());
      18       159293 : }
      19              : 
      20       159293 : server_connection::clock::time_point server_connection::next_deadline() const {
      21       159293 :     auto deadline = idle_deadline();
      22       159293 :     if (clock::time_point head; head_deadline(head)) deadline = std::min(deadline, head);
      23       159293 :     if (clock::time_point body; body_deadline(body)) deadline = std::min(deadline, body);
      24       159293 :     if (read_waits_ && read_bounded_) deadline = std::min(deadline, read_deadline_);
      25       159293 :     return deadline;
      26              : }
      27              : 
      28       159409 : server_connection::clock::time_point server_connection::idle_deadline() const {
      29              :     // A response the client still takes is no idle connection, however long it lasts: only a
      30              :     // write without progress for its deadline is. Not floored by the hold, as the request ones
      31       159409 :     if (write_waits_) return write_progress_ + options_->write_timeout;
      32       147223 :     return std::max(last_activity_ + timeout_, hold_);
      33              : }
      34              : 
      35         7339 : void server_connection::write_started() {
      36         7339 :     writing_ = true;
      37         7339 :     if (options_->write_timeout.count() <= 0) return;
      38         7336 :     write_waits_ = true;
      39         7336 :     write_progress_ = clock::now();
      40         7336 :     arm_deadline();
      41              : }
      42              : 
      43         7339 : bool server_connection::write_in_slices(const std::vector<boost::asio::const_buffer>& buffers) const {
      44              :     // Without the deadline, or within one slice, written at once
      45         7339 :     return options_->write_timeout.count() > 0 && boost::asio::buffer_size(buffers) > asio::socket::WRITE_SLICE_SIZE;
      46              : }
      47              : 
      48       159535 : bool server_connection::head_deadline(clock::time_point& deadline) const {
      49              :     // (not once the connection stopped reading)
      50       159535 :     if (!reading_head_ || !running_) return false;
      51              :     // Capped by the request deadline, as Node does; zero disables either
      52        46346 :     auto limit = options_->header_timeout;
      53        46346 :     auto request = options_->request_timeout;
      54        46346 :     if (request.count() > 0 && (limit.count() <= 0 || request < limit)) limit = request;
      55        46346 :     if (limit.count() <= 0) return false;
      56        46340 :     deadline = request_start_ + limit;
      57        46340 :     return true;
      58              : }
      59              : 
      60        27239 : void server_connection::head_started(bool pipelined) {
      61        27239 :     if (reading_head_) return;
      62         5132 :     request_start_ = pipelined ? clock::now() : last_activity_;
      63              : }
      64              : 
      65         3355 : void server_connection::head_incomplete() {
      66         3355 :     if (reading_head_) return;
      67          429 :     reading_head_ = true;
      68          429 :     arm_deadline();
      69              : }
      70              : 
      71          242 : void server_connection::deadline_expired() {
      72          242 :     auto now = clock::now();
      73          242 :     if (clock::time_point head; head_deadline(head) && now >= head) {
      74           18 :         head_timed_out();
      75           18 :         return;
      76              :     }
      77          224 :     if (read_waits_ && read_bounded_ && now >= read_deadline_) [[unlikely]] {
      78           75 :         read_waits_ = false;
      79           75 :         read_expired_ = true;
      80           75 :         read_cancel_.emit(boost::asio::cancellation_type::terminal);
      81           75 :         arm_deadline();
      82           75 :         return;
      83              :     }
      84          149 :     if (clock::time_point body; body_deadline(body) && now >= body) {
      85           33 :         request_timed_out(*in_progress_);
      86           33 :         return;
      87              :     }
      88          116 :     if (now >= idle_deadline()) {
      89              :         // Nor is a request received whole whose handler runs idle, as in Node and Go: looked at
      90              :         // again an idle timeout later (the watchdog is never left unarmed, whatever ends the
      91              :         // handler). Checked as it expires, so that a request costs nothing more. A body still
      92              :         // arriving is bounded by it, as every byte moves it
      93           86 :         if (!write_waits_ && in_progress_ && in_progress_->handler_running() && !in_progress_->input_pending()) {
      94            6 :             timeout_timer_.expires_at(now + std::max(timeout_, std::chrono::seconds{1}));
      95            6 :             return;
      96              :         }
      97           80 :         if (write_waits_) {
      98           28 :             LOG_DEBUG("http server connection closed: the client took no byte of its response within its write deadline");
      99              :         } else {
     100           52 :             LOG_DEBUG("http server connection timed out after {} seconds", timeout_.count());
     101              :         }
     102           80 :         close();
     103           80 :         return;
     104              :     }
     105              :     // None passed: the earliest that applies now
     106           30 :     arm_deadline();
     107              : }
     108              : 
     109           18 : void server_connection::head_timed_out() {
     110           18 :     reading_head_ = false;
     111              :     // No byte of a request arrived (only the first request of a connection is timed so): closed
     112              :     // without a response, which the client could take for that of a request it just sent, as Go
     113              :     // and nginx do. Logged at debug level only, as the other expiries: the client decides how often
     114           18 :     if (request_parser_.idle()) {
     115            9 :         LOG_DEBUG("http server connection closed: no request arrived within its header deadline");
     116            9 :         close();
     117            9 :         return;
     118              :     }
     119            9 :     LOG_DEBUG("http request head not complete within its deadline: answered 408");
     120              :     // The read loop stops reading (whatever its pending read returns), and the connection closes
     121              :     // once the 408 is written, at once: what the client still sends is not waited for
     122            9 :     running_ = false;
     123            9 :     answer_and_close(http_response::status::timed_out, detail::close_reason::request_timeout, false);
     124              : }
     125              : 
     126       159442 : bool server_connection::body_deadline(clock::time_point& deadline) const {
     127              :     // Received once its body was read to its end: told by the body reader, checked here
     128       159442 :     if (request_deadline_.count() <= 0 || !in_progress_ || !in_progress_->input_pending() || !running_) return false;
     129        45710 :     deadline = request_start_ + request_deadline_;
     130        45710 :     return true;
     131              : }
     132              : 
     133           27 : void server_connection::set_request_deadline(const http_stream& stream, std::chrono::milliseconds deadline) {
     134              :     // Only while its body is read: a request without one was received with its head
     135           27 :     if (in_progress_ != &stream || !stream.input_pending()) return;
     136           27 :     request_deadline_ = deadline;
     137              :     // An earlier deadline arms the watchdog again (at once if it passed already)
     138           27 :     arm_deadline();
     139              : }
     140              : 
     141          515 : void server_connection::set_read_deadline(const http_stream* stream, clock::time_point deadline) {
     142          515 :     if (in_progress_ != stream) return;
     143              :     // It replaces the request deadline for the rest of the body, as in Go: the read deadline
     144              :     // itself applies only while a read waits for the socket (see body_read_started()), a
     145              :     // read already waiting included
     146          515 :     request_deadline_ = std::chrono::milliseconds{0};
     147          515 :     read_deadline_ = deadline;
     148              :     // (the end of time, as Go's zero time: none)
     149          515 :     read_bounded_ = deadline.time_since_epoch() != clock::duration::max();
     150          515 :     if (read_waits_) arm_deadline();
     151              : }
     152              : 
     153        43462 : bool server_connection::read_deadline_passed() const {
     154        43462 :     return read_bounded_ && clock::now() >= read_deadline_;
     155              : }
     156              : 
     157        43459 : void server_connection::body_read_started(boost::asio::cancellation_slot caller) {
     158        43462 :     if (caller.is_connected()) caller.assign([this](auto type) { read_cancel_.emit(type); });
     159        43459 :     read_waits_ = true;
     160        43459 :     if (read_bounded_) arm_deadline();
     161        43459 : }
     162              : 
     163        43007 : void server_connection::body_read_ended(boost::asio::cancellation_slot caller, io_result& result) {
     164              :     // No longer forwarded: the slot may be emitted after the connection is gone
     165        43007 :     if (caller.is_connected()) caller.clear();
     166        43007 :     read_waits_ = false;
     167        43007 :     if (read_expired_) [[unlikely]] {
     168           75 :         read_expired_ = false;
     169           75 :         auto& ec = std::get<boost::system::error_code>(result);
     170           75 :         if (ec == boost::asio::error::operation_aborted) ec = boost::system::errc::make_error_code(read_deadline_expired);
     171              :     }
     172        43007 : }
     173              : 
     174           33 : void server_connection::request_timed_out(http_stream& stream) {
     175           33 :     request_deadline_ = std::chrono::milliseconds{0};
     176           33 :     stream.cancel(cancel_reason::timeout);
     177              :     // Its response not started, nor another one being written (pipelined): answered 408 here,
     178              :     // and the connection closed after it, gracefully as the body may still come (as Node
     179              :     // does). Otherwise closed now: a write in progress is never cut short to make room for a
     180              :     // 408. Either way, what its handler reads or writes afterwards fails on the closed connection
     181           63 :     bool answerable = !stream.responded() && !stream.has_posted_frames() && !writing_ &&
     182           63 :                       !request_queue_.empty() && request_queue_.front().get() == &stream;
     183           33 :     if (!answerable) {
     184            3 :         LOG_DEBUG("http request not received within its deadline, its response started: closing the connection");
     185            3 :         close();
     186            3 :         return;
     187              :     }
     188           30 :     LOG_DEBUG("http request not received within its deadline: answered 408");
     189              :     // The body read pending ends (nothing else uses the socket meanwhile)
     190           30 :     socket_->cancel();
     191           30 :     stream.must_close(detail::close_reason::request_timeout);
     192           30 :     handle_stock_error(request_queue_.front(), http_response::status::timed_out);
     193              : }
     194              : 
     195          381 : void server_connection::answer_and_close(http_response::status status, detail::close_reason reason,
     196              :                                          bool input_pending) {
     197              :     // Where the request ends is unknown: the connection closes after the response
     198          381 :     auto stream = std::make_shared<http_stream>(++request_id_, reason, socket_->get_io_context().get_executor());
     199          381 :     stream->set_input_pending(input_pending);
     200          381 :     request_queue_.push_back(stream);
     201          381 :     handle_stock_error(stream, status);
     202          381 : }
     203              : 
     204              : }
        

Generated by: LCOV version 2.0-1