[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/etr/libhttpserver/master/src/httpserver/detail/route_cache.hpp [Back]  [Original]

/*
     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_

Web Proxy Viewer  |  New URL  |  Original Page