Fix pool-account starvation on fresh/renamed characters DBs

Start the next PBV2_NNNN pool index past BOTH the realm-local counter and
any PBV2_* bnet account already present in the shared auth DB, so a fresh
or renamed characters DB no longer restarts numbering at 1 and collides
with the existing fleet (AOR_NAME_ALREADY_EXIST x8 -> pool starvation).
This commit is contained in:
devbox
2026-08-12 15:16:54 +10:00
parent b14ebad3fb
commit bbe7c54e3c
2 changed files with 50 additions and 2 deletions
@@ -6,6 +6,7 @@
#include "Log.h" #include "Log.h"
#include <algorithm> #include <algorithm>
#include <cstdlib>
#include <fmt/format.h> #include <fmt/format.h>
#include <random> #include <random>
#include <string> #include <string>
@@ -223,6 +224,37 @@ uint32 BotAccountMgr::next_pseudo_idx_locked() const
return f[0].GetUInt32(); return f[0].GetUInt32();
} }
uint32 BotAccountMgr::max_pool_idx_in_auth() const
{
// Pool accounts are keyed by a per-realm pseudo-id but live in the
// SHARED auth DB (bnet email [email protected], stored
// uppercased by CreateBattlenetAccount). When a realm boots against a
// fresh/renamed characters DB, next_pseudo_idx_locked restarts at 1
// while the auth DB still holds every PBV2_* account ever created —
// so account creation must jump past the max index that already
// exists in auth or every attempt collides and the pool starves.
auto r = LoginDatabase.PQuery(
"SELECT email FROM battlenet_accounts WHERE email LIKE 'PBV2\\_%'");
if (!r)
return 0;
uint32 maxIdx = 0;
do
{
const std::string email = r->Fetch()[0].GetString();
const size_t at = email.find('@');
if (at == std::string::npos)
continue;
const size_t us = email.rfind('_', at);
if (us == std::string::npos || us >= at)
continue;
const unsigned long val = std::strtoul(email.substr(us + 1, at - us - 1).c_str(), nullptr, 10);
maxIdx = std::max(maxIdx, static_cast<uint32>(val));
} while (r->NextRow());
return maxIdx;
}
uint32 BotAccountMgr::create_pool_account_locked() uint32 BotAccountMgr::create_pool_account_locked()
{ {
// Each V2 pool account is created as a full Battle.net account with // Each V2 pool account is created as a full Battle.net account with
@@ -236,9 +268,19 @@ uint32 BotAccountMgr::create_pool_account_locked()
// entry in `account` with bnetAccountId/bnetIndex set correctly. // entry in `account` with bnetAccountId/bnetIndex set correctly.
// Up to a few retries in case the chosen username collides with an // Up to a few retries in case the chosen username collides with an
// existing account (unlikely with the PBV2_ prefix but defensive). // existing account (unlikely with the PBV2_ prefix but defensive).
for (int attempt = 0; attempt < 8; ++attempt) // Index starts past BOTH the local counter and any PBV2_* account that
// already exists in the shared auth DB (a fresh/renamed characters DB
// restarts the local counter at 1 while auth retains the old fleet).
const uint32 localNext = next_pseudo_idx_locked();
const uint32 authMaxIdx = max_pool_idx_in_auth();
uint32 idx = std::max(localNext, authMaxIdx + 1);
if (idx != localNext)
TC_LOG_INFO("playerbot.v2",
"[BotAccountMgr] Pool index starts at {} (local next {}), skipping {} stale auth account(s).",
idx, localNext, idx - localNext);
for (int attempt = 0; attempt < 8; ++attempt, ++idx)
{ {
const uint32 idx = next_pseudo_idx_locked() + uint32(attempt);
const std::string name = PoolAccountName(idx); const std::string name = PoolAccountName(idx);
const std::string password = GeneratePassword(); const std::string password = GeneratePassword();
// The bnet "email" must look like a deliverable address; we use // The bnet "email" must look like a deliverable address; we use
@@ -70,6 +70,12 @@ private:
// Generates the next pseudo_account_idx (max + 1 from cache). // Generates the next pseudo_account_idx (max + 1 from cache).
uint32 next_pseudo_idx_locked() const; uint32 next_pseudo_idx_locked() const;
// Maximum pool index (PBV2_NNNN) that already exists in the SHARED
// auth DB, or 0. The realm-local counter restarts when a realm uses a
// fresh/renamed characters DB, so creation must start above this to
// avoid colliding with accounts previous realms already provisioned.
uint32 max_pool_idx_in_auth() const;
// Synchronously creates a new pool account via sAccountMgr, inserts // Synchronously creates a new pool account via sAccountMgr, inserts
// the row in playerbot_v2_account, and returns its account id. 0 on // the row in playerbot_v2_account, and returns its account id. 0 on
// failure (e.g., name already exists in the legacy `account` table). // failure (e.g., name already exists in the legacy `account` table).