From 3d37aacc06265492aac1866f0e7eaed69f8d24cd Mon Sep 17 00:00:00 2001 From: James Osborne Date: Thu, 11 Jun 2026 02:06:07 -0400 Subject: [PATCH] Add login lock coordinator heartbeat --- src/AccountSync.cc | 73 ++++++++++++++++++++++++++++++++++++++++++++++ src/AccountSync.hh | 3 ++ src/ServerState.cc | 1 + 3 files changed, 77 insertions(+) diff --git a/src/AccountSync.cc b/src/AccountSync.cc index f617da0b..95fbf250 100644 --- a/src/AccountSync.cc +++ b/src/AccountSync.cc @@ -3,8 +3,10 @@ #include "AsyncUtils.hh" #include +#include #include #include +#include #include #include #include @@ -18,6 +20,7 @@ namespace AccountSync { static std::mutex config_mutex; static std::mutex spool_mutex; static Config current_config; +static std::atomic heartbeat_task_started(false); static uint64_t now_usecs() { using namespace std::chrono; @@ -402,11 +405,81 @@ void configure_from_json(const phosg::JSON& json) { cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); + cfg.login_lock_heartbeat_interval_usecs = json.get_int("LoginLockHeartbeatIntervalUsecs", 60000000); cfg.notify_bb_sessions = json.get_bool("NotifyBBSessions", cfg.enable_login_locks); cfg.spool_directory = json.get_string("SpoolDirectory", "system/account-sync-spool"); configure(cfg); } +static asio::awaitable send_login_lock_heartbeat() { + auto cfg = get_config(); + + if (!cfg.enabled || !cfg.enable_login_locks) { + co_return; + } + if (cfg.coordinator_url.empty()) { + std::fprintf(stderr, + "[AccountSync] warning login_lock_heartbeat_skipped reason=coordinator_url_not_configured source=%s\n", + source_label(cfg).c_str()); + co_return; + } + + std::string body = std::format( + "{{\"source\":\"{}\",\"source_region\":\"{}\",\"source_ship\":\"{}\",\"account_store\":\"{}\"}}", + json_escape(source_label(cfg)), + json_escape(cfg.source_region), + json_escape(cfg.source_ship), + json_escape(cfg.account_store)); + + try { + phosg::JSON response = co_await post_json_with_timeout(cfg, "/account-locks/heartbeat", body); + bool ok = response.get_bool("ok", response.get_bool("OK", false)); + if (!ok) { + std::fprintf(stderr, + "[AccountSync] warning login_lock_heartbeat_rejected source=%s response=%s\n", + source_label(cfg).c_str(), + response.serialize().c_str()); + co_return; + } + + int64_t refreshed = response.get_int("refreshed", 0); + std::fprintf(stderr, + "[AccountSync] login_lock_heartbeat_ok source=%s refreshed=%" PRId64 "\n", + source_label(cfg).c_str(), + refreshed); + + } catch (const std::exception& e) { + std::fprintf(stderr, + "[AccountSync] warning login_lock_heartbeat_failed source=%s error=%s\n", + source_label(cfg).c_str(), + e.what()); + } +} + +static asio::awaitable login_lock_heartbeat_task() { + for (;;) { + auto cfg = get_config(); + uint64_t interval_usecs = cfg.login_lock_heartbeat_interval_usecs; + if (interval_usecs < 5000000) { + interval_usecs = 5000000; + } + + co_await async_sleep(std::chrono::microseconds(interval_usecs)); + co_await send_login_lock_heartbeat(); + } +} + +void start_login_lock_heartbeat_task(asio::io_context& io_context) { + bool expected = false; + if (!heartbeat_task_started.compare_exchange_strong(expected, true)) { + return; + } + + asio::co_spawn(io_context, login_lock_heartbeat_task(), asio::detached); + std::fprintf(stderr, "[AccountSync] login lock heartbeat task started\n"); +} + + asio::awaitable acquire_login_lock( uint32_t account_id, const std::string& version_name, diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 54c538cb..3e85da3b 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -29,6 +29,7 @@ struct Config { bool notify_backup_saves = true; bool notify_bb_sessions = false; bool enable_login_locks = false; // Reserved for future blocking lock behavior + uint64_t login_lock_heartbeat_interval_usecs = 60000000; std::string spool_directory = "system/account-sync-spool"; }; @@ -43,6 +44,8 @@ struct LoginLockAcquireResult { std::string holder_source; }; +void start_login_lock_heartbeat_task(asio::io_context& io_context); + asio::awaitable acquire_login_lock( uint32_t account_id, const std::string& version_name, diff --git a/src/ServerState.cc b/src/ServerState.cc index 78cf9bf5..ee612c80 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -881,6 +881,7 @@ void ServerState::load_config_early() { this->client_idle_timeout_usecs = this->config_json->get_int("ClientIdleTimeout", 60000000); this->patch_client_idle_timeout_usecs = this->config_json->get_int("PatchClientIdleTimeout", 300000000); AccountSync::configure_from_json(this->config_json->get("AccountSync", phosg::JSON::dict())); + AccountSync::start_login_lock_heartbeat_task(*this->io_context); this->psopeeps_dcv2_exp_multiplier = this->config_json->get_int("PsoPeepsDCV2EXPMultiplier", 5); if ((this->psopeeps_dcv2_exp_multiplier != 5) && (this->psopeeps_dcv2_exp_multiplier != 10)) { throw std::runtime_error("PsoPeepsDCV2EXPMultiplier must be 5 or 10");