Drain clientless sessions during normal shutdown with a 60-second grace and recovery audit. Recover invalid bot login states while preserving real transports and taxis. Handle bounded database reconnect failures with controlled service termination and clear OutdoorPvP references before shutdown destruction. Prevent obsolete transport actions from overriding newer movement. Add regression coverage, live validation notes and the updated parallel -j10 build workflow in AGENTS.md. Full build, twelve regression tests and live login/shutdown/database-fault checks passed.
70 lines
5.7 KiB
Python
70 lines
5.7 KiB
Python
"""Exercise production shutdown/drain methods using a deterministic monotonic clock."""
|
|
from pathlib import Path
|
|
import subprocess,tempfile,json
|
|
root=Path(__file__).resolve().parents[2]
|
|
s=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
|
|
def function(marker):
|
|
a=s.index(marker);i=s.index('{',a)+1;n=1
|
|
while n:n+=(s[i]=='{')-(s[i]=='}');i+=1
|
|
return s[a:i]
|
|
methods='\n'.join(function(m) for m in ['bool ClientlessPlayerMgr::ReadyForSafeLogout(', 'void ClientlessPlayerMgr::RequestSafeLogout(', 'void ClientlessPlayerMgr::RequestSafeLogoutAll(', 'void ClientlessPlayerMgr::ResumeSafeLogouts(', 'void ClientlessPlayerMgr::UpdateSafeLogouts(', 'void ClientlessPlayerMgr::BeginShutdown(', 'void ClientlessPlayerMgr::CancelShutdown(', 'bool ClientlessPlayerMgr::ShutdownReady(', 'void ClientlessPlayerMgr::WriteShutdownSnapshot('])
|
|
methods=methods.replace('std::chrono::steady_clock::now()', 'TestClock::now()')
|
|
code=r'''
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
using uint32=unsigned;
|
|
#define TC_LOG_ERROR(...) ((void)0)
|
|
#define TC_LOG_INFO(...) ((void)0)
|
|
constexpr int UNIT_STATE_CASTING=1,TEAM_ALLIANCE=0;
|
|
struct TestClock{static long long ms;static auto now(){return std::chrono::steady_clock::time_point(std::chrono::milliseconds(ms));}};long long TestClock::ms=0;
|
|
namespace GameTime{long GetGameTime(){return 123;}unsigned GetGameTimeMS(){return unsigned(TestClock::ms);}}
|
|
struct Guid{unsigned GetRawValue()const{return 99;}};
|
|
struct Player{
|
|
bool alive=true,combat=false,taxi=false,fly=false,fall=false,teleport=false,spline=false,casting=false;void* transport=nullptr;
|
|
struct{struct{Guid guid;}transport;}m_movementInfo;
|
|
bool IsInWorld()const{return true;}bool IsAlive()const{return alive;}bool IsInCombat()const{return combat;}
|
|
void*GetTransport()const{return transport;}bool IsInFlight()const{return taxi;}bool IsFlying()const{return fly;}bool IsFalling()const{return fall;}
|
|
bool IsBeingTeleported()const{return teleport;}bool HasUnitState(int)const{return casting;}bool IsSplineEnabled()const{return spline;}
|
|
std::string GetName(){return "Testbot";}unsigned getRace(){return 22;}unsigned GetTeamId(){return 0;}unsigned GetMapId(){return 0;}
|
|
float GetPositionX(){return 1;}float GetPositionY(){return 2;}float GetPositionZ(){return 3;}
|
|
} player;
|
|
struct Session{Player*GetPlayer(){return &player;}}session;
|
|
struct World{std::unordered_map<unsigned,Session*>sessions;unsigned removed=0;auto const&GetAllClientlessSessions(){return sessions;}unsigned GetClientlessSessionCount(){return sessions.size();}Session*FindClientlessSession(unsigned id){auto i=sessions.find(id);return i==sessions.end()?nullptr:i->second;}bool RemoveClientlessSession(unsigned id){++removed;sessions.erase(id);return true;}}world;auto sWorld=&world;
|
|
std::string directory;
|
|
struct Config{std::string GetStringDefault(char const*,char const*){return directory;}}config;auto sConfigMgr=&config;
|
|
struct MySQLConnection{static bool HasFatalConnectionError(){return false;}};
|
|
struct Controller{unsigned resumes=0;void OnSafeLogoutCancelled(Player*){++resumes;}}controller;
|
|
struct ClientlessPlayerMgr{
|
|
std::unordered_map<unsigned,bool>_safeLogouts;bool _safeLogoutAll=false,_shutdownDraining=false,_shutdownWasSafeLogoutAll=false,_shutdownSnapshotDone=false;
|
|
std::chrono::steady_clock::time_point _shutdownStarted;std::string _shutdownAuditFile;
|
|
struct Record{struct Controller*Controller=nullptr;};std::unordered_map<unsigned,Record>_records;
|
|
static bool ReadyForSafeLogout(Player const*);void RequestSafeLogout(unsigned);void RequestSafeLogoutAll();void ResumeSafeLogouts();void UpdateSafeLogouts();void BeginShutdown();void CancelShutdown();bool ShutdownReady();void WriteShutdownSnapshot(char const*);
|
|
};
|
|
'''+methods+r'''
|
|
int main(int argc,char**argv){directory=argv[1];ClientlessPlayerMgr m;world.sessions[26]=&session;m._records[26].Controller=&controller;
|
|
player.fly=true;m.BeginShutdown();assert(m._shutdownDraining&&m._safeLogoutAll);
|
|
m._safeLogouts[26]=true;m.UpdateSafeLogouts();assert(world.removed==0);
|
|
TestClock::ms=59999;assert(!m.ShutdownReady());TestClock::ms=60000;assert(m.ShutdownReady()&&world.removed==0);
|
|
assert(m._shutdownSnapshotDone);m.CancelShutdown();assert(!m._shutdownDraining&&!m._safeLogoutAll&&controller.resumes==1);
|
|
// Ready bots leave immediately after the controller acknowledged its pause.
|
|
player.fly=false;m.RequestSafeLogoutAll();m._safeLogouts[26]=true;m.UpdateSafeLogouts();assert(world.removed==1);
|
|
m.ResumeSafeLogouts();TestClock::ms=70000;m.BeginShutdown();assert(m.ShutdownReady());m.CancelShutdown();
|
|
// A pre-existing manual drain survives cancellation of a server shutdown.
|
|
world.sessions[26]=&session;m.RequestSafeLogoutAll();m.BeginShutdown();m.CancelShutdown();assert(m._safeLogoutAll);
|
|
for(int flag=0;flag<7;++flag){player=Player{};switch(flag){case 0:player.combat=true;break;case 1:player.taxi=true;break;case 2:player.transport=&session;break;case 3:player.teleport=true;break;case 4:player.spline=true;break;case 5:player.fall=true;break;case 6:player.alive=false;break;}assert(!m.ReadyForSafeLogout(&player));}
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='vcore-shutdown-')as folder:
|
|
p=Path(folder);(p/'test.cpp').write_text(code)
|
|
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
|
subprocess.run([str(p/'test'),str(p/'audit')],check=True)
|
|
events=[json.loads(l) for f in (p/'audit').glob('*.jsonl') for l in f.read_text().splitlines()]
|
|
assert any(e['event']=='deadline' and e['bots'][0]['flying']==1 and e['bots'][0]['faction']=='alliance' for e in events)
|
|
assert any(e['event']=='complete' and e['remaining']==0 for e in events)
|
|
print('Production drain: 60s deadline, safe logout, no forced passenger detach, cancellation/resume and durable JSON audit passed.')
|