fix(server/game): write saves in the background
This commit is contained in:
1 parent
9f797c099c
commit
8afec91226
8 files changed
+128
-4
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user