diff --git a/dCommon/FdbSnapshot.cpp b/dCommon/FdbSnapshot.cpp index 990b280a7..bffb7a88e 100644 --- a/dCommon/FdbSnapshot.cpp +++ b/dCommon/FdbSnapshot.cpp @@ -174,6 +174,17 @@ std::optional 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); { diff --git a/dCommon/FdbSnapshot.h b/dCommon/FdbSnapshot.h index 17789c712..79c9ff3d9 100644 --- a/dCommon/FdbSnapshot.h +++ b/dCommon/FdbSnapshot.h @@ -62,6 +62,17 @@ namespace FdbSnapshot { std::optional 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); diff --git a/dDashboardServer/DashboardServer.cpp b/dDashboardServer/DashboardServer.cpp index 45c6648d0..60b409502 100644 --- a/dDashboardServer/DashboardServer.cpp +++ b/dDashboardServer/DashboardServer.cpp @@ -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()); diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 3529a11a3..8eb89a6b0 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -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; diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 49f719e65..92d5af051 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -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::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(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(Master::MIGRATE_STATUS, [](const MigrationStatus& status, const SystemAddress&) { WorldMigration::HandleStatus(status); }); handlers.On(Master::MIGRATE_PREPARE, [](const MigratePrepare& prepare, const SystemAddress&) { WorldMigration::HandlePrepare(prepare); }); handlers.On(Master::LIVE_UPDATE_STATUS, [](const LiveUpdateStatus& status, const SystemAddress&) { WorldMigration::HandleLiveUpdateStatus(status); }); + handlers.On(Master::CDCLIENT_RELOAD, [](const CDClientReload& reload, const SystemAddress&) { OnCDClientReload(reload); }); handlers.On(Master::CHAT_SERVER_READY, [](const ChatServerReady&, const SystemAddress&) { OnChatServerReady(); }); handlers.On(Master::PLAYER_ACTION, OnPlayerAction); handlers.On(Master::MESSAGE_CAPTURE_CONTROL, [](const MessageCaptureControl& control, const SystemAddress&) { MessageInspector::Control(control); }); diff --git a/tests/dCommonTests/FdbSnapshotTests.cpp b/tests/dCommonTests/FdbSnapshotTests.cpp index 0301d8638..214f39016 100644 --- a/tests/dCommonTests/FdbSnapshotTests.cpp +++ b/tests/dCommonTests/FdbSnapshotTests.cpp @@ -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)); +}