Синхронный IdProvider
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user