Синхронный 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)
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})

View File

@@ -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;

View File

@@ -2,206 +2,278 @@
#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 {
template<class Enum = EnumAssets>
template<class Enum = EnumAssets, size_t ShardCount = 64>
class IdProvider {
public:
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 {
std::string Domain, Key;
};
public:
IdProvider() {
std::fill(NextId.begin(), NextId.end(), 1);
for(size_t type = 0; type < static_cast<size_t>(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<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) {
#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<size_t>(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<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())
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<BindDomainKeyInfo>,
MAX_ENUM
> bake() {
#ifndef NDEBUG
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> bake() {
#ifndef NDEBUG
assert(!DKToIdInBakingMode);
DKToIdInBakingMode = true;
struct _tempStruct {
IdProvider* handler;
~_tempStruct() { handler->DKToIdInBakingMode = false; }
} _lock{this};
#endif
#endif
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> result;
std::array<
std::vector<BindDomainKeyInfo>,
MAX_ENUM
> result;
for(size_t t = 0; t < MAX_ENUM; ++t) {
auto type = static_cast<Enum>(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<ResourceId> 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<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 -> Домен+Ключ
{
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<BindDomainKeyInfo>,
static_cast<size_t>(EnumAssets::MAX_ENUM)
>& idToDK() const {
// 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<std::vector<BindDomainKeyInfo>, 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<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;
#endif
#endif
/*
Работает с таблицами для новых идентификаторов, в синхронном режиме.
Используется когда в основных таблицах не нашлось привязки,
она будет найдена или создана здесь синхронно.
*/
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 -> Домен+Ключ.
*/
// stable "full sync" table for new clients:
std::array<std::vector<BindDomainKeyInfo>, MAX_ENUM> IdToDK;
// Требующие синхронизации
/*
Таблица в которой выделяются новые идентификаторы, перед вливанием в DKToId.
Домен+Ключ -> Id.
*/
std::array<TOS::SpinlockObject<IdTable>, 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<size_t>(type)][idx];
}
/*
Списки в которых пишутся новые привязки.
Id + LastMaxId -> Домен+Ключ.
*/
std::array<TOS::SpinlockObject<std::vector<BindDomainKeyInfo>>, 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<size_t>(type)][idx];
}
// Для последовательного выделения идентификаторов
std::array<ResourceId, MAX_ENUM> NextId;
void _storeReverse(Enum type, ResourceId id, std::string&& domain, std::string&& key) {
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