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:
2026-06-08 23:51:46 +02:00
parent cfa2d1e531
commit d69f26b415
3 changed files with 250 additions and 5 deletions
+6
View File
@@ -49,10 +49,16 @@ int main(int argc, char** argv) {
const auto port = static_cast<uint16_t>(std::stoi(arg(argc, argv, "--port", "5000")));
const auto equipment_yaml = arg(argc, argv, "--config", "/app/data/equipment.yaml");
const auto state_yaml = arg(argc, argv, "--state-table", "/app/data/control_state.yaml");
const auto spool_dir = arg(argc, argv, "--spool-dir", "");
auto logfn = [](const std::string& m) { std::cout << "[equip] " << m << std::endl; };
auto model = std::make_shared<gem::EquipmentDataModel>();
if (!spool_dir.empty()) {
model->spool.enable_persistence(spool_dir);
logfn("spool: persisting to " + spool_dir +
" (replayed " + std::to_string(model->spool.size()) + " messages)");
}
config::EquipmentDescriptor desc;
config::ControlStateConfig sm_cfg;
try {