From 16a0fa5f7a9c9b4cbb984fd43b0307b78e6ae651 Mon Sep 17 00:00:00 2001 From: DrSocalkwe3n Date: Sun, 11 Jan 2026 22:28:03 +0600 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20IdProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 10 + Src/Client/AssetsManager.hpp | 10 +- Src/Common/IdProvider.hpp | 354 +++++++++++++++++++++-------------- 3 files changed, 229 insertions(+), 145 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be0a9f3..d9744ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,16 @@ FetchContent_Declare( FetchContent_MakeAvailable(Boost) target_link_libraries(luavox_common INTERFACE Boost::asio Boost::thread Boost::json Boost::iostreams Boost::interprocess Boost::timer Boost::circular_buffer Boost::lockfree Boost::stacktrace Boost::uuid Boost::serialization Boost::nowide) +# unordered_dense +FetchContent_Declare( + unordered_dense + GIT_REPOSITORY https://github.com/martinus/unordered_dense.git + GIT_TAG v4.8.1 +) +FetchContent_MakeAvailable(unordered_dense) + +target_link_libraries(luavox_common INTERFACE unordered_dense::unordered_dense) + # glm # find_package(glm REQUIRED) # target_include_directories(${PROJECT_NAME} PUBLIC ${GLM_INCLUDE_DIR}) diff --git a/Src/Client/AssetsManager.hpp b/Src/Client/AssetsManager.hpp index 223eeaf..91d5fc9 100644 --- a/Src/Client/AssetsManager.hpp +++ b/Src/Client/AssetsManager.hpp @@ -536,12 +536,14 @@ private: continue; } - const auto& dkTable = IdToDK[typeIndex]; std::string domain = "core"; std::string key; - if(id < dkTable.size()) { - domain = dkTable[id].Domain; - key = dkTable[id].Key; + { + auto d = getDK((EnumAssets) typeIndex, id); + if(d) { + domain = d->Domain; + key = d->Key; + } } std::u8string data = dataIter->second; diff --git a/Src/Common/IdProvider.hpp b/Src/Common/IdProvider.hpp index 287a63f..ad7eb57 100644 --- a/Src/Common/IdProvider.hpp +++ b/Src/Common/IdProvider.hpp @@ -2,206 +2,278 @@ #include "Common/Abstract.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace LV { -template +template class IdProvider { public: static constexpr size_t MAX_ENUM = static_cast(Enum::MAX_ENUM); - using IdTable = - std::unordered_map< - std::string, // Domain - std::unordered_map< - std::string, // Key - uint32_t, // ResourceId - detail::TSVHash, - detail::TSVEq - >, - detail::TSVHash, - detail::TSVEq - >; struct BindDomainKeyInfo { std::string Domain, Key; }; public: - IdProvider() { - std::fill(NextId.begin(), NextId.end(), 1); - for(size_t type = 0; type < static_cast(Enum::MAX_ENUM); ++type) { - DKToId[type]["core"]["none"] = 0; - IdToDK[type].emplace_back("core", "none"); + explicit IdProvider() { + for(size_t type = 0; type < MAX_ENUM; ++type) { + _NextId[type].store(1, std::memory_order_relaxed); + _Reverse[type].reserve(1024); + + IdToDK[type].push_back({"core", "none"}); + + auto& sh = _shardFor(static_cast(type), "core", "none"); + std::unique_lock lk(sh.mutex); + sh.map.emplace(Key{"core", "none"}, 0); } } + /* Находит или выдаёт идентификатор на запрошенный ресурс. Функция не требует внешней синхронизации. - Требуется периодически вызывать bake(). */ - inline ResourceId getId(EnumAssets type, std::string_view domain, std::string_view key) { - #ifndef NDEBUG + inline ResourceId getId(Enum type, std::string_view domain, std::string_view key) { +#ifndef NDEBUG assert(!DKToIdInBakingMode); - #endif +#endif + auto& sh = _shardFor(type, domain, key); - const auto& typeTable = DKToId[static_cast(type)]; - auto domainTable = typeTable.find(domain); + // 1) Поиск в режиме для чтения + { + std::shared_lock lk(sh.mutex); + if(auto it = sh.map.find(KeyView{domain, key}); it != sh.map.end()) { + return it->second; + } + } - #ifndef NDEBUG - assert(!DKToIdInBakingMode); - #endif + // 2) Блокируем и повторно ищем запись (может кто уже успел её добавить) + std::unique_lock lk(sh.mutex); + if (auto it = sh.map.find(KeyView{domain, key}); it != sh.map.end()) { + return it->second; + } - if(domainTable == typeTable.end()) - return _getIdNew(type, domain, key); + // Выделяем идентификатор + ResourceId id = _NextId[static_cast(type)].fetch_add(1, std::memory_order_relaxed); - auto keyTable = domainTable->second.find(key); + std::string d(domain); + std::string k(key); - if (keyTable == domainTable->second.end()) - return _getIdNew(type, domain, key); + sh.map.emplace(Key{d, k}, id); + sh.newlyInserted.push_back(id); - return keyTable->second; + _storeReverse(type, id, std::move(d), std::move(k)); - return 0; + return id; } /* Переносит все новые идентификаторы в основную таблицу. - Нельзя использовать пока есть вероятность что кто-то использует getId(). - Out_bakeId <- Возвращает все новые привязки. + В этой реализации "основная таблица" уже основная (forward map обновляется сразу), + а bake() собирает только новые привязки (domain,key) по логам вставок и дополняет IdToDK. + + Нельзя использовать пока есть вероятность что кто-то использует getId(), если ты хочешь + строгий debug-контроль как раньше. В релизе это не требуется: bake читает только reverse, + а forward не трогает. */ - std::array< - std::vector, - MAX_ENUM - > bake() { - #ifndef NDEBUG - + std::array, MAX_ENUM> bake() { +#ifndef NDEBUG assert(!DKToIdInBakingMode); DKToIdInBakingMode = true; struct _tempStruct { IdProvider* handler; ~_tempStruct() { handler->DKToIdInBakingMode = false; } } _lock{this}; +#endif - #endif + std::array, MAX_ENUM> result; - std::array< - std::vector, - MAX_ENUM - > result; + for(size_t t = 0; t < MAX_ENUM; ++t) { + auto type = static_cast(t); - for(size_t type = 0; type < MAX_ENUM; ++type) { - // Домен+Ключ -> Id - { - auto lock = NewDKToId[type].lock(); - auto& dkToId = DKToId[type]; - for(auto& [domain, keys] : *lock) { - // Если домен не существует, просто воткнёт новые ключи - auto [iterDomain, inserted] = dkToId.try_emplace(domain, std::move(keys)); - if(!inserted) { - // Домен уже существует, сливаем новые ключи - iterDomain->second.merge(keys); - } + // 1) собрать новые id из всех шардов + std::vector new_ids; + _drainNew(type, new_ids); + + if(new_ids.empty()) + continue; + + // 2) превратить id -> (domain,key) через reverse и вернуть наружу + // + дописать в IdToDK[type] в порядке id (по желанию) + std::sort(new_ids.begin(), new_ids.end()); + new_ids.erase(std::unique(new_ids.begin(), new_ids.end()), new_ids.end()); + + result[t].reserve(new_ids.size()); + + // reverse читаем под shared lock + std::shared_lock rlk(_ReverseMutex[t]); + for(ResourceId id : new_ids) { + // id=0 не бывает в newlyInserted + const std::size_t idx = static_cast(id - 1); + if(idx >= _Reverse[t].size()) { + // теоретически не должно случаться (мы пишем reverse до push в log) + continue; } - lock->clear(); + const auto& e = _Reverse[t][idx]; + result[t].push_back({e.Domain, e.Key}); } - // Id -> Домен+Ключ - { - auto lock = NewIdToDK[type].lock(); + rlk.unlock(); - auto& idToDK = IdToDK[type]; - result[type] = std::move(*lock); - lock->clear(); - idToDK.append_range(result[type]); - } + // 3) дописать в IdToDK (для новых клиентов) + // Важно: IdToDK[0] уже содержит "core/none" как элемент 0. + IdToDK[t].append_range(result[t]); // C++23 } return result; } - // Для отправки новым подключенным клиентам - const std::array< - std::vector, - static_cast(EnumAssets::MAX_ENUM) - >& idToDK() const { + // id to DK + std::optional getDK(Enum type, ResourceId id) { + auto& vec = _Reverse[static_cast(type)]; + auto& mtx = _ReverseMutex[static_cast(type)]; + + std::unique_lock lk(mtx); + if(id >= vec.size()) + return std::nullopt; + + return vec[id]; + } + + // Для отправки новым подключенным клиентам + const std::array, MAX_ENUM>& idToDK() const { return IdToDK; } -protected: - #ifndef NDEBUG - // Для контроля за режимом слияния ключей +private: + // ---- key types for unordered_dense ---- + struct Key { + std::string domain; + std::string key; + }; + + struct KeyView { + std::string_view domain; + std::string_view key; + }; + + struct KeyHash { + using is_transparent = void; + + static inline std::size_t h(std::string_view sv) noexcept { + // если у тебя есть detail::TSVHash под string_view — можно подставить + return std::hash{}(sv); + } + + static inline std::size_t mix(std::size_t a, std::size_t b) noexcept { + a ^= b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2); + return a; + } + + std::size_t operator()(const Key& k) const noexcept { + return mix(h(k.domain), h(k.key)); + } + + std::size_t operator()(const KeyView& kv) const noexcept { + return mix(h(kv.domain), h(kv.key)); + } + }; + + struct KeyEq { + using is_transparent = void; + + bool operator()(const Key& a, const Key& b) const noexcept { + return a.domain == b.domain && a.key == b.key; + } + + bool operator()(const Key& a, const KeyView& b) const noexcept { + return a.domain == b.domain && a.key == b.key; + } + + bool operator()(const KeyView& a, const Key& b) const noexcept { + return a.domain == b.domain && a.key == b.key; + } + }; + + using Map = ankerl::unordered_dense::map; + + struct Shard { + mutable std::shared_mutex mutex; + Map map; + std::vector newlyInserted; + }; + +private: + // Кластер таблиц идентификаторов + std::array< + std::array, MAX_ENUM + > _Shards; + + // Счётчики идентификаторов + std::array, MAX_ENUM> _NextId; + + // Таблица обратных связок (Id to DK) + std::array, MAX_ENUM> _Reverse; + mutable std::array _ReverseMutex; + +#ifndef NDEBUG bool DKToIdInBakingMode = false; - #endif +#endif - /* - Работает с таблицами для новых идентификаторов, в синхронном режиме. - Используется когда в основных таблицах не нашлось привязки, - она будет найдена или создана здесь синхронно. - */ - inline ResourceId _getIdNew(EnumAssets type, std::string_view domain, std::string_view key) { - // Блокировка по нужному типу ресурса - auto lock = NewDKToId[static_cast(type)].lock(); - - auto iterDomainNewTable = lock->find(domain); - // Если домена не нашлось, сразу вставляем его на подходящее место - if(iterDomainNewTable == lock->end()) { - iterDomainNewTable = lock->emplace_hint( - iterDomainNewTable, - (std::string) domain, - std::unordered_map{} - ); - } - - auto& domainNewTable = iterDomainNewTable->second; - - - if(auto iter = domainNewTable.find(key); iter != domainNewTable.end()) - return iter->second; - else { - uint32_t id = NextId[static_cast(type)]++; - domainNewTable.emplace_hint(iter, (std::string) key, id); - - // Добавился новый идентификатор, теперь добавим обратную связку - auto lock2 = NewIdToDK[static_cast(type)].lock(); - lock.unlock(); - - lock2->emplace_back((std::string) domain, (std::string) key); - return id; - } - } - -// Условно многопоточные объекты - /* - Таблица идентификаторов. Новые идентификаторы выделяются в NewDKToId, - и далее вливаются в основную таблицу при вызове bakeIdTables(). - - Домен+Ключ -> Id - */ - std::array DKToId; - - /* - Таблица обратного резолва. - Id -> Домен+Ключ. - */ + // stable "full sync" table for new clients: std::array, MAX_ENUM> IdToDK; -// Требующие синхронизации - /* - Таблица в которой выделяются новые идентификаторы, перед вливанием в DKToId. - Домен+Ключ -> Id. - */ - std::array, MAX_ENUM> NewDKToId; +private: + Shard& _shardFor(Enum type, std::string_view domain, std::string_view key) { + const std::size_t idx = KeyHash{}(KeyView{domain, key}) % ShardCount; + return _Shards[static_cast(type)][idx]; + } - /* - Списки в которых пишутся новые привязки. - Id + LastMaxId -> Домен+Ключ. - */ - std::array>, MAX_ENUM> NewIdToDK; + const Shard& _shardFor(Enum type, std::string_view domain, std::string_view key) const { + const std::size_t idx = KeyHash{}(KeyView{domain, key}) % ShardCount; + return _Shards[static_cast(type)][idx]; + } - // Для последовательного выделения идентификаторов - std::array NextId; + void _storeReverse(Enum type, ResourceId id, std::string&& domain, std::string&& key) { + auto& vec = _Reverse[static_cast(type)]; + auto& mtx = _ReverseMutex[static_cast(type)]; + const std::size_t idx = static_cast(id); + + std::unique_lock lk(mtx); + if(idx >= vec.size()) + vec.resize(idx + 1); + + vec[idx] = BindDomainKeyInfo{std::move(domain), std::move(key)}; + } + + void _drainNew(Enum type, std::vector& out) { + out.clear(); + auto& shards = _Shards[static_cast(type)]; + + // Можно добавить reserve по эвристике + for (auto& sh : shards) { + std::unique_lock lk(sh.mutex); + if (sh.newlyInserted.empty()) continue; + + const auto old = out.size(); + out.resize(old + sh.newlyInserted.size()); + std::copy(sh.newlyInserted.begin(), sh.newlyInserted.end(), out.begin() + old); + sh.newlyInserted.clear(); + } + } }; -} \ No newline at end of file +} // namespace LV