mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(live-update): never move properties; mark old instances outdated
Property instances are left out of the replace plan. Once the database is up to date every instance running at the start is marked outdated, and a zone that already got a new instance is not started again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "CDActivitiesTable.h"
|
||||
@@ -31,6 +32,10 @@ namespace {
|
||||
uint32_t g_UpdateId = 0;
|
||||
uint32_t g_NextMigrationId = 0;
|
||||
Clock::time_point g_NextTick{};
|
||||
// The instances running when the update started: marked outdated once the database is up to date
|
||||
std::set<std::pair<uint32_t, uint32_t>> g_OldInstances;
|
||||
bool g_OldMarked = false;
|
||||
void MarkOldInstances();
|
||||
std::map<size_t, std::pair<eUnitState, std::string>> g_Logged; // unit index -> what was logged last
|
||||
ePhase g_LoggedPhase = ePhase::IDLE;
|
||||
|
||||
@@ -237,7 +242,13 @@ bool LiveUpdateCoordinator::Start(const std::string& by, LWOOBJID requesterId, i
|
||||
}
|
||||
if (!CheckBinaries(error)) return false;
|
||||
const auto settings = ReadSettings(warnSeconds);
|
||||
if (!g_Machine.Start(settings, Observe(), Clock::now(), error)) return false;
|
||||
const auto observed = Observe();
|
||||
if (!g_Machine.Start(settings, observed, Clock::now(), error)) return false;
|
||||
g_OldInstances.clear();
|
||||
for (const auto& world : observed.worlds) {
|
||||
if (!world.view.shuttingDown) g_OldInstances.insert({ world.view.zoneId, world.view.instanceId });
|
||||
}
|
||||
g_OldMarked = false;
|
||||
g_UpdateId++;
|
||||
g_Logged.clear();
|
||||
g_LoggedPhase = ePhase::IDLE;
|
||||
@@ -295,6 +306,22 @@ void LiveUpdateCoordinator::Update() {
|
||||
const auto phaseBefore = g_Machine.Phase();
|
||||
const auto finishedBefore = FinishedUnits();
|
||||
if (g_Machine.Tick(Observe(), now, g_Actions)) Publish(phaseBefore != g_Machine.Phase() || finishedBefore != FinishedUnits());
|
||||
MarkOldInstances();
|
||||
}
|
||||
|
||||
namespace {
|
||||
void MarkOldInstances() {
|
||||
if (g_OldMarked || g_Machine.Units().empty()) return;
|
||||
const auto database = g_Machine.Units().front().state;
|
||||
if (database != eUnitState::STOPPED && database != eUnitState::SKIPPED) return;
|
||||
g_OldMarked = true;
|
||||
// Nobody new goes to an instance on the old binaries: requests start new ones. Properties aren't moved; they stop
|
||||
// once everyone left (OutdatedInstances.h)
|
||||
const auto marked = Game::im->MarkOutdated([](const Instance& instance) {
|
||||
return g_OldInstances.contains({ instance.GetMapID(), instance.GetInstanceID() });
|
||||
});
|
||||
LOG("Live update %u: %u instance(s) on the old build take nobody new; properties among them stop once empty", g_UpdateId, marked);
|
||||
}
|
||||
}
|
||||
|
||||
void LiveUpdateCoordinator::Abort(const std::string& why) {
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace LiveUpdate {
|
||||
enum class eWorldPlan : uint8_t {
|
||||
STOP, // nobody there: shut it down (a new one starts when someone goes there)
|
||||
REPLACE_EMPTY, // nobody there, but the zone should always have one: start the new one, then stop this one
|
||||
MOVE, // replace it and move its players (properties save and freeze first)
|
||||
MOVE, // replace it and move its players (properties are never planned: InReplacePlan)
|
||||
WAIT_THEN_MOVE, // activity zones and character selection: nobody new goes there; its players leave by
|
||||
// themselves or, after a while, are moved
|
||||
};
|
||||
@@ -146,6 +146,16 @@ namespace LiveUpdate {
|
||||
return eWorldPlan::MOVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which running instances a live update replaces: all but those already shutting down and properties. A property
|
||||
* (clone) is never moved: building in progress there isn't saved. It is marked outdated like every other instance
|
||||
* (master: nobody new goes there) and stops by itself once everyone left (OutdatedInstances.h).
|
||||
*/
|
||||
inline bool InReplacePlan(const InstanceMigration::InstanceView& view) {
|
||||
return !view.shuttingDown && view.cloneId == 0;
|
||||
}
|
||||
|
||||
|
||||
struct Unit {
|
||||
eUnitKind kind{};
|
||||
eUnitState state{ eUnitState::PENDING };
|
||||
@@ -189,7 +199,7 @@ namespace LiveUpdate {
|
||||
// Character selection first (logins go to the new one soonest), then the busiest
|
||||
std::vector<WorldView> worlds;
|
||||
for (const auto& world : observed.worlds) {
|
||||
if (!world.view.shuttingDown) worlds.push_back(world);
|
||||
if (InReplacePlan(world.view)) worlds.push_back(world);
|
||||
}
|
||||
std::stable_sort(worlds.begin(), worlds.end(), [](const WorldView& a, const WorldView& b) {
|
||||
if ((a.view.zoneId == 0) != (b.view.zoneId == 0)) return a.view.zoneId == 0;
|
||||
@@ -487,10 +497,24 @@ namespace LiveUpdate {
|
||||
SetState(unit, eUnitState::STOPPING, message);
|
||||
}
|
||||
|
||||
// Whether zone has a public instance on the new build already: one started after the update began (not in the
|
||||
// plan), running and taking players
|
||||
bool HasNewInstance(const Observed& observed, uint32_t zone) const {
|
||||
return std::any_of(observed.worlds.begin(), observed.worlds.end(), [&](const WorldView& world) {
|
||||
const auto& view = world.view;
|
||||
if (view.zoneId != zone || view.cloneId != 0 || view.isPrivate || view.outdated || view.shuttingDown || view.draining) return false;
|
||||
return std::none_of(m_Units.begin(), m_Units.end(), [&](const Unit& unit) {
|
||||
return unit.kind == eUnitKind::WORLD && unit.zone == view.zoneId && unit.instance == view.instanceId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void StartWorld(Unit& unit, const Observed& observed, Clock::time_point now, IActions& actions) {
|
||||
const auto* world = observed.Find(unit.zone, unit.instance);
|
||||
if (!world || world->view.shuttingDown) return SetState(unit, eUnitState::STOPPED, "It stopped by itself");
|
||||
unit.players = static_cast<uint32_t>(std::max(world->view.players, 0));
|
||||
// A zone somebody went to since the update began has its new instance already
|
||||
if (HasNewInstance(observed, unit.zone)) m_ReplacedZones.insert(unit.zone);
|
||||
unit.plan = PlanWorld(world->view, world->activityZone, m_Settings.keepZones.contains(unit.zone), m_ReplacedZones.contains(unit.zone));
|
||||
switch (unit.plan) {
|
||||
case eWorldPlan::STOP:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
|
||||
#include "LiveUpdateMachine.h"
|
||||
@@ -203,6 +204,16 @@ TEST(LiveUpdateTest, RoutingSkipsDrainingAndFullInstances) {
|
||||
world.shuttingDown = false;
|
||||
world.isPrivate = true;
|
||||
EXPECT_FALSE(AcceptsNewPlayers(world, 1100, 0, true));
|
||||
world.isPrivate = false;
|
||||
// On the old binary or old files: nobody new, friends and properties included
|
||||
world.outdated = true;
|
||||
EXPECT_FALSE(AcceptsNewPlayers(world, 1100, 0, false));
|
||||
EXPECT_FALSE(AcceptsNewPlayers(world, 1100, 0, true));
|
||||
auto property = World(1150, 3, 1, 42);
|
||||
EXPECT_TRUE(AcceptsNewPlayers(property, 1150, 42, false));
|
||||
property.outdated = true;
|
||||
EXPECT_FALSE(AcceptsNewPlayers(property, 1150, 42, false));
|
||||
EXPECT_FALSE(AcceptsNewPlayers(property, 1150, 42, true));
|
||||
// A replacement still starting takes players: they wait for it
|
||||
auto starting = World(1100, 2, 0);
|
||||
starting.ready = false;
|
||||
@@ -325,20 +336,46 @@ TEST(LiveUpdateTest, OldInstanceThatNeverStopsFails) {
|
||||
EXPECT_EQ(h.WorldUnit(1100, 1).state, eUnitState::FAILED);
|
||||
}
|
||||
|
||||
TEST(LiveUpdateTest, PropertiesAreSavedFirst) {
|
||||
TEST(LiveUpdateTest, PropertiesAreNeverInThePlan) {
|
||||
EXPECT_FALSE(InReplacePlan(World(1150, 1, 2, 42)));
|
||||
EXPECT_FALSE(InReplacePlan(World(1150, 2, 0, 43)));
|
||||
EXPECT_TRUE(InReplacePlan(World(1100, 3, 2)));
|
||||
auto stopping = World(1100, 4, 0);
|
||||
stopping.shuttingDown = true;
|
||||
EXPECT_FALSE(InReplacePlan(stopping));
|
||||
|
||||
Harness h;
|
||||
h.master.Add(World(1150, 1, 2, 42));
|
||||
h.master.Add(World(1150, 1, 2, 42)); // busy property
|
||||
h.master.Add(World(1150, 2, 0, 43)); // empty property
|
||||
h.master.Add(World(1100, 3, 2));
|
||||
h.Start();
|
||||
size_t worlds = 0;
|
||||
for (const auto& unit : h.machine.Units()) {
|
||||
if (unit.kind != eUnitKind::WORLD) continue;
|
||||
worlds++;
|
||||
EXPECT_EQ(unit.clone, 0u);
|
||||
}
|
||||
EXPECT_EQ(worlds, 1u);
|
||||
h.ServicesComeBack();
|
||||
ASSERT_EQ(h.master.moves.size(), 1u);
|
||||
EXPECT_TRUE(h.master.moves[0].prepare);
|
||||
EXPECT_EQ(h.master.moves[0].prepareWaitSeconds, h.settings.propertyBuildWaitSeconds);
|
||||
const auto& unit = h.WorldUnit(1150, 1);
|
||||
EXPECT_EQ(unit.state, eUnitState::PREPARING);
|
||||
h.machine.OnMigration(Status(1, InstanceMigration::eState::PREPARED));
|
||||
EXPECT_EQ(unit.state, eUnitState::STARTING);
|
||||
h.machine.OnMigration(Status(1, InstanceMigration::eState::FAILED));
|
||||
EXPECT_EQ(unit.state, eUnitState::FAILED);
|
||||
EXPECT_EQ(h.master.moves[0].zone, 1100u);
|
||||
EXPECT_FALSE(h.master.moves[0].prepare);
|
||||
EXPECT_TRUE(h.master.log.end() == std::find_if(h.master.log.begin(), h.master.log.end(), [](const std::string& line) {
|
||||
return line.find("1150") != std::string::npos;
|
||||
})) << "nothing is done to a property";
|
||||
}
|
||||
|
||||
TEST(LiveUpdateTest, ZoneWithANewInstanceAlreadyIsNotStartedAgain) {
|
||||
Harness h;
|
||||
h.master.Add(World(1000, 1, 0));
|
||||
h.Start();
|
||||
// Someone went to the zone after the update began: master started a new instance for them
|
||||
h.master.Add(World(1000, 50, 1));
|
||||
h.ServicesComeBack();
|
||||
h.Tick();
|
||||
EXPECT_EQ(h.WorldUnit(1000, 1).plan, eWorldPlan::STOP);
|
||||
EXPECT_TRUE(h.master.Logged("stop world 1000/1"));
|
||||
EXPECT_TRUE(std::none_of(h.master.log.begin(), h.master.log.end(), [](const std::string& line) { return line.rfind("start world 1000/", 0) == 0; }));
|
||||
}
|
||||
|
||||
TEST(LiveUpdateTest, EmptyInstancesStopOrAreReplaced) {
|
||||
@@ -602,7 +639,7 @@ TEST(LiveUpdateTest, ShuttingDownInstancesAreLeftOut) {
|
||||
|
||||
TEST(LiveUpdateTest, StatusCarriesEveryUnit) {
|
||||
Harness h;
|
||||
h.master.Add(World(1150, 4, 2, 42));
|
||||
h.master.Add(World(1150, 4, 2));
|
||||
h.Start();
|
||||
LiveUpdateStatus status;
|
||||
h.machine.FillStatus(status);
|
||||
@@ -624,7 +661,7 @@ TEST(LiveUpdateTest, StatusCarriesEveryUnit) {
|
||||
EXPECT_EQ(read.units[4].kind, eUnitKind::WORLD);
|
||||
EXPECT_EQ(read.units[4].zoneId, 1150u);
|
||||
EXPECT_EQ(read.units[4].instanceId, 4u);
|
||||
EXPECT_EQ(read.units[4].cloneId, 42u);
|
||||
EXPECT_EQ(read.units[4].cloneId, 0u);
|
||||
EXPECT_EQ(read.units[4].players, 2u);
|
||||
EXPECT_EQ(read.units[5].kind, eUnitKind::DASHBOARD);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user