CC1: persistent file-backed spool
Adds opt-in disk persistence to SpoolStore. `enable_persistence(dir)` turns every enqueue into a single `<seq>.spool` file alongside the in-memory queue; drain and clear delete the matching files; restart replays the directory sorted by seq. Writes are atomic: serialize the message via the SECS-II codec, write to `.tmp`, then `std::filesystem::rename` to the final name. Malformed records are dropped silently so a single bad file can't poison the whole spool. `secs_server --spool-dir <path>` enables persistence at startup. Without the flag the behaviour is identical to before (in-memory only). Two new tests: enqueue → restart → replay → drain restores the wire order, and clear deletes the journal files. Test suite: 291 cases / 1515 assertions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "secsgem/gem/data_model.hpp"
|
||||
|
||||
@@ -300,3 +304,76 @@ TEST_CASE("spool force flag controls whether enqueue is taken") {
|
||||
s.set_force_spool(true);
|
||||
CHECK(s.force_spool());
|
||||
}
|
||||
|
||||
TEST_CASE("spool persistence: write, restart, replay") {
|
||||
namespace fs = std::filesystem;
|
||||
// Unique temp dir per test run.
|
||||
auto dir = fs::temp_directory_path() /
|
||||
("spool_test_" + std::to_string(::getpid()) + "_" +
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
fs::remove_all(dir);
|
||||
|
||||
{
|
||||
SpoolStore s;
|
||||
s.set_spoolable_streams({5, 6});
|
||||
s.enable_persistence(dir);
|
||||
CHECK(s.empty());
|
||||
s.enqueue(s2::Message(6, 11, false, s2::Item::u4(uint32_t{42})));
|
||||
s.enqueue(s2::Message(5, 1, false, s2::Item::ascii("ALARM")));
|
||||
CHECK(s.size() == 2);
|
||||
// Two journal files written.
|
||||
std::size_t count = 0;
|
||||
for (auto& e : fs::directory_iterator(dir)) {
|
||||
if (e.path().extension() == ".spool") ++count;
|
||||
}
|
||||
CHECK(count == 2);
|
||||
}
|
||||
|
||||
// Simulate restart: fresh store rehydrates from the same dir.
|
||||
{
|
||||
SpoolStore s;
|
||||
s.set_spoolable_streams({5, 6});
|
||||
s.enable_persistence(dir);
|
||||
REQUIRE(s.size() == 2);
|
||||
auto drained = s.drain();
|
||||
REQUIRE(drained.size() == 2);
|
||||
CHECK(drained[0].stream == 6);
|
||||
CHECK(drained[1].stream == 5);
|
||||
// FIFO: first-enqueued comes out first.
|
||||
REQUIRE(drained[0].body.has_value());
|
||||
CHECK(std::get<std::vector<uint32_t>>(drained[0].body->storage()).front() == 42);
|
||||
CHECK(drained[1].body->as_ascii() == "ALARM");
|
||||
// Files removed after drain.
|
||||
std::size_t count = 0;
|
||||
for (auto& e : fs::directory_iterator(dir)) {
|
||||
if (e.path().extension() == ".spool") ++count;
|
||||
}
|
||||
CHECK(count == 0);
|
||||
}
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
TEST_CASE("spool persistence: clear deletes files") {
|
||||
namespace fs = std::filesystem;
|
||||
auto dir = fs::temp_directory_path() /
|
||||
("spool_clear_" + std::to_string(::getpid()) + "_" +
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count() + 1));
|
||||
fs::remove_all(dir);
|
||||
|
||||
SpoolStore s;
|
||||
s.set_spoolable_streams({6});
|
||||
s.enable_persistence(dir);
|
||||
s.enqueue(s2::Message(6, 11, false));
|
||||
s.enqueue(s2::Message(6, 11, false));
|
||||
CHECK(s.size() == 2);
|
||||
s.clear();
|
||||
CHECK(s.empty());
|
||||
std::size_t count = 0;
|
||||
for (auto& e : fs::directory_iterator(dir)) {
|
||||
if (e.path().extension() == ".spool") ++count;
|
||||
}
|
||||
CHECK(count == 0);
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user