/*
This file is part of libhttpserver
Copyright (C) 2011-2026 Sebastiano Merlino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
*/
// LRU cache fronting the 3-tier route table.
//
// Plain std::mutex (not shared_mutex) because every cache touch the
// LRU promotion on a hit included is a write (std::list::splice).
// Lock-order discipline: route_table_mutex_ is always acquired BEFORE
// the cache's internal mutex when both are held.
//
// Internal header only reachable when compiling libhttpserver.
#if !defined(HTTPSERVER_COMPILATION)
#error "route_cache.hpp is internal; only reachable when compiling libhttpserver."
#endif
#ifndef SRC_HTTPSERVER_DETAIL_ROUTE_CACHE_HPP_
#define SRC_HTTPSERVER_DETAIL_ROUTE_CACHE_HPP_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "httpserver/http_method.hpp"
#include "httpserver/detail/route_entry.hpp"
namespace httpserver {
namespace detail {
// (method, path) cache key. Hashed by combining the path's string hash
// with the method enum value via the boost-style mix-shift.
struct cache_key {
http_method method = http_method::get;
std::string path;
friend bool operator==(const cache_key& a, const cache_key& b) noexcept {
return a.method == b.method && a.path == b.path;
}
};
struct cache_key_hash {
// Golden-ratio mix constant: reduces hash clustering vs. plain XOR.
static constexpr std::size_t kHashMix = 0x9e3779b97f4a7c15ULL;
// Keep this formula in sync with the inline string_view copy in
// route_cache::find_by_view below -- the two must agree for the
// manual bucket probe to land where insert() put the entry.
std::size_t operator()(const cache_key& k) const noexcept {
std::size_t h1 = std::hash{}(k.path);
std::size_t h2 = static_cast(k.method);
return h1 ^ (h2 + kHashMix + (h1 > 2));
}
};
// cache_value: the hit payload. Carries a copy of the route_entry (one
// shared_ptr ref-bump for the class arm; std::function copy for the
// lambda arm) and the parameter capture vector so the cache hit can
// replay parameter binding without re-walking the segment trie.
struct cache_value {
route_entry entry;
// Read in src/webserver.cpp at the cache-hit replay site
// (`result.captured_params = std::move(cached.captured_params)`); cppcheck
// analyses each TU in isolation and does not see the cross-TU read.
// cppcheck-suppress unusedStructMember
std::vector captured_params;
};
// route_cache: bounded LRU front-end for the tier chain. Bounded to a
// configurable max-size (default 256 per architecture spec). Insertion
// of a new key evicts the back of the LRU list when the size cap is
// reached. find() promotes the hit to the front via splice().
class route_cache {
public:
explicit route_cache(std::size_t max_entries = 256)
: max_entries_(max_entries) {
if (max_entries == 0) {
throw std::invalid_argument(
"route_cache max_entries must be > 0");
}
}
// Find by key; returns true on hit and copies the value into `out`.
// Promotes the hit to the front of the LRU list as a side effect.
bool find(const cache_key& key, cache_value& out) {
std::lock_guard lock(mutex_);
auto it = map_.find(key);
if (it == map_.end()) return false;
list_.splice(list_.begin(), list_, it->second);
out = it->second->second;
return true;
}
// Zero-allocation warm-path variant: looks up without constructing a
// cache_key (avoids copying `path` into a std::string on every call,
// including every warm cache hit). Uses a compatible hash computed
// from (method, string_view) and a heterogeneous equality check.
// On hit, copies the value into `out` and promotes the entry.
// std::hash produces the same hash as
// std::hash for identical character sequences (C++17
// standard guarantee), so the probe always lands on the correct
// bucket.
bool find_by_view(http_method method, std::string_view path,
cache_value& out) {
// Compute the same hash as cache_key_hash without owning `path`
// (mirror of cache_key_hash::operator() -- keep in sync).
std::size_t h1 = std::hash{}(path);
std::size_t h2 = static_cast(method);
std::size_t bucket_hash = h1 ^ (h2 + cache_key_hash::kHashMix
+ (h1 > 2));
std::lock_guard lock(mutex_);
// Empty-cache early-out. On libc++ a default-constructed
// unordered_map has bucket_count() == 0, and calling begin(0) /
// end(0) on it dereferences a null bucket-list pointer (UB);
// the first request against a fresh server hits exactly this.
if (map_.bucket_count() == 0) {
return false;
}
// Walk the target bucket manually via the bucket-iterator
// overloads begin(b)/end(b) so no full cache_key is constructed.
// ASSUMPTION: bucket index == hash % bucket_count(). The
// standard does not mandate that mapping, but libstdc++ and
// libc++ both use it; on an implementation that maps
// differently this probe would merely report a miss (the miss
// path re-walks the tiers and re-inserts), costing hit rate,
// never correctness.
std::size_t b = bucket_hash % map_.bucket_count();
for (auto it = map_.begin(b), end = map_.end(b); it != end; ++it) {
if (it->first.method == method && it->first.path == path) {
// Promote using the mutable bucket iterator directly
// avoids a second map_.find() call on every cache hit.
list_.splice(list_.begin(), list_, it->second);
out = it->second->second;
return true;
}
}
return false;
}
// Insert (or replace) the entry for `key`. Evicts the LRU back if
// the size cap is reached.
void insert(const cache_key& key, cache_value value) {
std::lock_guard lock(mutex_);
auto it = map_.find(key);
if (it != map_.end()) {
// Replace in place; promote.
it->second->second = std::move(value);
list_.splice(list_.begin(), list_, it->second);
return;
}
list_.emplace_front(key, std::move(value));
map_[key] = list_.begin();
if (map_.size() > max_entries_) {
auto& back = list_.back();
map_.erase(back.first);
list_.pop_back();
}
}
// Note: not noexcept std::lock_guard can throw std::system_error
// (though in practice it does not on POSIX under normal conditions).
void clear() {
std::lock_guard lock(mutex_);
map_.clear();
list_.clear();
}
std::size_t size() const {
std::lock_guard lock(mutex_);
return map_.size();
}
private:
using list_t = std::list;
mutable std::mutex mutex_;
std::size_t max_entries_;
list_t list_;
std::unordered_map map_;
};
} // namespace detail
} // namespace httpserver
#endif // SRC_HTTPSERVER_DETAIL_ROUTE_CACHE_HPP_