From 69803448bb4a60f3a45d4466460dea9cb5a3edff Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 09:23:16 -0500 Subject: [PATCH] feat(master): report the UGC server in the server list and wait for it on shutdown The server list now carries whether master starts the UGC server, whether it is connected and the pid it was started as. Settings reloads reach it like auth and chat, and shutdown waits for it. The UGC server sends its totals and storage with its traffic reports. Co-Authored-By: Claude Opus 5.5 --- dMasterServer/MasterServer.cpp | 17 +++++++++++++---- dNet/MasterPackets.cpp | 6 ++++++ dNet/MasterPackets.h | 5 +++++ dUgcServer/UgcProcessor.h | 7 +++++++ dUgcServer/UgcServer.cpp | 10 ++++++++++ .../dGameTests/dNetTests/MasterPacketsTests.cpp | 14 +++++++++++++- 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index df6b14fcb..f407208aa 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -84,6 +84,8 @@ SystemAddress authServerMasterPeerSysAddr; SystemAddress chatServerMasterPeerSysAddr; SystemAddress dashboardServerMasterPeerSysAddr; SystemAddress ugcServerMasterPeerSysAddr; +// The UGC server's process id from its last start (0: not started), for the dashboard +uint32_t ugcServerPid = 0; namespace { // Dashboard player actions waiting for world servers to answer @@ -452,7 +454,7 @@ int main(int argc, char** argv) { // The UGC server makes and serves player models' meshes and icons (docs/UgcServer.md) if (Game::config->GetValue("enable_ugc_server") == "1") { - StartUgcServer(); + ugcServerPid = StartUgcServer(); } auto t = std::chrono::high_resolution_clock::now(); @@ -888,8 +890,8 @@ namespace { LOG("Reloading settings (changed on the dashboard)"); Game::config->ReloadConfig(); Game::im->LoadZoneLimits(); - // Everyone else: auth, chat and every world - for (const auto& peer : { authServerMasterPeerSysAddr, chatServerMasterPeerSysAddr }) { + // Everyone else: auth, chat, UGC and every world + for (const auto& peer : { authServerMasterPeerSysAddr, chatServerMasterPeerSysAddr, ugcServerMasterPeerSysAddr }) { if (peer != UNASSIGNED_SYSTEM_ADDRESS) MasterPackets::SendTo(peer, reload); } for (const auto& instance : Game::im->GetInstances()) { @@ -938,6 +940,9 @@ namespace { ServerListResponse response; response.authOnline = authServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0; response.chatOnline = chatServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0; + response.ugcEnabled = Game::config->GetValue("enable_ugc_server") == "1" ? 1 : 0; + response.ugcOnline = ugcServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0; + response.ugcPid = ugcServerPid; for (const auto& inst : Game::im->GetInstances()) { if (!inst || !inst->GetIsReady() || inst->GetIsShuttingDown()) continue; @@ -1057,7 +1062,7 @@ void HandlePacket(Packet* packet) { MasterPackets::SendTo(dashboardServerMasterPeerSysAddr, offline); } - StartUgcServer(); + ugcServerPid = StartUgcServer(); } } @@ -1132,6 +1137,7 @@ int ShutdownSequence(int32_t signal) { if (allInstancesShutdown && \ authServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && \ chatServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && \ + ugcServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && \ dashboardServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS) { LOG("Finished shutting down MasterServer!"); break; @@ -1154,6 +1160,9 @@ int ShutdownSequence(int32_t signal) { if (dashboardServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) { LOG("Dashboard server did not shutdown in time"); } + if (ugcServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) { + LOG("UGC server did not shutdown in time"); + } for (const auto& instance : Game::im->GetInstances()) { if (instance == nullptr) { continue; diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index d3935ae77..ebe95daba 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -241,6 +241,9 @@ namespace MasterPackets { bitStream.Write(instance.port); bitStream.Write(instance.isPrivate); } + bitStream.Write(ugcEnabled); + bitStream.Write(ugcOnline); + bitStream.Write(ugcPid); } bool ServerListResponse::Deserialize(RakNet::BitStream& bitStream) { @@ -259,6 +262,9 @@ namespace MasterPackets { VALIDATE_READ(bitStream.Read(instance.port)); VALIDATE_READ(bitStream.Read(instance.isPrivate)); } + VALIDATE_READ(bitStream.Read(ugcEnabled)); + VALIDATE_READ(bitStream.Read(ugcOnline)); + VALIDATE_READ(bitStream.Read(ugcPid)); return true; } diff --git a/dNet/MasterPackets.h b/dNet/MasterPackets.h index c11177c86..3b6a65bc2 100644 --- a/dNet/MasterPackets.h +++ b/dNet/MasterPackets.h @@ -262,6 +262,11 @@ namespace MasterPackets { uint8_t authOnline{}; uint8_t chatOnline{}; std::vector instances; // u32 count + // The UGC server (docs/UgcServer.md), after the instances: whether master starts it (enable_ugc_server), + // whether it is connected, and the process id master last started it as (0: unknown) + uint8_t ugcEnabled{}; + uint8_t ugcOnline{}; + uint32_t ugcPid{}; ServerListResponse() : LUBitStream(ServiceType::MASTER, MessageType::Master::SERVER_LIST_RESPONSE) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/dUgcServer/UgcProcessor.h b/dUgcServer/UgcProcessor.h index e3fa4ba15..ca0f34cc8 100644 --- a/dUgcServer/UgcProcessor.h +++ b/dUgcServer/UgcProcessor.h @@ -63,6 +63,13 @@ public: size_t Busy() const { std::lock_guard lock(m_Mutex); return m_Active; } size_t Threads() const { return m_Config.threads; } + // Main thread: totals since the start and the files' size (traffic reports, then the dashboard and /metrics) + uint64_t Made() const { return m_Made; } + uint64_t Failed() const { return m_Failed; } + uint64_t Evicted() const { return m_Evicted; } + uint64_t StoredBytes() const { return m_StoredBytes; } + uint64_t MaxStorageBytes() const { return m_Config.maxStorageBytes; } + private: struct Job { Kind kind{}; diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index b2bfcc8a4..3bafb1225 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -323,6 +323,15 @@ int main(int argc, char** argv) { TrafficStats::Local().SetGauge("workers_busy", [&processor] { return static_cast(processor.Busy()); }); TrafficStats::Local().SetGauge("workers_queued", [&processor] { return static_cast(processor.Queued()); }); TrafficStats::Local().SetGauge("workers_threads", [&processor] { return static_cast(processor.Threads()); }); + // Read on this thread, when a traffic report is taken (dServer::ReceiveFromMaster) + const std::vector>> ugcGauges{ + { "ugc_made_total", [&processor] { return static_cast(processor.Made()); } }, + { "ugc_failed_total", [&processor] { return static_cast(processor.Failed()); } }, + { "ugc_evicted_total", [&processor] { return static_cast(processor.Evicted()); } }, + { "ugc_stored_bytes", [&processor] { return static_cast(processor.StoredBytes()); } }, + { "ugc_max_storage_bytes", [&processor] { return static_cast(processor.MaxStorageBytes()); } }, + }; + for (const auto& [name, gauge] : ugcGauges) TrafficStats::Local().SetGauge(name, gauge); const auto listenIp = Game::config->GetValue("listen_ip").empty() ? std::string("0.0.0.0") : Game::config->GetValue("listen_ip"); const auto port = Setting("port", 2008); @@ -355,6 +364,7 @@ int main(int argc, char** argv) { TrafficStats::Local().SetGauge("workers_busy", nullptr); TrafficStats::Local().SetGauge("workers_queued", nullptr); TrafficStats::Local().SetGauge("workers_threads", nullptr); + for (const auto& [name, gauge] : ugcGauges) TrafficStats::Local().SetGauge(name, nullptr); Game::web.Shutdown(); g_Processor = nullptr; Database::Destroy("UgcServer"); diff --git a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp index 847f0776c..a37479ea0 100644 --- a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp @@ -353,7 +353,16 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) { entry.port = instance.port; entry.isPrivate = instance.isPrivate ? 1 : 0; } - EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteServerList(b, true, count % 2, instances); }), StructPacket(response)); + response.ugcEnabled = 1; + response.ugcOnline = count % 2; + response.ugcPid = 4242 + count; + // The legacy list, then the UGC server's state + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { + LegacyMaster::WriteServerList(b, true, count % 2, instances); + b.Write(1); + b.Write(count % 2); + b.Write(4242 + count); + }), StructPacket(response)); RakNet::BitStream stream; LoadPayload(stream, response); const auto legacy = LegacyMaster::ReadServerList(stream); @@ -365,6 +374,9 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) { EXPECT_EQ(legacy.instances[i].ip, copy.instances[i].ip.string); EXPECT_EQ(legacy.instances[i].isPrivate, copy.instances[i].isPrivate != 0); } + EXPECT_EQ(copy.ugcEnabled, 1); + EXPECT_EQ(copy.ugcOnline, count % 2); + EXPECT_EQ(copy.ugcPid, 4242 + count); ExpectTruncatedFails(response); } }