feat(cdclient): servers open master's fdb copy, never the client's file

Worlds, the UGC server and the dashboard open the pair named in
resServer/cdclient-current (CDServer.sqlite without an fdb when there is
none). Worlds switch to a new pair on CDCLIENT_RELOAD between frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 23:19:55 -05:00
parent b05b84ff30
commit 1f87b2557d
6 changed files with 70 additions and 6 deletions

View File

@@ -174,6 +174,17 @@ std::optional<FdbSnapshot::Current> FdbSnapshot::ReadCurrent(const std::filesyst
return current;
}
FdbSnapshot::Resolved FdbSnapshot::Resolve(const std::filesystem::path& dir) {
Resolved resolved{ dir / DEFAULT_SQLITE, {} };
const auto current = ReadCurrent(dir);
if (!current) return resolved;
std::error_code code;
if (!std::filesystem::is_regular_file(dir / current->sqlite, code) || !std::filesystem::is_regular_file(dir / current->fdb, code)) return resolved;
resolved.sqlite = dir / current->sqlite;
resolved.fdb = dir / current->fdb;
return resolved;
}
bool FdbSnapshot::WriteCurrent(const std::filesystem::path& dir, const Current& current) {
const auto temp = dir / TempName(CURRENT_FILE);
{

View File

@@ -62,6 +62,17 @@ namespace FdbSnapshot {
std::optional<Current> ReadCurrent(const std::filesystem::path& dir);
/**
* The files a server opens at startup: the pair the pointer file names, when both are there, or CDServer.sqlite
* and no fdb (never the client's own file).
*/
struct Resolved {
std::filesystem::path sqlite;
std::filesystem::path fdb; // empty: read CDServer.sqlite only
};
Resolved Resolve(const std::filesystem::path& dir);
// Writes the pointer file under a temporary name and renames it over the old one (it is never held open)
bool WriteCurrent(const std::filesystem::path& dir, const Current& current);

View File

@@ -17,6 +17,7 @@
#include "dServer.h"
#include "AssetManager.h"
#include "BinaryPathFinder.h"
#include "FdbSnapshot.h"
#include "ServiceType.h"
#include "MessageType/Master.h"
#include "MasterPackets.h"
@@ -395,7 +396,7 @@ int main(int argc, char** argv) {
// Connect to CDClient database
try {
const std::string cdclientPath = BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite";
const std::string cdclientPath = FdbSnapshot::Resolve(BinaryPathFinder::GetBinaryDir() / "resServer").sqlite.string();
CDClientDatabase::Connect(cdclientPath);
} catch (std::exception& ex) {
LOG("Failed to connect to CDClient database: %s", ex.what());

View File

@@ -15,6 +15,7 @@
#include "Profiler.h"
#include "AssetManager.h"
#include "BinaryPathFinder.h"
#include "FdbSnapshot.h"
#include "CDClientDatabase.h"
#include "ConfigSync.h"
#include "Database.h"
@@ -654,7 +655,7 @@ namespace {
outcome = UgcJobs::ProcessModel(*data, library, settings);
}
} else {
CDClientDatabase::Connect((BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite").string());
CDClientDatabase::Connect(FdbSnapshot::Resolve(BinaryPathFinder::GetBinaryDir() / "resServer").sqlite.string());
UgcJobs::ModularInput modular;
std::string error;
if (!UgcCdClient::GatherModular(input, modular, error)) {
@@ -715,7 +716,7 @@ int main(int argc, char** argv) {
}
try {
CDClientDatabase::Connect((BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite").string());
CDClientDatabase::Connect(FdbSnapshot::Resolve(BinaryPathFinder::GetBinaryDir() / "resServer").sqlite.string());
} catch (std::exception& ex) {
LOG("Failed to connect to CDClient database: %s", ex.what());
return EXIT_FAILURE;

View File

@@ -36,6 +36,8 @@
#include "PerformanceManager.h"
#include "Diagnostics.h"
#include "BinaryPathFinder.h"
#include "FdbSnapshot.h"
#include "master/CDClientReload.h"
#include "dPlatforms.h"
//RakNet includes:
@@ -173,6 +175,23 @@ void ResendPlayersToChat() {
LOG("Sent %u player(s) to the new chat server", sent);
}
// CDCLIENT_RELOAD: the client's cdclient.fdb changed; switch to master's new copy between frames (packets are handled on
// the main thread). What is already spawned keeps what it loaded; what is made from now on reads the new data
void OnCDClientReload(const CDClientReload& reload) {
if (reload.IsRequest()) return;
const auto resServer = BinaryPathFinder::GetBinaryDir() / "resServer";
const auto start = std::chrono::steady_clock::now();
try {
CDClientDatabase::Reconnect((resServer / reload.sqlite).string());
CDClientManager::Reload(resServer / reload.fdb);
} catch (const std::exception& e) {
LOG("CDClient reload: could not switch to %s: %s", reload.sqlite.c_str(), e.what());
return;
}
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
LOG("CDClient reload: switched to %s and %s in %lld ms", reload.fdb.c_str(), reload.sqlite.c_str(), static_cast<long long>(ms));
}
// CHAT_SERVER_READY (live update): connect to the new chat server now rather than at the next retry
void OnChatServerReady() {
if (g_ChatConnected) {
@@ -265,9 +284,13 @@ int main(int argc, char** argv) {
return EXIT_FAILURE;
}
// The copy of the client's fdb and its CDServer.sqlite that master names; never the client's own file, so it can be
// replaced while this runs (docs/CDClientFdb.md)
const auto cdclientFiles = FdbSnapshot::Resolve(BinaryPathFinder::GetBinaryDir() / "resServer");
// Connect to CDClient
try {
CDClientDatabase::Connect((BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite").string());
CDClientDatabase::Connect(cdclientFiles.sqlite.string());
} catch (const CppSQLite3Exception& e) {
LOG("Unable to connect to CDServer SQLite Database");
LOG("Error: %s", e.errorMessage());
@@ -278,8 +301,8 @@ int main(int argc, char** argv) {
return EXIT_FAILURE;
}
// The client's fdb, read in place and shared between all server processes, when there is one
CDClientManager::LoadValuesFromDatabase(Game::assetManager->GetResPath() / "cdclient.fdb");
// The fdb copy is mapped and shared between all server processes, when there is one
CDClientManager::LoadValuesFromDatabase(cdclientFiles.fdb);
Diagnostics::SetProduceMemoryDump(Game::config->GetValue("generate_dump") == "1");
@@ -949,6 +972,7 @@ namespace {
handlers.On<MigrationStatus>(Master::MIGRATE_STATUS, [](const MigrationStatus& status, const SystemAddress&) { WorldMigration::HandleStatus(status); });
handlers.On<MigratePrepare>(Master::MIGRATE_PREPARE, [](const MigratePrepare& prepare, const SystemAddress&) { WorldMigration::HandlePrepare(prepare); });
handlers.On<LiveUpdateStatus>(Master::LIVE_UPDATE_STATUS, [](const LiveUpdateStatus& status, const SystemAddress&) { WorldMigration::HandleLiveUpdateStatus(status); });
handlers.On<CDClientReload>(Master::CDCLIENT_RELOAD, [](const CDClientReload& reload, const SystemAddress&) { OnCDClientReload(reload); });
handlers.On<ChatServerReady>(Master::CHAT_SERVER_READY, [](const ChatServerReady&, const SystemAddress&) { OnChatServerReady(); });
handlers.On<PlayerActionRequest>(Master::PLAYER_ACTION, OnPlayerAction);
handlers.On<MessageCaptureControl>(Master::MESSAGE_CAPTURE_CONTROL, [](const MessageCaptureControl& control, const SystemAddress&) { MessageInspector::Control(control); });

View File

@@ -168,3 +168,19 @@ TEST_F(FdbSnapshotTest, DescribeChangesNamesChangedTables) {
ASSERT_EQ(lines.size(), 1u);
EXPECT_EQ(lines[0], "Objects: 1 -> 1 rows (values changed)");
}
TEST_F(FdbSnapshotTest, ResolveNeverNamesTheClientsFile) {
auto resolved = FdbSnapshot::Resolve(Server());
EXPECT_EQ(resolved.sqlite, Server() / "CDServer.sqlite");
EXPECT_TRUE(resolved.fdb.empty());
// Named but not there yet: still the defaults
ASSERT_TRUE(FdbSnapshot::WriteCurrent(Server(), { FdbSnapshot::FdbName(7), FdbSnapshot::SqliteName(7) }));
EXPECT_TRUE(FdbSnapshot::Resolve(Server()).fdb.empty());
FdbTestWriter::WriteFile(Server() / FdbSnapshot::FdbName(7), { 1 });
FdbTestWriter::WriteFile(Server() / FdbSnapshot::SqliteName(7), { 1 });
resolved = FdbSnapshot::Resolve(Server());
EXPECT_EQ(resolved.sqlite, Server() / FdbSnapshot::SqliteName(7));
EXPECT_EQ(resolved.fdb, Server() / FdbSnapshot::FdbName(7));
}