Add safe bank failure diagnostics for T5 and T6

Address #14 with request correlation, bounded parser reasons, startup version reporting, and rate-limited storage and settlement logs. Preserve accounting and API identity contracts, extend portable tests, and update release versions and rollout guidance.
This commit is contained in:
2026-10-13 10:02:36 -04:00
parent ab989395f7
commit 463b4bcc51
19 changed files with 804 additions and 78 deletions
@@ -3,6 +3,8 @@
#include "gsc.hpp"
#include "game/scripting/array.hpp"
#include <utils/bank.hpp>
#include <array>
#include <chrono>
namespace bank
{
@@ -13,11 +15,33 @@ namespace bank
return {"tws-bank/t6", server, endpoint};
}
void storage_error()
// Only fixed operation names are printed, never exception text or paths
// from private configuration. Per-operation state has bounded memory.
void storage_result(size_t operation, bool failed)
{
// Do not echo parse exceptions: their text can contain player data.
printf("^1[tws_bank] Outbox operation failed; check tws-bank/t6 files, disk space and permissions. Existing reports retained.\n");
struct status
{
bool failed = false;
std::chrono::steady_clock::time_point logged{};
unsigned suppressed = 0;
};
static std::array<status, 3> states;
constexpr const char* names[] = {"load", "write", "remove"};
auto& state = states[operation];
const auto now = std::chrono::steady_clock::now();
if (failed && state.failed && now - state.logged < std::chrono::seconds(30))
{
++state.suppressed;
return;
}
if (failed || state.failed)
printf("[tws_bank] native_storage operation=%s outcome=%s suppressed=%u\n",
names[operation], failed ? "storage_failure" : "recovered", state.suppressed);
state.failed = failed;
state.logged = now;
state.suppressed = 0;
}
}
class component final : public component_interface
@@ -25,6 +49,7 @@ namespace bank
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add("bankversion", []() { return std::string("1.2.0"); });
gsc::function::add("bankload", [](const std::string& server, const std::string& endpoint) -> scripting::script_value
{
try
@@ -38,25 +63,27 @@ namespace bank
entry["body"] = value.body;
entries.emplace_back(entry);
}
storage_result(0, false);
return entries;
}
catch (...) { storage_error(); return {}; }
catch (...) { storage_result(0, true); return {}; }
});
gsc::function::add("bankstore", [](const std::string& server, const std::string& endpoint, const std::string& body)
{
try { spool(server, endpoint).store(body); return true; }
catch (...) { storage_error(); return false; }
try { spool(server, endpoint).store(body); storage_result(1, false); return true; }
catch (...) { storage_result(1, true); return false; }
});
gsc::function::add("bankremove", [](const std::string& server, const std::string& endpoint, const std::string& id)
{
try { spool(server, endpoint).erase(id); return true; }
catch (...) { storage_error(); return false; }
try { spool(server, endpoint).erase(id); storage_result(2, false); return true; }
catch (...) { storage_result(2, true); return false; }
});
gsc::function::add("bankresponse", [](const std::string& response)
{
const auto ack = utils::bank::parse_acknowledgement(response);
scripting::array result;
result["valid"] = ack.valid;
result["reason"] = ack.reason;
result["state"] = ack.state;
result["code"] = ack.code;
result["request_id"] = ack.id;