fix(server/game): write saves in the background

This commit is contained in:
capyb2222 committed 2026-09-12 22:00:29 -07:00
1 parent 9f797c099c
commit 8afec91226
8 files changed
+128 -4

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
#include "core/files.h"
#include <condition_variable>
#include <map>
#include <mutex>
#include <thread>
#include "core/logger.h"
#include "core/util.h"
namespace files {
namespace {
// A function-local static, so pending writes land and the thread is joined on exit even
// when nobody calls flush().
class Writer {
public:
~Writer() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
wake_.notify_one();
if (thread_.joinable()) thread_.join();
}
void queue(std::string path, std::string data) {
{
std::lock_guard lock(mutex_);
pending_[std::move(path)] = std::move(data);
if (!thread_.joinable()) thread_ = std::thread([this] { run(); });
}
wake_.notify_one();
}
void flush() {
std::unique_lock lock(mutex_);
idle_.wait(lock, [this] { return pending_.empty() && !writing_; });
}
private:
void run() {
std::unique_lock lock(mutex_);
while (true) {
wake_.wait(lock, [this] { return stopping_ || !pending_.empty(); });
if (pending_.empty()) return;
auto next = pending_.extract(pending_.begin());
writing_ = true;
lock.unlock();
if (!util::writeFile(next.key(), next.mapped())) {
logging::warn("files", "could not write {}", next.key());
}
lock.lock();
writing_ = false;
idle_.notify_all();
}
}
std::mutex mutex_;
std::condition_variable wake_;
std::condition_variable idle_;
std::map<std::string, std::string> pending_;
std::thread thread_;
bool writing_ = false;
bool stopping_ = false;
};
Writer& writer() {
static Writer instance;
return instance;
}
} // namespace
void writeLater(std::string path, std::string data) { writer().queue(std::move(path), std::move(data)); }
void flush() { writer().flush(); }
} // namespace files
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace files {
// Queues `data` for a background thread that writes it to `path` through util::writeFile.
// A newer write to the same path replaces one still waiting, so a burst costs one write.
void writeLater(std::string path, std::string data);
// Blocks until every queued write has landed. Anything that reads a file back first calls
// this, so it never sees an older copy than the one last queued.
void flush();
} // namespace files
+3 -1
View File
@@ -1,6 +1,7 @@
#include "game/player.h"
#include "core/config.h"
#include "core/files.h"
#include "core/logger.h"
#include "core/util.h"
#include "data/excel.h"
@@ -125,7 +126,8 @@ void Player::save() {
void Player::saveNow() {
lastSaveMs_ = util::nowMs();
savePlayerState(*this);
// Built here, where the state is consistent; written off the packet thread.
files::writeLater(core::Config::get().paths.playerFile, playerStateJson(*this));
}
} // namespace game
+9 -2
View File
@@ -3,6 +3,7 @@
#include <nlohmann/json.hpp>
#include "core/config.h"
#include "core/files.h"
#include "core/logger.h"
#include "core/util.h"
#include "game/player.h"
@@ -40,6 +41,8 @@ std::vector<uint32_t> u32Array(const json& j, const char* key) {
} // namespace
bool loadPlayerState(Player& player) {
// A session that just ended may still have its save queued.
files::flush();
const std::string& path = core::Config::get().paths.playerFile;
bool ok = false;
std::string text = util::readFile(path, &ok);
@@ -188,7 +191,7 @@ bool loadPlayerState(Player& player) {
return true;
}
bool savePlayerState(const Player& player) {
std::string playerStateJson(const Player& player) {
const LineupBook& book = player.lineups();
json squads = json::array();
for (uint32_t i = 0; i < kSquadCount; ++i) {
@@ -269,8 +272,12 @@ bool savePlayerState(const Player& player) {
{"character", pity(gacha.character)},
{"lightcone", pity(gacha.lightcone)}};
return j.dump(2);
}
bool savePlayerState(const Player& player) {
const std::string& path = core::Config::get().paths.playerFile;
if (!util::writeFile(path, j.dump(2))) {
if (!util::writeFile(path, playerStateJson(player))) {
logging::warn("player", "could not write {}", path);
return false;
}
+4
View File
@@ -1,5 +1,7 @@
#pragma once
#include <string>
namespace game {
class Player;
@@ -8,5 +10,7 @@ class Player;
// Everything else the client shows comes from the srtools build or the game tables.
bool loadPlayerState(Player& player);
bool savePlayerState(const Player& player);
// What savePlayerState writes, for a caller that queues the write instead.
std::string playerStateJson(const Player& player);
} // namespace game
+6 -1
View File
@@ -3,6 +3,7 @@
#include <nlohmann/json.hpp>
#include "core/config.h"
#include "core/files.h"
#include "core/logger.h"
#include "core/util.h"
@@ -275,7 +276,9 @@ void SrTools::mutate(const std::function<void(SrToolsData&)>& edit, bool persist
bool SrTools::writeToDisk(const SrToolsData& data) const {
try {
return util::writeFile(core::Config::get().paths.srtoolsFile, toJson(data).dump(2));
// Queued: edits come in on the packet thread, and the file is only read at startup.
files::writeLater(core::Config::get().paths.srtoolsFile, toJson(data).dump(2));
return true;
} catch (const std::exception& e) {
logging::warn("srtools", "could not serialise the build: {}", e.what());
return false;
@@ -311,6 +314,8 @@ std::string SrTools::upload(const std::string& body) {
try {
json j = json::parse(body, nullptr, true, true);
if (auto wrapper = j.find("data"); wrapper != j.end() && wrapper->is_object()) j = *wrapper;
// An edit still queued must not land on top of the build replacing it.
files::flush();
if (!util::writeFile(path, j.dump(2))) return "could not write " + path;
} catch (const std::exception& e) {
return std::string("could not store the build: ") + e.what();
+2
View File
@@ -6,6 +6,7 @@
#include "core/config.h"
#include "core/crash.h"
#include "core/files.h"
#include "core/logger.h"
#include "core/util.h"
#include "data/excel.h"
@@ -92,6 +93,7 @@ int main(int argc, char** argv) {
logging::info("capysr", "shutting down");
gateway.stop();
web.stop();
files::flush();
logging::shutdown();
return 0;
}
+10
View File
@@ -4,6 +4,7 @@
#include <string_view>
#include <vector>
#include "core/files.h"
#include "core/logger.h"
#include "core/util.h"
#include "net/cmd_ids.h"
@@ -318,6 +319,15 @@ void testSafeWrite() {
bool ok = false;
check(util::readFile(path, &ok) == "second, and longer" && ok, "with the new content whole");
check(!util::fileExists(path + ".tmp"), "leaving no temp file behind");
// Queued writes: the newest one for a path wins, and flush waits for it to land.
const std::string queued = "build/test-queued-write.json";
files::writeLater(queued, "older");
files::writeLater(queued, "newest");
files::flush();
check(util::readFile(queued, &ok) == "newest" && ok, "a queued write lands, newest first");
files::flush();
check(true, "flushing an empty queue returns");
}
} // namespace