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

            Line data    Source code
       1              : #include "connection_pool.hpp"
       2              : #include "../../util/logger.hpp"
       3              : 
       4              : #include <algorithm>
       5              : 
       6              : namespace thinger::http {
       7              : 
       8              : namespace {
       9              : 
      10              :     // A key for the logs, without its last field: the credentials of a proxy, if it has one
      11              :     // (see proxy_endpoint::key())
      12         3945 :     [[maybe_unused]] std::string_view loggable(std::string_view key) {
      13         3945 :         return key.substr(0, key.rfind('|'));
      14              :     }
      15              : 
      16              : }
      17              : 
      18              : struct connection_pool::state : std::enable_shared_from_this<state> {
      19              :     struct idle_entry {
      20              :         std::string key;
      21              :         std::shared_ptr<client_connection> connection;
      22              :         std::chrono::steady_clock::time_point expires;
      23              :         uint64_t id;
      24              :     };
      25              :     using idle_list = std::list<idle_entry>;
      26              : 
      27              :     struct host {
      28              :         std::string origin;
      29              :         std::vector<idle_list::iterator> idle;     // oldest first
      30              :         size_t active = 0;
      31              :     };
      32              : 
      33              :     mutable std::mutex mutex;
      34              :     config limits;
      35              :     idle_list idle;                                 // oldest first, of every key
      36              :     std::unordered_map<std::string, host> hosts;
      37              :     uint64_t next_id = 0;
      38              :     size_t created = 0;
      39              :     size_t reused = 0;
      40              :     bool closed = false;
      41              :     bool timers = true;
      42              : 
      43              :     // Take an idle connection out of the pool (lock held), to close or lend
      44        48111 :     std::shared_ptr<client_connection> remove(idle_list::iterator entry) {
      45        48111 :         auto& entries = hosts[entry->key].idle;
      46        48111 :         entries.erase(std::find(entries.begin(), entries.end(), entry));
      47        48111 :         auto connection = std::move(entry->connection);
      48        48111 :         idle.erase(entry);
      49        48111 :         return connection;
      50              :     }
      51              : 
      52              :     // Forget a key with nothing idle nor lent (lock held)
      53        18774 :     void prune(const std::string& key) {
      54        18774 :         auto it = hosts.find(key);
      55        18774 :         if (it != hosts.end() && it->second.idle.empty() && it->second.active == 0) hosts.erase(it);
      56        18774 :     }
      57              : 
      58              :     // Take the expired idle connections out, to close (lock held)
      59        59029 :     void sweep(std::chrono::steady_clock::time_point now, std::vector<std::shared_ptr<client_connection>>& closing) {
      60       408324 :         for (auto it = idle.begin(); it != idle.end();) {
      61       349295 :             auto entry = it++;
      62       349295 :             if (entry->expires > now) continue;
      63           33 :             auto key = entry->key;
      64           33 :             closing.push_back(remove(entry));
      65           33 :             prune(key);
      66           33 :         }
      67        59029 :     }
      68              : 
      69              :     // Close the oldest idle connections beyond the limits (lock held)
      70          928 :     void trim(std::vector<std::shared_ptr<client_connection>>& closing) {
      71         1313 :         for (auto& [key, entry] : hosts) {
      72         1329 :             while (entry.idle.size() > limits.max_idle_per_host) closing.push_back(remove(entry.idle.front()));
      73              :         }
      74          928 :         while (idle.size() > limits.max_idle) closing.push_back(remove(idle.begin()));
      75         1313 :         for (auto it = hosts.begin(); it != hosts.end();) {
      76          430 :             it = it->second.idle.empty() && it->second.active == 0 ? hosts.erase(it) : std::next(it);
      77              :         }
      78          928 :     }
      79              : 
      80              :     // The server closes a connection once idle for the time it announced: it is not reused
      81              :     // near that time, as undici does, nor at all if that is too short
      82        56278 :     std::chrono::milliseconds idle_time_for(std::optional<std::chrono::seconds> announced) const {
      83        56278 :         auto timeout = limits.idle_timeout;
      84        56278 :         if (announced) {
      85           90 :             timeout = std::min(timeout, std::chrono::duration_cast<std::chrono::milliseconds>(*announced) -
      86           45 :                                             keep_alive_margin);
      87              :         }
      88        56278 :         return timeout;
      89              :     }
      90              : 
      91              :     // Room for one more idle connection of `key`, closing the oldest idle ones: the most
      92              :     // likely to be closed by their servers soon (lock held)
      93        49771 :     void make_room(const std::string& key, host& entry, std::vector<std::shared_ptr<client_connection>>& closing) {
      94        50788 :         while (entry.idle.size() >= limits.max_idle_per_host) closing.push_back(remove(entry.idle.front()));
      95        50773 :         while (idle.size() >= limits.max_idle) {
      96         1002 :             auto oldest = idle.front().key;
      97         1002 :             closing.push_back(remove(idle.begin()));
      98         1002 :             if (oldest != key) prune(oldest);
      99         1002 :         }
     100        49771 :     }
     101              : 
     102              :     // Keep a connection idle for `timeout` (lock held)
     103        49771 :     void park(const std::string& key, host& entry, const std::shared_ptr<client_connection>& connection,
     104              :               std::chrono::milliseconds timeout) {
     105        49771 :         auto id = ++next_id;
     106        49771 :         connection->unclaim();
     107        49771 :         if (timers) {
     108        47543 :             connection->expire_after(timeout, [pool = weak_from_this(), key, id]() {
     109           18 :                 if (auto self = pool.lock()) self->expire(key, id);
     110           18 :             });
     111              :         }
     112        49771 :         idle.push_back(idle_entry{key, connection, std::chrono::steady_clock::now() + timeout, id});
     113        99542 :         entry.idle.push_back(std::prev(idle.end()));
     114        49771 :     }
     115              : 
     116        56278 :     void release(const std::string& key, std::shared_ptr<client_connection> connection) {
     117              :         // The connection is not shared with another request now
     118        56278 :         bool reusable = connection->reusable();
     119        56278 :         auto announced = connection->keep_alive_timeout();
     120        56278 :         bool keep = false;
     121        56278 :         std::vector<std::shared_ptr<client_connection>> closing;
     122              :         {
     123        56278 :             std::lock_guard lock(mutex);
     124              :             // Without timers, the expired ones are closed as a request ends too, before they
     125              :             // take the room of this one. This key is not pruned: the lease counts as active
     126        56278 :             if (!timers) sweep(std::chrono::steady_clock::now(), closing);
     127        56278 :             if (auto it = hosts.find(key); it != hosts.end()) {
     128        56278 :                 if (it->second.active > 0) --it->second.active;
     129        56278 :                 auto timeout = idle_time_for(announced);
     130       106052 :                 keep = reusable && !closed && timeout.count() > 0 && limits.max_idle_per_host > 0 &&
     131        49774 :                        limits.max_idle > 0;
     132        56278 :                 if (keep) {
     133        49771 :                     make_room(key, it->second, closing);
     134        49771 :                     park(key, it->second, connection, timeout);
     135              :                 } else {
     136         6507 :                     prune(key);
     137              :                 }
     138              :             }
     139        56278 :         }
     140              :         // Not reusable, it is already closed, or belongs to a new protocol (a 101 response)
     141        56278 :         if (reusable && !keep) connection->close();
     142        58306 :         for (auto& stale : closing) stale->close();
     143        56278 :     }
     144              : 
     145              :     // The idle timeout of a connection elapsed (on its io_context)
     146           18 :     void expire(const std::string& key, uint64_t id) {
     147           18 :         std::shared_ptr<client_connection> connection;
     148              :         {
     149           18 :             std::lock_guard lock(mutex);
     150           18 :             auto it = hosts.find(key);
     151           18 :             if (it == hosts.end()) return;
     152           18 :             auto& entries = it->second.idle;
     153           30 :             auto entry = std::find_if(entries.begin(), entries.end(), [id](auto e) { return e->id == id; });
     154              :             // Lent again, or closed, meanwhile
     155           18 :             if (entry == entries.end()) return;
     156           12 :             connection = remove(*entry);
     157           12 :             prune(key);
     158           18 :         }
     159           12 :         LOG_DEBUG("closing a connection idle for too long ({})", loggable(key));
     160           12 :         connection->close();
     161           18 :     }
     162              : };
     163              : 
     164        10529 : connection_pool::lease& connection_pool::lease::operator=(lease&& other) noexcept {
     165        10529 :     if (this != &other) {
     166        10529 :         release();
     167        10529 :         pool_ = std::move(other.pool_);
     168        10529 :         key_ = std::move(other.key_);
     169        10529 :         connection_ = std::move(other.connection_);
     170              :     }
     171        10529 :     return *this;
     172              : }
     173              : 
     174       140159 : void connection_pool::lease::release() {
     175       140159 :     if (!connection_) return;
     176        56281 :     auto connection = std::move(connection_);
     177        56281 :     connection_.reset();
     178        56281 :     if (auto pool = pool_.lock()) {
     179        56278 :         pool->release(key_, std::move(connection));
     180            3 :     } else if (connection->reusable()) {
     181            3 :         connection->close();
     182        56281 :     }
     183        56281 : }
     184              : 
     185         2764 : connection_pool::connection_pool() : state_(std::make_shared<state>()) {
     186         2764 : }
     187              : 
     188         2764 : connection_pool::~connection_pool() {
     189              :     {
     190         2764 :         std::lock_guard lock(state_->mutex);
     191         2764 :         state_->closed = true;
     192         2764 :     }
     193              :     // Nothing can reach the idle connections anymore. Those lent are closed by their
     194              :     // requests: they are not kept once given back
     195         2764 :     clear();
     196         2764 : }
     197              : 
     198          928 : void connection_pool::configure(const config& config) {
     199          928 :     std::vector<std::shared_ptr<client_connection>> closing;
     200              :     {
     201          928 :         std::lock_guard lock(state_->mutex);
     202          928 :         state_->limits = config;
     203          928 :         state_->trim(closing);
     204          928 :     }
     205         1872 :     for (auto& connection : closing) connection->close();
     206          928 : }
     207              : 
     208         2103 : void connection_pool::use_idle_timers(bool enabled) {
     209         2103 :     std::lock_guard lock(state_->mutex);
     210         2103 :     state_->timers = enabled;
     211         2103 : }
     212              : 
     213          904 : connection_pool::config connection_pool::get_config() const {
     214          904 :     std::lock_guard lock(state_->mutex);
     215         1808 :     return state_->limits;
     216          904 : }
     217              : 
     218        56167 : connection_pool::lease connection_pool::acquire(const std::string& key) {
     219        56167 :     std::shared_ptr<client_connection> found;
     220        56167 :     std::vector<std::shared_ptr<client_connection>> closing;
     221              :     {
     222        56167 :         std::lock_guard lock(state_->mutex);
     223              :         // Expired ones of any key too, in case their timers did not run
     224        56167 :         state_->sweep(std::chrono::steady_clock::now(), closing);
     225        56167 :         if (auto it = state_->hosts.find(key); it != state_->hosts.end()) {
     226        52303 :             auto& entry = it->second;
     227              :             // The most recently used first, so the others expire if not needed. A server may
     228              :             // close an idle connection at any time: one it closed (or wrote to) is not used
     229        56437 :             while (!entry.idle.empty()) {
     230        45103 :                 auto connection = state_->remove(entry.idle.back());
     231        45103 :                 if (!connection->stale() && connection->claim()) {
     232        40969 :                     found = std::move(connection);
     233        40969 :                     break;
     234              :                 }
     235         4134 :                 closing.push_back(std::move(connection));
     236        45103 :             }
     237        52303 :             if (found) {
     238        40969 :                 ++entry.active;
     239        40969 :                 ++state_->reused;
     240              :             } else {
     241        11334 :                 state_->prune(key);
     242              :             }
     243              :         }
     244        56167 :     }
     245        56167 :     if (!closing.empty()) LOG_DEBUG("closing {} idle connections closed or expired ({})", closing.size(), loggable(key));
     246        60325 :     for (auto& connection : closing) connection->close();
     247        56167 :     if (!found) return {};
     248        40969 :     return lease(state_, key, std::move(found));
     249        56167 : }
     250              : 
     251        15312 : connection_pool::lease connection_pool::add(const std::string& key, const std::string& origin,
     252              :                                             std::shared_ptr<client_connection> connection) {
     253              :     {
     254        15312 :         std::lock_guard lock(state_->mutex);
     255        15312 :         auto& entry = state_->hosts[key];
     256        15312 :         if (entry.origin.empty()) entry.origin = origin;
     257        15312 :         ++entry.active;
     258        15312 :         ++state_->created;
     259        15312 :     }
     260        15312 :     connection->claim();
     261        15312 :     return lease(state_, key, std::move(connection));
     262              : }
     263              : 
     264          137 : size_t connection_pool::size() const {
     265          137 :     std::lock_guard lock(state_->mutex);
     266          274 :     return state_->idle.size();
     267          137 : }
     268              : 
     269          114 : connection_pool::stats connection_pool::get_stats() const {
     270          114 :     std::lock_guard lock(state_->mutex);
     271          114 :     stats result;
     272          114 :     result.created = state_->created;
     273          114 :     result.reused = state_->reused;
     274          229 :     for (const auto& [key, entry] : state_->hosts) {
     275          115 :         auto& host = result.hosts[entry.origin];
     276          115 :         host.idle += entry.idle.size();
     277          115 :         host.active += entry.active;
     278          115 :         result.idle += entry.idle.size();
     279          115 :         result.active += entry.active;
     280              :     }
     281          228 :     return result;
     282          114 : }
     283              : 
     284         7747 : void connection_pool::clear() {
     285         7747 :     std::vector<std::shared_ptr<client_connection>> closing;
     286              :     {
     287         7747 :         std::lock_guard lock(state_->mutex);
     288         9407 :         for (auto& entry : state_->idle) closing.push_back(std::move(entry.connection));
     289         7747 :         state_->idle.clear();
     290         9445 :         for (auto it = state_->hosts.begin(); it != state_->hosts.end();) {
     291         1698 :             it->second.idle.clear();
     292         1893 :             it = it->second.active == 0 ? state_->hosts.erase(it) : std::next(it);
     293              :         }
     294         7747 :     }
     295         9407 :     for (auto& connection : closing) connection->close();
     296         7747 : }
     297              : 
     298              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1