diff --git a/dMasterServer/LiveUpdateCoordinator.cpp b/dMasterServer/LiveUpdateCoordinator.cpp index 0d5fddc5e..58241732a 100644 --- a/dMasterServer/LiveUpdateCoordinator.cpp +++ b/dMasterServer/LiveUpdateCoordinator.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #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> g_OldInstances; + bool g_OldMarked = false; + void MarkOldInstances(); std::map> 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) { diff --git a/dMasterServer/LiveUpdateMachine.h b/dMasterServer/LiveUpdateMachine.h index 77b4f439e..1e081ad30 100644 --- a/dMasterServer/LiveUpdateMachine.h +++ b/dMasterServer/LiveUpdateMachine.h @@ -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 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(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: diff --git a/tests/dCommonTests/LiveUpdateTests.cpp b/tests/dCommonTests/LiveUpdateTests.cpp index bca3acb25..05d4759da 100644 --- a/tests/dCommonTests/LiveUpdateTests.cpp +++ b/tests/dCommonTests/LiveUpdateTests.cpp @@ -1,5 +1,6 @@ #include +#include #include #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); }