feat(cdclient): make a new CDServer.sqlite off-thread and swap tables in place

CDClientSnapshot converts a copy of the client's fdb into its own
CDServer-<hash>.sqlite with the cdserver migrations applied, on its own
connection. CDClientDatabase::Reconnect opens the new file before letting go
of the old one; CDClientManager::Reload empties every table (old entries kept
alive so references from spawned objects stay valid), retires the old fdb view
and loads again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 22:50:20 -05:00
parent 1e0fc92f2e
commit 27a4564fec
13 changed files with 390 additions and 2 deletions

View File

@@ -67,6 +67,23 @@ void CDClientDatabase::Connect(const std::string& filename) {
sqlite3_trace_v2(conn->handle(), SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE, Trace, nullptr);
}
void CDClientDatabase::Reconnect(const std::string& filename) {
auto* next = new CppSQLite3DB();
try {
next->open(filename.c_str());
} catch (...) {
delete next;
throw;
}
sqlite3_trace_v2(next->handle(), SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE, Trace, nullptr);
g_Running.clear();
auto* old = std::exchange(conn, next);
isConnected = true;
// close_v2 waits for statements still open on the old file (a query a caller holds) before it really closes. The
// wrapper is left behind on purpose: its destructor would close the handle a second time
sqlite3_close_v2(old->handle());
}
//! Queries the CDClient
CppSQLite3Query CDClientDatabase::ExecuteQuery(const std::string& query) {
return conn->execQuery(query.c_str());

View File

@@ -26,6 +26,12 @@ namespace CDClientDatabase {
*/
void Connect(const std::string& filename);
/**
* Switches to another CDServer.sqlite (a CDClient reload, see FdbSnapshot.h). The new file is opened before the
* old connection is let go; throws CppSQLite3Exception and keeps the old one if it can't be opened.
*/
void Reconnect(const std::string& filename);
//! Queries the CDClient
/*!
\param query The query

View File

@@ -1,4 +1,9 @@
#include "CDClientManager.h"
#include <functional>
#include <memory>
#include <vector>
#include "CDFdb.h"
#include "Logger.h"
#include "CDActivityRewardsTable.h"
@@ -66,7 +71,25 @@
#define SPECIALIZE_TABLE_STORAGE(table) \
template<> typename table::StorageType& CDClientManager::GetEntriesMutable<table>() { return table##Entries; };
#define DEFINE_TABLE_STORAGE(table) namespace { table::StorageType table##Entries; }; SPECIALIZE_TABLE_STORAGE(table)
namespace {
// Empties each table's storage for Reload, keeping the old entries alive in g_Retired
std::vector<std::function<void()>>& Resetters() {
static std::vector<std::function<void()>> resetters;
return resetters;
}
std::vector<std::shared_ptr<void>> g_Retired;
template<typename Storage>
bool RegisterReset(Storage& storage) {
Resetters().push_back([&storage]() {
g_Retired.push_back(std::make_shared<Storage>(std::move(storage)));
storage = Storage{};
});
return true;
}
}
#define DEFINE_TABLE_STORAGE(table) namespace { table::StorageType table##Entries; const bool table##Reset = RegisterReset(table##Entries); }; SPECIALIZE_TABLE_STORAGE(table)
DEFINE_TABLE_STORAGE(CDActivityRewardsTable);
DEFINE_TABLE_STORAGE(CDActivitiesTable);
@@ -178,6 +201,21 @@ void CDClientManager::LoadValuesFromDatabase(const std::filesystem::path& fdbPat
CDZoneTableTable::LoadValuesFromDatabase();
}
void CDClientManager::ResetTables() {
CDFdb::Retire();
for (const auto& reset : Resetters()) reset();
CDZoneTableTable::Reset();
}
void CDClientManager::Reload(const std::filesystem::path& fdbPath) {
ResetTables();
LoadValuesFromDatabase(fdbPath);
}
uint32_t CDClientManager::GetTableCount() {
return static_cast<uint32_t>(Resetters().size()) + 1;
}
void CDClientManager::LoadValuesFromDefaults() {
LOG("Loading default CDClient tables!");

View File

@@ -17,6 +17,20 @@ namespace CDClientManager {
void LoadValuesFromDatabase(const std::filesystem::path& fdbPath = {});
void LoadValuesFromDefaults();
/**
* Loads every table again after a CDClient reload (the caller has already reconnected CDClientDatabase to the new
* CDServer.sqlite). Each table's old entries are emptied out but kept alive, and so is the old fdb's view, so a
* reference an entity took from a table before the reload stays valid: what is spawned keeps what it loaded, and
* what is made afterwards reads the new data. Main thread only.
*/
void Reload(const std::filesystem::path& fdbPath);
// The first half of Reload: lets go of the fdb and empties every table, keeping the old entries alive
void ResetTables();
// How many tables ResetTables empties
uint32_t GetTableCount();
/**
* Fetch a table from CDClient
*

View File

@@ -1,8 +1,17 @@
#include "CDZoneTableTable.h"
#include <memory>
#include <vector>
namespace CDZoneTableTable {
Table entries;
void Reset() {
static std::vector<std::unique_ptr<Table>> retired;
retired.push_back(std::make_unique<Table>(std::move(entries)));
entries = {};
}
void LoadValuesFromDatabase() {
// Get the data from the database
auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM ZoneTable");

View File

@@ -37,6 +37,9 @@ namespace CDZoneTableTable {
using Table = std::map<uint32_t, CDZoneTable>;
void LoadValuesFromDatabase();
// Empties the table for a CDClient reload. The old entries are kept alive, so a pointer from Query stays valid
void Reset();
// Queries the table with a zoneID to find.
const CDZoneTable* Query(uint32_t zoneID);
};

View File

@@ -7,6 +7,7 @@
namespace {
std::unique_ptr<FdbReader> g_Fdb;
std::unique_ptr<FdbReader> g_Retired;
// Column name -> index for each table handed out by GetTable, so RowFields looks up names in O(1)
std::unordered_map<const FdbReader::Table*, std::unordered_map<std::string, int32_t>> g_ColumnIndices;
@@ -127,6 +128,11 @@ void CDFdb::Close() {
g_Fdb.reset();
}
void CDFdb::Retire() {
g_ColumnIndices.clear();
if (g_Fdb) g_Retired = std::move(g_Fdb);
}
const FdbReader* CDFdb::Get() {
return g_Fdb.get();
}

View File

@@ -30,6 +30,12 @@ namespace CDFdb {
bool Open(const std::filesystem::path& path, bool allowMapping = true);
void Close();
/**
* Closes the fdb for a CDClient reload, but keeps its view open until the next Retire, so a row or table pointer
* handed out before the reload still reads from memory that is there. Only the latest retired fdb is kept.
*/
void Retire();
// The open fdb, or nullptr
const FdbReader* Get();