Fix alt-create zombie login (never 'login already in flight' again)

HandleAltCreate submitted the login via SessionMgr().LoginBot immediately
after BotCharacterFactory::Create. SaveToDB is async, so the character
row is not committed yet: BeginLogin's holder loads nothing, the BotSession
never completes and never leaves sessions_ — the bot appears 'stuck in
login' and every later .playerbot login fails with 'login already in
flight'. The .playerbot squad path already avoided this; alt create now
matches it by deferring to DrainAltFinalizes, which submits the login on
the next world tick against the committed row + CharacterCache entry.

Also hardened DrainAltFinalizes: an in-flight session older than 25s is a
wedged zombie — reap it (LogoutBot) and resubmit once so a leftover zombie
from a pre-fix build (or any future race) self-heals instead of wedging
that bot forever.
This commit is contained in:
devbox
2026-08-12 17:02:36 +10:00
parent 2e9c165e97
commit fb0723b653
3 changed files with 50 additions and 17 deletions
+32 -1
View File
@@ -3441,6 +3441,13 @@ void Module::DrainAltFinalizes(uint32 /*now_ms*/)
constexpr uint32 kMaxWaitMs = 120000; // 2 minutes
constexpr uint8 kMaxAttempts = 240; // ~250ms tick → ~60s+; hard cap
// A login that is still in flight this long after the queue entry was
// created is a zombie: BeginLogin was submitted against an uncommitted
// character row (or a wedged holder), so the BotSession never completes
// and never leaves sessions_ — every later `.playerbot login` then fails
// with "login already in flight". Reap it and resubmit on the now
// committed row. Normal logins land in well under this window.
constexpr uint32 kZombieReapMs = 25000;
std::vector<PendingAltFinalize> still;
still.reserve(pending_alt_finalize_.size());
@@ -3488,7 +3495,31 @@ void Module::DrainAltFinalizes(uint32 /*now_ms*/)
if (inFlight)
{
// Login already submitted — do NOT call LoginBot again
// Login already submitted — do NOT call LoginBot again. But a
// login can wedge permanently (see kZombieReapMs): if BeginLogin
// ran against an uncommitted character row the holder loads
// nothing, the session never completes, and the bot stays
// "login already in flight" forever. Reap it once the sanity
// window elapses and resubmit against the committed row.
if (getMSTimeDiff(e.queued_ms, now) >= kZombieReapMs)
{
::Playerbot::Services::SessionMgr().LogoutBot(e.botGuid);
TC_LOG_ERROR("playerbot.v2",
"[AltFinalize] reaped zombie login bot={} elapsed_ms={}",
e.botGuid.ToString(), getMSTimeDiff(e.queued_ms, now));
auto lr = ::Playerbot::Services::SessionMgr().LoginBot(e.botGuid);
TC_LOG_ERROR("playerbot.v2",
"[AltFinalize] LoginBot retry bot={} ok={} reason={}",
e.botGuid.ToString(), lr.ok, lr.reason);
if (lr.ok)
{
requeue("login_submitted");
continue;
}
}
requeue("login_in_flight");
continue;
}
@@ -7846,15 +7846,16 @@ index 0000000000..8119d9c01a
+ "[alt create] OK name={} guid={} race={} class={}",
+ picked.name, id, uint32(picked.race), uint32(picked.cls));
+
+ auto login = SessionMgr().LoginBot(r.guid);
+ if (!login.ok)
+ handler->PSendSysMessage("Altbot %s created; login deferred (%s).",
+ picked.name.c_str(), login.reason.c_str());
+ else
+ handler->PSendSysMessage("Altbot %s created (login submitted).",
+ picked.name.c_str());
+
+ // Do NOT submit the login here. The character row is not DB-committed yet
+ // (SaveToDB is async), so BeginLogin loads nothing and the BotSession
+ // would sit in sessions_ forever as an "in flight" zombie that never
+ // enters world — every later `.playerbot login` then reports
+ // "login already in flight". DrainAltFinalizes submits the login on the
+ // next world tick, by which time the row is committed and the
+ // CharacterCache entry exists. (Mirrors the .playerbot squad path.)
+ Module::instance().QueueAltFinalize(r.guid, me->GetGUID(), me->GetLevel());
+ handler->PSendSysMessage("Altbot %s created; will log in momentarily.",
+ picked.name.c_str());
+ return true; // success — no help text
+ }
+
@@ -7600,15 +7600,16 @@ index 0000000000..8119d9c01a
+ "[alt create] OK name={} guid={} race={} class={}",
+ picked.name, id, uint32(picked.race), uint32(picked.cls));
+
+ auto login = SessionMgr().LoginBot(r.guid);
+ if (!login.ok)
+ handler->PSendSysMessage("Altbot %s created; login deferred (%s).",
+ picked.name.c_str(), login.reason.c_str());
+ else
+ handler->PSendSysMessage("Altbot %s created (login submitted).",
+ picked.name.c_str());
+
+ // Do NOT submit the login here. The character row is not DB-committed yet
+ // (SaveToDB is async), so BeginLogin loads nothing and the BotSession
+ // would sit in sessions_ forever as an "in flight" zombie that never
+ // enters world — every later `.playerbot login` then reports
+ // "login already in flight". DrainAltFinalizes submits the login on the
+ // next world tick, by which time the row is committed and the
+ // CharacterCache entry exists. (Mirrors the .playerbot squad path.)
+ Module::instance().QueueAltFinalize(r.guid, me->GetGUID(), me->GetLevel());
+ handler->PSendSysMessage("Altbot %s created; will log in momentarily.",
+ picked.name.c_str());
+ return true; // success — no help text
+ }
+