LCOV - code coverage report
Current view: top level - http/client - async_client.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 93.9 % 131 123
Test Date: 2026-11-10 10:38:07 Functions: 93.9 % 33 31

            Line data    Source code
       1              : #include "async_client.hpp"
       2              : #include "../../util/logger.hpp"
       3              : #include <algorithm>
       4              : #include <condition_variable>
       5              : #include <list>
       6              : #include <mutex>
       7              : #include <thread>
       8              : #include <vector>
       9              : 
      10              : namespace thinger::http {
      11              : 
      12              : namespace detail {
      13              : 
      14              : // How long stop() and the destructor wait for the cancelled requests to end, before leaving
      15              : // behind what remains of them: the cancellation reaches every operation of a request at once,
      16              : // unless one ignores it
      17              : constexpr auto end_grace = std::chrono::seconds{1};
      18              : 
      19              : // The HTTP client the requests of an async_client use, and the requests in flight. The client
      20              : // holds it, and each request as long as any of its frames exists
      21              : class client_state final : public http_client_base {
      22              : public:
      23          625 :     explicit client_state(asio::workers& workers) : workers(workers) {}
      24              : 
      25              :     asio::workers& workers;
      26              : 
      27              :     std::mutex mutex;
      28              :     std::condition_variable changed;
      29              : 
      30              :     // Guarded by the mutex: the requests in flight, the threads running a synchronous callback
      31              :     // of one of them, and how many calls of stop() or the destructor run (or the destructor
      32              :     // ran), which start no request
      33              :     std::list<std::shared_ptr<client_request>> requests;
      34              :     std::vector<std::thread::id> callbacks;
      35              :     unsigned closers = 0;
      36              :     // The requests in flight not cancelled (and those the application tracks), written under
      37              :     // the mutex
      38              :     std::atomic<size_t> active{0};
      39              : 
      40              :     // Whether a thread other than the caller runs a callback (lock held)
      41          685 :     bool callbacks_elsewhere() const {
      42          685 :         return std::any_of(callbacks.begin(), callbacks.end(),
      43         1417 :                            [this_thread = std::this_thread::get_id()](auto id) { return id != this_thread; });
      44              :     }
      45              : 
      46              : protected:
      47              :     // The worker of the calling thread, or else one of the pool
      48         2688 :     boost::asio::io_context& get_io_context() override { return workers.get_thread_io_context(); }
      49              : };
      50              : 
      51              : // A request in flight: where it runs, what cancels it, and its place among the requests
      52              : class client_request {
      53              : public:
      54         2764 :     client_request(std::shared_ptr<client_state> state, boost::asio::io_context::executor_type executor)
      55         2764 :         : state(std::move(state)), executor(executor) {}
      56              : 
      57              :     std::shared_ptr<client_state> state;
      58              :     boost::asio::io_context::executor_type executor;
      59              :     // Connected by co_spawn to the coroutine; emitted on its thread
      60              :     boost::asio::cancellation_signal signal;
      61              :     std::list<std::shared_ptr<client_request>>::iterator position;
      62              :     // Guarded by the mutex of the state: its coroutine is spawned (the signal may be emitted), it
      63              :     // was cancelled, and stop() or the destructor gave up waiting for it
      64              :     bool spawned = false;
      65              :     bool cancelled = false;
      66              :     bool left_behind = false;
      67              :     // Set under the mutex, read too on its thread, where it is set
      68              :     std::atomic<bool> ended{false};
      69              : 
      70              :     // Cancel it, on its thread: whatever it waits for ends with operation_aborted
      71         1656 :     static void cancel(const std::shared_ptr<client_request>& request) {
      72         1656 :         boost::asio::dispatch(request->executor, [request] {
      73         1655 :             if (!request->ended) request->signal.emit(boost::asio::cancellation_type::terminal);
      74         1655 :         });
      75         1656 :     }
      76              : 
      77         2764 :     void end() {
      78         2764 :         std::lock_guard lock(state->mutex);
      79         2764 :         ended = true;
      80         2764 :         if (!cancelled && state->active > 0) --state->active;
      81         2764 :         state->requests.erase(position);
      82         2764 :         state->changed.notify_all();
      83         2764 :     }
      84              : };
      85              : 
      86              : } // namespace detail
      87              : 
      88          625 : async_client::async_client(asio::workers& workers)
      89          625 :     : state_(std::make_shared<detail::client_state>(workers)), client_(*state_) {
      90          625 :     LOG_DEBUG("Created HTTP async client");
      91          625 : }
      92              : 
      93          625 : async_client::~async_client() {
      94          625 :     LOG_DEBUG("Destroying HTTP async client");
      95          625 :     running_ = false;
      96              :     // The idle connections close now, those in use when their requests give them back: they
      97              :     // belong to the io_contexts of the workers, which the requests may outlive as frames
      98          625 :     client_.max_idle_connections_per_host(0);
      99          625 :     close_requests(true);
     100          625 : }
     101              : 
     102        27640 : async_client::request_end::~request_end() {
     103        27640 :     if (request_) request_->end();
     104        27640 : }
     105              : 
     106        22159 : async_client::callback_scope::callback_scope(detail::client_request& request) : request_(request) {
     107        22159 :     auto& state = *request.state;
     108        22159 :     std::lock_guard lock(state.mutex);
     109        22159 :     entered_ = !request.cancelled;
     110        22159 :     if (entered_) state.callbacks.push_back(std::this_thread::get_id());
     111        22159 : }
     112              : 
     113        43931 : async_client::callback_scope::~callback_scope() {
     114        22159 :     if (!entered_) return;
     115        21772 :     auto& state = *request_.state;
     116        21772 :     std::lock_guard lock(state.mutex);
     117        21772 :     state.callbacks.erase(std::find(state.callbacks.begin(), state.callbacks.end(), std::this_thread::get_id()));
     118        21772 :     state.changed.notify_all();
     119        22159 : }
     120              : 
     121         1538 : async_client::request_context::request_context(detail::client_request& request)
     122         1538 :     : client(*request.state), request_(request) {}
     123              : 
     124            9 : stream_callback async_client::request_context::chunks(stream_callback callback) const {
     125           18 :     return [&request = request_, callback = std::move(callback)](const stream_info& info) {
     126        20634 :         callback_scope running(request);
     127        41268 :         return running && callback(info);
     128        20652 :     };
     129              : }
     130              : 
     131         2776 : std::shared_ptr<detail::client_request> async_client::start_request() {
     132         2776 :     auto& io_context = state_->workers.get_thread_io_context();
     133         2776 :     std::lock_guard lock(state_->mutex);
     134         2776 :     if (state_->closers > 0) {
     135           12 :         LOG_DEBUG("Async client: stopping, a request started now does not run");
     136           12 :         return nullptr;
     137              :     }
     138         2764 :     auto request = std::make_shared<detail::client_request>(state_, io_context.get_executor());
     139         2764 :     request->position = state_->requests.insert(state_->requests.end(), request);
     140         2764 :     ++state_->active;
     141         2764 :     return request;
     142         2776 : }
     143              : 
     144         2764 : boost::asio::io_context::executor_type async_client::executor_of(const detail::client_request& request) {
     145         2764 :     return request.executor;
     146              : }
     147              : 
     148         2764 : boost::asio::cancellation_slot async_client::slot_of(detail::client_request& request) {
     149         2764 :     return request.signal.slot();
     150              : }
     151              : 
     152         2764 : void async_client::spawned(const std::shared_ptr<detail::client_request>& request) {
     153              :     bool cancel;
     154              :     {
     155         2764 :         std::lock_guard lock(request->state->mutex);
     156         2764 :         request->spawned = true;
     157         2764 :         cancel = request->cancelled;
     158         2764 :     }
     159         2764 :     if (cancel) detail::client_request::cancel(request);
     160         2764 : }
     161              : 
     162         2765 : bool async_client::cancelled(detail::client_request& request) {
     163         2765 :     std::lock_guard lock(request.state->mutex);
     164         2765 :     return request.cancelled;
     165         2765 : }
     166              : 
     167          763 : void async_client::close_requests(bool final) {
     168          763 :     auto& state = *state_;
     169              :     // The requests this call waits for (any cancelled before too, e.g. from a worker, which did
     170              :     // not wait for them), and those it cancels: one not spawned yet is cancelled by spawned()
     171          763 :     std::vector<std::shared_ptr<detail::client_request>> ending;
     172          763 :     std::vector<std::shared_ptr<detail::client_request>> cancelling;
     173              :     {
     174          763 :         std::lock_guard lock(state.mutex);
     175          763 :         ++state.closers;
     176         2518 :         for (const auto& request : state.requests) {
     177         1755 :             if (request->left_behind) continue;
     178         1755 :             ending.push_back(request);
     179         1755 :             if (request->cancelled) continue;
     180         1656 :             request->cancelled = true;
     181         1656 :             if (request->spawned) cancelling.push_back(request);
     182              :         }
     183              :         // Release wait() and wait_for(): the client no longer runs
     184          763 :         state.changed.notify_all();
     185          763 :     }
     186         2419 :     for (const auto& request : cancelling) detail::client_request::cancel(request);
     187              : 
     188              :     // No request calls its callback from now on, so those running only end. They are waited for
     189              :     // unless stop() runs on a worker: the client lives on, and two callbacks stopping it at once
     190              :     // would wait for each other. The cancelled requests end then (their worker may have been
     191              :     // running a callback), unless this is one of the workers: those of its thread could not run
     192              :     // meanwhile, and those of other threads may use connections of this one
     193          763 :     bool on_worker = state.workers.in_worker_thread();
     194          763 :     std::unique_lock lock(state.mutex);
     195         1448 :     if (final || !on_worker) state.changed.wait(lock, [&] { return !state.callbacks_elsewhere(); });
     196          763 :     if (!on_worker && state.workers.running()) {
     197          530 :         auto has_ended = [](const auto& request) { return request->ended.load(); };
     198          523 :         state.changed.wait_for(lock, detail::end_grace, [&] {
     199          651 :             return std::all_of(ending.begin(), ending.end(), has_ended);
     200              :         });
     201          655 :         for (const auto& request : ending) {
     202          132 :             if (!has_ended(request)) request->left_behind = true;
     203              :         }
     204              :     }
     205              : 
     206          763 :     state.active = 0;
     207          763 :     if (!final) --state.closers;
     208          763 :     state.changed.notify_all();
     209          763 : }
     210              : 
     211            0 : void async_client::track_request_start() {
     212            0 :     std::lock_guard lock(state_->mutex);
     213            0 :     ++state_->active;
     214            0 : }
     215              : 
     216            0 : void async_client::track_request_end() {
     217            0 :     std::lock_guard lock(state_->mutex);
     218            0 :     if (state_->active > 0 && --state_->active == 0) state_->changed.notify_all();
     219            0 : }
     220              : 
     221           60 : size_t async_client::pending_requests() const {
     222          120 :     return state_->active.load();
     223              : }
     224              : 
     225          138 : bool async_client::stop() {
     226          138 :     LOG_DEBUG("Stopping HTTP async client");
     227          138 :     bool result = running_.exchange(false);
     228          138 :     close_requests(false);
     229              :     // The idle connections belong to the io_contexts of the workers, which may go away
     230          138 :     clear_connections();
     231          138 :     return result;
     232              : }
     233              : 
     234          361 : void async_client::wait() {
     235          361 :     std::unique_lock lock(state_->mutex);
     236         1711 :     state_->changed.wait(lock, [this] { return state_->active == 0 || !running_; });
     237          361 : }
     238              : 
     239           48 : bool async_client::wait_for(std::chrono::milliseconds timeout) {
     240           48 :     std::unique_lock lock(state_->mutex);
     241          276 :     bool completed = state_->changed.wait_for(lock, timeout, [this] { return state_->active == 0 || !running_; });
     242           96 :     return completed && state_->active == 0;
     243           48 : }
     244              : 
     245              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1