LCOV - code coverage report
Current view: top level - model - Map.hpp (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 99 99
Test Date: 2026-10-04 17:31:08 Functions: 93.9 % 163 153

            Line data    Source code
       1              : /*
       2              :  ___________________________________________
       3              : |    _     ___                        _     |
       4              : |   | |   |__ \                      | |    |
       5              : |   | |__    ) |__ _  __ _  ___ _ __ | |_   |
       6              : |   | '_ \  / // _` |/ _` |/ _ \ '_ \| __|  |  HTTP/2 AGENT FOR MOCK TESTING
       7              : |   | | | |/ /| (_| | (_| |  __/ | | | |_   |  Version 0.0.z
       8              : |   |_| |_|____\__,_|\__, |\___|_| |_|\__|  |  https://github.com/testillano/h2agent
       9              : |                     __/ |                 |
      10              : |                    |___/                  |
      11              : |___________________________________________|
      12              : 
      13              : Licensed under the MIT License <http://opensource.org/licenses/MIT>.
      14              : SPDX-License-Identifier: MIT
      15              : Copyright (c) 2021 Eduardo Ramos
      16              : 
      17              : Permission is hereby  granted, free of charge, to any  person obtaining a copy
      18              : of this software and associated  documentation files (the "Software"), to deal
      19              : in the Software  without restriction, including without  limitation the rights
      20              : to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
      21              : copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
      22              : furnished to do so, subject to the following conditions:
      23              : 
      24              : The above copyright notice and this permission notice shall be included in all
      25              : copies or substantial portions of the Software.
      26              : 
      27              : THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
      28              : IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
      29              : FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
      30              : AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
      31              : LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      32              : OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
      33              : SOFTWARE.
      34              : */
      35              : 
      36              : #pragma once
      37              : 
      38              : // Better unordered_map than map:
      39              : // Slighly more memory consumption (not significative in load tests) due to the hash map.
      40              : // But order is not important for us, and the size is not very big (prune is normally
      41              : //  applied in load test provisions), so the cache is not used.
      42              : // As insertion and deletion are equally fast for both containers, we focus on search
      43              : //  (O(log2(n)) for map as binary tree, O(1) constant as average (O(n) in worst case)
      44              : //  for unordered map as hash table), so for our case, unordered_map seems to be the best choice.
      45              : #include <array>
      46              : #include <functional>
      47              : #include <unordered_map>
      48              : 
      49              : #include <common.hpp>
      50              : 
      51              : #include <nlohmann/json.hpp>
      52              : 
      53              : 
      54              : namespace h2agent
      55              : {
      56              : namespace model
      57              : {
      58              : 
      59              : /**
      60              :  * Thread-safe associative map with OPT-IN mutex sharding.
      61              :  *
      62              :  * @tparam Key    key type
      63              :  * @tparam Value  value type
      64              :  * @tparam Shards number of independent lock shards (default 1).
      65              :  *
      66              :  * With \c Shards == 1 (the default) this is byte-for-byte the historical
      67              :  * single-\c shared_mutex map: every existing user (event stores, etc.) keeps
      68              :  * exactly the old behaviour with zero extra overhead.
      69              :  *
      70              :  * With \c Shards > 1 the keyspace is partitioned across N independent
      71              :  * (\c mutex, \c unordered_map) shards, routed by \c std::hash(key) % Shards:
      72              :  * - PER-KEY operations (get/tryGet/exists/add/remove/modifyOrInsert) lock ONLY
      73              :  *   the shard owning the key, so writes to independent keys from different
      74              :  *   threads do not serialize against each other. This is the hot-traffic win
      75              :  *   (e.g. a provision touching many vault keys under high concurrency).
      76              :  * - WHOLE-MAP operations (size/empty/forEach/getJson/clear/copy) must observe
      77              :  *   every shard, so they lock ALL shards in a FIXED ascending order (which is
      78              :  *   deadlock-free). These are administrative/report paths (e.g.
      79              :  *   GET /admin/v1/vault), NOT the traffic hot path, so global locking there is
      80              :  *   acceptable by design.
      81              :  *
      82              :  * Per-key atomicity of \c modifyOrInsert (read-modify-write under one lock) is
      83              :  * preserved in both modes.
      84              :  */
      85              : template<typename Key, typename Value, std::size_t Shards = 1>
      86              : class Map {
      87              : 
      88              :     static_assert(Shards >= 1, "Map requires at least one shard");
      89              : 
      90              :     typedef typename std::unordered_map<Key, Value> map_t;
      91              :     using IterationCallback = std::function<void(const Key&, const Value&)>;
      92              : 
      93              :     struct Shard {
      94              :         mutable mutex_t mutex_{};
      95              :         map_t map_{};
      96              :     };
      97              : 
      98              :     std::array<Shard, Shards> shards_{};
      99              : 
     100              :     // Route a key to its shard. For Shards==1 this is a no-op (index 0), so the
     101              :     // hash is not even computed on the single-shard hot path.
     102       117351 :     Shard& shardFor(const Key& key) {
     103          753 :         if constexpr (Shards == 1) return shards_[0];
     104       116598 :         else return shards_[std::hash<Key>{}(key) % Shards];
     105              :     }
     106          990 :     const Shard& shardFor(const Key& key) const {
     107          440 :         if constexpr (Shards == 1) return shards_[0];
     108          550 :         else return shards_[std::hash<Key>{}(key) % Shards];
     109              :     }
     110              : 
     111              : public:
     112              : 
     113        19746 :     Map() {};
     114              : 
     115              :     /** copy constructor: snapshot every shard of 'other' under read locks
     116              :      * (fixed ascending order), then copy into our matching shards. */
     117            9 :     Map(const Map& other) {
     118            9 :         for (std::size_t i = 0; i < Shards; ++i) {
     119            8 :             read_guard_t guard(other.shards_[i].mutex_);
     120            8 :             shards_[i].map_ = other.shards_[i].map_;
     121              :         }
     122            1 :     }
     123              : 
     124         2713 :     ~Map() = default;
     125              : 
     126              :     // getters
     127              : 
     128          169 :     bool exists(const Key& key) const
     129              :     {
     130          169 :         const Shard& s = shardFor(key);
     131          169 :         read_guard_t guard(s.mutex_);
     132          338 :         return (s.map_.find(key) != s.map_.end());
     133          169 :     }
     134              : 
     135              :     /**
     136              :      * Searchs map key
     137              :      *
     138              :      * @param key key to find
     139              :      * @param exits written by reference
     140              :      * @return Value for provided key, or initizalized Value when key is missing
     141              :      */
     142          640 :     Value get(const Key& key, bool &exists) const
     143              :     {
     144          640 :         const Shard& s = shardFor(key);
     145          640 :         read_guard_t guard(s.mutex_);
     146          640 :         auto it = s.map_.find(key);
     147          640 :         exists = (it != s.map_.end());
     148         1280 :         return (exists ? it->second : Value{}); // return copy
     149          640 :     }
     150              : 
     151              :     /**
     152              :      * Getter which avoid copy of empty value and is more readable than get()
     153              :      */
     154          181 :     bool tryGet(const Key& key, Value& out_value) const {
     155          181 :         const Shard& s = shardFor(key);
     156          181 :         read_guard_t guard(s.mutex_);
     157          181 :         auto it = s.map_.find(key);
     158          181 :         if (it != s.map_.end()) {
     159          108 :             out_value = it->second;
     160          108 :             return true;
     161              :         }
     162           73 :         return false;
     163          181 :     }
     164              : 
     165              :     /** map size (sum across shards; each shard read-locked in turn) */
     166          373 :     size_t size() const
     167              :     {
     168          373 :         size_t total = 0;
     169         1554 :         for (const auto& s : shards_) {
     170         1181 :             read_guard_t guard(s.mutex_);
     171         1181 :             total += s.map_.size();
     172              :         }
     173          373 :         return total;
     174              :     }
     175              : 
     176           16 :     bool empty() const
     177              :     {
     178          328 :         for (const auto& s : shards_) {
     179          163 :             read_guard_t guard(s.mutex_);
     180          163 :             if (!s.map_.empty()) return false;
     181              :         }
     182            2 :         return true;
     183              :     }
     184              : 
     185              :     /**
     186              :      * @brief Iterates safely over all elements in the map.
     187              :      * * This method provides **read access** to the map's elements in a **thread-safe** manner
     188              :      * by applying a user-defined callback function to each key-value pair. With sharding, ALL
     189              :      * shards are read-locked in fixed ascending order for the full iteration (a consistent
     190              :      * snapshot). Admin/report path -- keep callbacks short.
     191              :      *
     192              :      * @param callback A function applied to every key-value pair, signature void(const Key&, const Value&).
     193              :      *
     194              :      * @note This function uses the callback pattern to prevent the exposure of unsafe iterators
     195              :      * (\c dangling iterators) to external threads.
     196              :      */
     197           68 :     void forEach(const IterationCallback& callback) const {
     198              :         // Lock all shards (ascending order) for a consistent whole-map view.
     199           68 :         std::array<read_guard_t, Shards> guards = lockAllShared();
     200          623 :         for (const auto& s : shards_) {
     201          670 :             for (const auto& pair : s.map_) {
     202          115 :                 callback(pair.first, pair.second);
     203              :             }
     204              :         }
     205           68 :     }
     206              : 
     207              :     /**
     208              :      * @brief Safely converts the internal map content into a JSON object.
     209              :      *
     210              :      * Locks ALL shards (read, fixed ascending order) for the duration so the
     211              :      * serialized object is a consistent snapshot across the whole keyspace.
     212              :      * Administrative path (e.g. GET /admin/v1/vault), not the traffic hot path.
     213              :      *
     214              :      * @return nlohmann::json A new copy of the map's content as a JSON object.
     215              :      */
     216           71 :     nlohmann::json getJson() const {
     217           71 :         std::array<read_guard_t, Shards> guards = lockAllShared();
     218              :         if constexpr (Shards == 1) {
     219            2 :             return nlohmann::json(shards_[0].map_);  // return copy
     220              :         } else {
     221           70 :             nlohmann::json j = nlohmann::json::object();
     222         1182 :             for (const auto& s : shards_) {
     223       163658 :                 for (const auto& pair : s.map_) {
     224       162546 :                     j[pair.first] = pair.second;
     225              :                 }
     226              :             }
     227          140 :             return j;
     228           70 :         }
     229           71 :     }
     230              : 
     231              :     // setters
     232              : 
     233              :     /**
     234              :      * Adds a new value to the map (Lvalue variant). Locks only the key's shard.
     235              :      */
     236          888 :     void add(const Key& key, const Value &value) {
     237          888 :         Shard& s = shardFor(key);
     238          888 :         write_guard_t guard(s.mutex_);
     239          888 :         s.map_.insert_or_assign(key, value);
     240          888 :     }
     241              : 
     242              :     // Rvalue variant (std::move)
     243          496 :     void add(const Key& key, Value&& value) {
     244          496 :         Shard& s = shardFor(key);
     245          496 :         write_guard_t guard(s.mutex_);
     246          496 :         s.map_.insert_or_assign(key, std::move(value));
     247          496 :     }
     248              : 
     249              :     /**
     250              :      * Atomically reads, modifies and writes back a value under a single shard
     251              :      * write lock. If the key doesn't exist, a default-constructed Value is
     252              :      * passed to the modifier.
     253              :      *
     254              :      * @param key key to modify
     255              :      * @param modifier function that receives a reference to the value and modifies it in place
     256              :      */
     257              :     template<typename Modifier>
     258       115975 :     void modifyOrInsert(const Key& key, Modifier&& modifier) {
     259       115975 :         Shard& s = shardFor(key);
     260       115861 :         write_guard_t guard(s.mutex_);
     261       117880 :         modifier(s.map_[key]); // operator[] inserts default if missing
     262       116740 :     }
     263              : 
     264              :     /**
     265              :      * Adds another map of same kind to the map. Each entry is routed to its own
     266              :      * shard and locked individually.
     267              :      *
     268              :      * @param m map to add
     269              :      */
     270              :     void add(const map_t& m)
     271              :     {
     272              :         for (const auto& kv : m) {
     273              :             Shard& s = shardFor(kv.first);
     274              :             write_guard_t guard(s.mutex_);
     275              :             s.map_.insert_or_assign(kv.first, kv.second);
     276              :         }
     277              :     }
     278              : 
     279              :     /**
     280              :      * Removes key (locks only the key's shard)
     281              :      *
     282              :      * @param key key to remove
     283              :      */
     284           11 :     void remove(const Key& key, bool &exists)
     285              :     {
     286           11 :         Shard& s = shardFor(key);
     287           11 :         write_guard_t guard(s.mutex_);
     288           11 :         exists = (s.map_.erase(key) > 0);
     289           11 :     }
     290              : 
     291              :     /** Clear map (locks ALL shards for write, fixed ascending order).
     292              :      *  @return true if something was deleted */
     293           31 :     bool clear()
     294              :     {
     295           31 :         std::array<write_guard_t, Shards> guards = lockAllExclusive();
     296           31 :         bool result = false;
     297          136 :         for (auto& s : shards_) {
     298          105 :             if (!s.map_.empty()) result = true;
     299          105 :             s.map_.clear();
     300              :         }
     301           31 :         return result;
     302           31 :     }
     303              : 
     304              : private:
     305              :     // Acquire a read lock on every shard in fixed ascending order (deadlock-free).
     306          139 :     std::array<read_guard_t, Shards> lockAllShared() const {
     307          139 :         return lockAllSharedImpl(std::make_index_sequence<Shards>{});
     308              :     }
     309              :     template<std::size_t... I>
     310          139 :     std::array<read_guard_t, Shards> lockAllSharedImpl(std::index_sequence<I...>) const {
     311          139 :         return { read_guard_t(shards_[I].mutex_)... };
     312          139 :     }
     313              : 
     314              :     // Acquire a write lock on every shard in fixed ascending order (deadlock-free).
     315           31 :     std::array<write_guard_t, Shards> lockAllExclusive() {
     316           31 :         return lockAllExclusiveImpl(std::make_index_sequence<Shards>{});
     317              :     }
     318              :     template<std::size_t... I>
     319           31 :     std::array<write_guard_t, Shards> lockAllExclusiveImpl(std::index_sequence<I...>) {
     320           31 :         return { write_guard_t(shards_[I].mutex_)... };
     321           31 :     }
     322              : };
     323              : 
     324              : }
     325              : }
     326              : 
        

Generated by: LCOV version 2.0-1