Синхронный IdProvider

This commit is contained in:
2026-01-11 22:28:03 +06:00
parent a29e772f35
commit 16a0fa5f7a
3 changed files with 229 additions and 145 deletions

View File

@@ -84,6 +84,16 @@ FetchContent_Declare(
FetchContent_MakeAvailable(Boost) 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) 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 # glm
# find_package(glm REQUIRED) # find_package(glm REQUIRED)
# target_include_directories(${PROJECT_NAME} PUBLIC ${GLM_INCLUDE_DIR}) # target_include_directories(${PROJECT_NAME} PUBLIC ${GLM_INCLUDE_DIR})

View File

@@ -536,12 +536,14 @@ private:
continue; continue;
} }
const auto& dkTable = IdToDK[typeIndex];
std::string domain = "core"; std::string domain = "core";
std::string key; std::string key;
if(id < dkTable.size()) { {
domain = dkTable[id].Domain; auto d = getDK((EnumAssets) typeIndex, id);
key = dkTable[id].Key; if(d) {
domain = d->Domain;
key = d->Key;
}
} }
std::u8string data = dataIter->second; std::u8string data = dataIter->second;

View File

@@ -2,206 +2,278 @@
#include "Common/Abstract.hpp" #include "Common/Abstract.hpp"
#include <ankerl/unordered_dense.h>
#include <array>
#include <atomic>
#include <cassert>
#include <optional>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <algorithm>
namespace LV { namespace LV {
template<class Enum = EnumAssets> template<class Enum = EnumAssets, size_t ShardCount = 64>
class IdProvider { class IdProvider {
public: public:
static constexpr size_t MAX_ENUM = static_cast<size_t>(Enum::MAX_ENUM); static constexpr size_t MAX_ENUM = static_cast<size_t>(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 { struct BindDomainKeyInfo {
std::string Domain, Key; std::string Domain, Key;
}; };
public: public:
IdProvider() { explicit IdProvider() {
std::fill(NextId.begin(), NextId.end(), 1); for(size_t type = 0; type < MAX_ENUM; ++type) {
for(size_t type = 0; type < static_cast<size_t>(Enum::MAX_ENUM); ++type) { _NextId[type].store(1, std::memory_order_relaxed);
DKToId[type]["core"]["none"] = 0; _Reverse[type].reserve(1024);
IdToDK[type].emplace_back("core", "none");
IdToDK[type].push_back({"core", "none"});
auto& sh = _shardFor(static_cast<Enum>(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) { inline ResourceId getId(Enum type, std::string_view domain, std::string_view key) {
#ifndef NDEBUG #ifndef NDEBUG
assert(!DKToIdInBakingMode); assert(!DKToIdInBakingMode);
#endif #endif
auto& sh = _shardFor(type, domain, key);
const auto& typeTable = DKToId[static_cast<size_t>(type)]; // 1) Поиск в режиме для чтения
auto domainTable = typeTable.find(domain); {
std::shared_lock lk(sh.mutex);
if(auto it = sh.map.find(KeyView{domain, key}); it != sh.map.end()) {
return it->second;
}
}
#ifndef NDEBUG // 2) Блокируем и повторно ищем запись (может кто уже успел её добавить)
assert(!DKToIdInBakingMode); std::unique_lock lk(sh.mutex);
#endif 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<size_t>(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()) sh.map.emplace(Key{d, k}, id);
return _getIdNew(type, domain, key); 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::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> bake() {
std::vector<BindDomainKeyInfo>, #ifndef NDEBUG
MAX_ENUM
> bake() {
#ifndef NDEBUG
assert(!DKToIdInBakingMode); assert(!DKToIdInBakingMode);
DKToIdInBakingMode = true; DKToIdInBakingMode = true;
struct _tempStruct { struct _tempStruct {
IdProvider* handler; IdProvider* handler;
~_tempStruct() { handler->DKToIdInBakingMode = false; } ~_tempStruct() { handler->DKToIdInBakingMode = false; }
} _lock{this}; } _lock{this};
#endif
#endif std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> result;
std::array< for(size_t t = 0; t < MAX_ENUM; ++t) {
std::vector<BindDomainKeyInfo>, auto type = static_cast<Enum>(t);
MAX_ENUM
> result;
for(size_t type = 0; type < MAX_ENUM; ++type) { // 1) собрать новые id из всех шардов
// Домен+Ключ -> Id std::vector<ResourceId> new_ids;
{ _drainNew(type, new_ids);
auto lock = NewDKToId[type].lock();
auto& dkToId = DKToId[type]; if(new_ids.empty())
for(auto& [domain, keys] : *lock) { continue;
// Если домен не существует, просто воткнёт новые ключи
auto [iterDomain, inserted] = dkToId.try_emplace(domain, std::move(keys)); // 2) превратить id -> (domain,key) через reverse и вернуть наружу
if(!inserted) { // + дописать в IdToDK[type] в порядке id (по желанию)
// Домен уже существует, сливаем новые ключи std::sort(new_ids.begin(), new_ids.end());
iterDomain->second.merge(keys); 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<std::size_t>(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 -> Домен+Ключ rlk.unlock();
{
auto lock = NewIdToDK[type].lock();
auto& idToDK = IdToDK[type]; // 3) дописать в IdToDK (для новых клиентов)
result[type] = std::move(*lock); // Важно: IdToDK[0] уже содержит "core/none" как элемент 0.
lock->clear(); IdToDK[t].append_range(result[t]); // C++23
idToDK.append_range(result[type]);
}
} }
return result; return result;
} }
// id to DK
std::optional<BindDomainKeyInfo> getDK(Enum type, ResourceId id) {
auto& vec = _Reverse[static_cast<size_t>(type)];
auto& mtx = _ReverseMutex[static_cast<size_t>(type)];
std::unique_lock lk(mtx);
if(id >= vec.size())
return std::nullopt;
return vec[id];
}
// Для отправки новым подключенным клиентам // Для отправки новым подключенным клиентам
const std::array< const std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM>& idToDK() const {
std::vector<BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& idToDK() const {
return IdToDK; return IdToDK;
} }
protected: private:
#ifndef NDEBUG // ---- 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<std::string_view>{}(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<Key, ResourceId, KeyHash, KeyEq>;
struct Shard {
mutable std::shared_mutex mutex;
Map map;
std::vector<ResourceId> newlyInserted;
};
private:
// Кластер таблиц идентификаторов
std::array<
std::array<Shard, ShardCount>, MAX_ENUM
> _Shards;
// Счётчики идентификаторов
std::array<std::atomic<ResourceId>, MAX_ENUM> _NextId;
// Таблица обратных связок (Id to DK)
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> _Reverse;
mutable std::array<std::shared_mutex, MAX_ENUM> _ReverseMutex;
#ifndef NDEBUG
bool DKToIdInBakingMode = false; bool DKToIdInBakingMode = false;
#endif #endif
/* // stable "full sync" table for new clients:
Работает с таблицами для новых идентификаторов, в синхронном режиме.
Используется когда в основных таблицах не нашлось привязки,
она будет найдена или создана здесь синхронно.
*/
inline ResourceId _getIdNew(EnumAssets type, std::string_view domain, std::string_view key) {
// Блокировка по нужному типу ресурса
auto lock = NewDKToId[static_cast<size_t>(type)].lock();
auto iterDomainNewTable = lock->find(domain);
// Если домена не нашлось, сразу вставляем его на подходящее место
if(iterDomainNewTable == lock->end()) {
iterDomainNewTable = lock->emplace_hint(
iterDomainNewTable,
(std::string) domain,
std::unordered_map<std::string, uint32_t, detail::TSVHash, detail::TSVEq>{}
);
}
auto& domainNewTable = iterDomainNewTable->second;
if(auto iter = domainNewTable.find(key); iter != domainNewTable.end())
return iter->second;
else {
uint32_t id = NextId[static_cast<size_t>(type)]++;
domainNewTable.emplace_hint(iter, (std::string) key, id);
// Добавился новый идентификатор, теперь добавим обратную связку
auto lock2 = NewIdToDK[static_cast<size_t>(type)].lock();
lock.unlock();
lock2->emplace_back((std::string) domain, (std::string) key);
return id;
}
}
// Условно многопоточные объекты
/*
Таблица идентификаторов. Новые идентификаторы выделяются в NewDKToId,
и далее вливаются в основную таблицу при вызове bakeIdTables().
Домен+Ключ -> Id
*/
std::array<IdTable, MAX_ENUM> DKToId;
/*
Таблица обратного резолва.
Id -> Домен+Ключ.
*/
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> IdToDK; std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> IdToDK;
// Требующие синхронизации private:
/* Shard& _shardFor(Enum type, std::string_view domain, std::string_view key) {
Таблица в которой выделяются новые идентификаторы, перед вливанием в DKToId. const std::size_t idx = KeyHash{}(KeyView{domain, key}) % ShardCount;
Домен+Ключ -> Id. return _Shards[static_cast<size_t>(type)][idx];
*/ }
std::array<TOS::SpinlockObject<IdTable>, MAX_ENUM> NewDKToId;
/* const Shard& _shardFor(Enum type, std::string_view domain, std::string_view key) const {
Списки в которых пишутся новые привязки. const std::size_t idx = KeyHash{}(KeyView{domain, key}) % ShardCount;
Id + LastMaxId -> Домен+Ключ. return _Shards[static_cast<size_t>(type)][idx];
*/ }
std::array<TOS::SpinlockObject<std::vector<BindDomainKeyInfo>>, MAX_ENUM> NewIdToDK;
// Для последовательного выделения идентификаторов void _storeReverse(Enum type, ResourceId id, std::string&& domain, std::string&& key) {
std::array<ResourceId, MAX_ENUM> NextId; auto& vec = _Reverse[static_cast<size_t>(type)];
auto& mtx = _ReverseMutex[static_cast<size_t>(type)];
const std::size_t idx = static_cast<std::size_t>(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<ResourceId>& out) {
out.clear();
auto& shards = _Shards[static_cast<size_t>(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();
}
}
}; };
} } // namespace LV