Files
secs-gem/include/secsgem/gem/store/substrates.hpp
T
raphael 1548b49afd persistence: SubstrateStore enable_persistence(dir)
Same pattern as carriers: per-substrate binary record (.sub) with
atomic .tmp+rename, replay on enable, delete on remove. Records
current state across all three E90 axes (location / processing /
ID-status), plus substid / carrierid / slot / free-form location
label. History is deliberately NOT journaled — it's an in-memory
ring buffer and rebuilding from replayed state would mislead.

Five new tests cover full-axis replay, every terminal processing
state, remove-deletes-journal, corrupt-record drop, and the
history-is-transient invariant.

Closes #2 in the test-gap backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 10:31:54 +02:00

309 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
#include <variant>
#include <vector>
#include "secsgem/gem/substrate_state.hpp"
namespace secsgem::gem {
// One entry in a substrate's per-axis transition log. Holds the
// post-transition state on one axis (the other stays at NoState),
// the triggering event encoded back into the SubstrateEvent or
// SubstrateProcessingEvent enum (held discriminated by `is_processing`),
// the location string at the moment, and a steady_clock timestamp.
struct SubstrateHistoryEntry {
std::chrono::steady_clock::time_point at;
SubstrateState location_to{SubstrateState::NoState};
SubstrateProcessingState processing_to{SubstrateProcessingState::NoState};
std::variant<SubstrateEvent, SubstrateProcessingEvent> event;
std::string location_label; // free-form location at the time of the event
};
struct Substrate {
std::string substid; // SUBSTID — E90 wafer identifier
std::string carrierid; // CARRIERID where this wafer originated
uint8_t slot = 0; // 1-based slot in the source carrier
std::string location; // free-form: equipment-defined module name
std::vector<SubstrateHistoryEntry> history;
std::unique_ptr<SubstrateStateMachine> fsm;
std::filesystem::path journal_path; // empty when persistence disabled
};
class SubstrateStore {
public:
using LocationChangeHandler =
std::function<void(const std::string& substid, SubstrateState from,
SubstrateState to, SubstrateEvent)>;
using ProcessingChangeHandler =
std::function<void(const std::string& substid,
SubstrateProcessingState from,
SubstrateProcessingState to,
SubstrateProcessingEvent)>;
SubstrateStore() = default;
SubstrateStore(const SubstrateStore&) = delete;
SubstrateStore& operator=(const SubstrateStore&) = delete;
SubstrateStore(SubstrateStore&&) = delete;
SubstrateStore& operator=(SubstrateStore&&) = delete;
void set_location_handler(LocationChangeHandler h) { on_loc_ = std::move(h); }
void set_processing_handler(ProcessingChangeHandler h) { on_proc_ = std::move(h); }
enum class CreateResult { Created, Denied_AlreadyExists };
// Cap per-substrate history retention to keep memory bounded. 0 ==
// unlimited. Default cap is 256 entries which comfortably covers a
// typical wafer's lifecycle.
void set_history_limit(std::size_t n) { history_limit_ = n; }
std::size_t history_limit() const { return history_limit_; }
CreateResult create(std::string substid, std::string carrierid = "",
uint8_t slot = 0) {
if (subs_.count(substid)) return CreateResult::Denied_AlreadyExists;
install_(substid, carrierid, slot, "", std::nullopt, std::nullopt,
std::nullopt);
if (persistent_) write_record_(substid);
return CreateResult::Created;
}
bool has(const std::string& id) const { return subs_.count(id) > 0; }
const Substrate* get(const std::string& id) const {
auto it = subs_.find(id);
return it == subs_.end() ? nullptr : &it->second;
}
Substrate* get(const std::string& id) {
auto it = subs_.find(id);
return it == subs_.end() ? nullptr : &it->second;
}
bool fire_location_event(const std::string& id, SubstrateEvent e,
std::string new_location = "") {
auto* s = get(id);
if (!s) return false;
if (!new_location.empty()) s->location = std::move(new_location);
bool ok = s->fsm->on_location_event(e);
if (persistent_) write_record_(id);
return ok;
}
bool fire_processing_event(const std::string& id, SubstrateProcessingEvent e) {
auto* s = get(id);
if (!(s && s->fsm->on_processing_event(e))) return false;
if (persistent_) write_record_(id);
return true;
}
bool remove(const std::string& id) {
auto it = subs_.find(id);
if (it == subs_.end()) return false;
if (persistent_ && !it->second.journal_path.empty()) {
std::error_code ec;
std::filesystem::remove(it->second.journal_path, ec);
}
subs_.erase(it);
return true;
}
std::size_t size() const { return subs_.size(); }
std::vector<std::string> ids() const {
std::vector<std::string> out;
out.reserve(subs_.size());
for (const auto& kv : subs_) out.push_back(kv.first);
return out;
}
const std::vector<SubstrateHistoryEntry>* history(const std::string& id) const {
const auto* s = get(id);
return s ? &s->history : nullptr;
}
// ---- Persistence ----------------------------------------------------
// Records *current state* per substrate. History is not journaled —
// it's a transient in-memory ring buffer and rebuilding it from
// replayed state would be misleading.
void enable_persistence(std::filesystem::path dir) {
namespace fs = std::filesystem;
journal_dir_ = std::move(dir);
persistent_ = true;
std::error_code ec;
fs::create_directories(journal_dir_, ec);
std::vector<std::pair<uint64_t, fs::path>> existing;
for (auto& entry : fs::directory_iterator(journal_dir_, ec)) {
if (!entry.is_regular_file()) continue;
const auto& p = entry.path();
if (p.extension() != ".sub") continue;
uint64_t seq = 0;
try { seq = std::stoull(p.stem().string()); }
catch (...) { continue; }
existing.emplace_back(seq, p);
}
std::sort(existing.begin(), existing.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
for (auto& [seq, path] : existing) {
auto rec = load_record_(path);
if (!rec) { fs::remove(path, ec); continue; }
install_(rec->substid, rec->carrierid, rec->slot, rec->location,
rec->loc_state, rec->proc_state, rec->id_state);
auto* s = get(rec->substid);
if (s) s->journal_path = path;
if (seq >= next_seq_) next_seq_ = seq + 1;
}
}
bool persistence_enabled() const { return persistent_; }
std::filesystem::path journal_dir() const { return journal_dir_; }
private:
struct Record {
std::string substid;
std::string carrierid;
uint8_t slot;
std::string location;
SubstrateState loc_state;
SubstrateProcessingState proc_state;
SubstrateIDStatus id_state;
};
// Substrate record:
// [u8 magic = 0xC6][u8 version = 1]
// [u8 loc_state][u8 proc_state][u8 id_state][u8 slot]
// [u16 substid_len][substid_len × u8]
// [u16 carrierid_len][carrierid_len × u8]
// [u16 location_len][location_len × u8]
static constexpr uint8_t kMagic = 0xC6;
static constexpr uint8_t kVersion = 0x01;
void install_(const std::string& substid, const std::string& carrierid,
uint8_t slot, const std::string& location,
std::optional<SubstrateState> loc,
std::optional<SubstrateProcessingState> proc,
std::optional<SubstrateIDStatus> id) {
auto fsm = std::make_unique<SubstrateStateMachine>();
if (loc || proc || id) {
fsm->restore_state(loc.value_or(SubstrateState::AtSource),
proc.value_or(SubstrateProcessingState::NeedsProcessing),
id.value_or(SubstrateIDStatus::NotConfirmed));
}
const std::string keyed = substid;
fsm->set_location_handler(
[this, keyed](SubstrateState f, SubstrateState t, SubstrateEvent e) {
record_history(keyed, t, SubstrateProcessingState::NoState, e);
if (on_loc_) on_loc_(keyed, f, t, e);
});
fsm->set_processing_handler(
[this, keyed](SubstrateProcessingState f, SubstrateProcessingState t,
SubstrateProcessingEvent e) {
record_history(keyed, SubstrateState::NoState, t, e);
if (on_proc_) on_proc_(keyed, f, t, e);
});
subs_.emplace(substid,
Substrate{substid, carrierid, slot, location, {},
std::move(fsm), {}});
}
void write_record_(const std::string& substid) {
namespace fs = std::filesystem;
auto* s = get(substid);
if (!s) return;
if (s->journal_path.empty()) {
char namebuf[32];
std::snprintf(namebuf, sizeof(namebuf), "%010llu.sub",
static_cast<unsigned long long>(next_seq_++));
s->journal_path = journal_dir_ / namebuf;
}
fs::path tmp = s->journal_path;
tmp += ".tmp";
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
if (!out) return;
uint8_t header[6] = {
kMagic, kVersion,
static_cast<uint8_t>(s->fsm->location_state()),
static_cast<uint8_t>(s->fsm->processing_state()),
static_cast<uint8_t>(s->fsm->id_state()),
s->slot};
out.write(reinterpret_cast<const char*>(header), sizeof(header));
auto write_str = [&](const std::string& str) {
uint16_t n = static_cast<uint16_t>(str.size());
uint8_t lb[2] = {static_cast<uint8_t>(n >> 8), static_cast<uint8_t>(n)};
out.write(reinterpret_cast<const char*>(lb), 2);
out.write(str.data(), static_cast<std::streamsize>(n));
};
write_str(s->substid);
write_str(s->carrierid);
write_str(s->location);
out.flush();
out.close();
std::error_code ec;
fs::rename(tmp, s->journal_path, ec);
if (ec) fs::remove(tmp, ec);
}
static std::optional<Record> load_record_(const std::filesystem::path& p) {
std::ifstream in(p, std::ios::binary);
if (!in) return std::nullopt;
uint8_t header[6];
in.read(reinterpret_cast<char*>(header), sizeof(header));
if (!in || header[0] != kMagic || header[1] != kVersion) return std::nullopt;
Record r;
r.loc_state = static_cast<SubstrateState>(header[2]);
r.proc_state = static_cast<SubstrateProcessingState>(header[3]);
r.id_state = static_cast<SubstrateIDStatus>(header[4]);
r.slot = header[5];
auto read_str = [&](std::string& out) {
uint8_t lb[2];
in.read(reinterpret_cast<char*>(lb), 2);
if (!in) return false;
uint16_t n = static_cast<uint16_t>((uint16_t(lb[0]) << 8) | lb[1]);
out.resize(n);
in.read(out.data(), n);
return static_cast<bool>(in) || n == 0;
};
if (!read_str(r.substid)) return std::nullopt;
if (!read_str(r.carrierid)) return std::nullopt;
if (!read_str(r.location)) return std::nullopt;
return r;
}
template <typename Event>
void record_history(const std::string& id, SubstrateState loc_to,
SubstrateProcessingState proc_to, Event ev) {
auto* s = get(id);
if (!s) return;
SubstrateHistoryEntry entry;
entry.at = std::chrono::steady_clock::now();
entry.location_to = loc_to;
entry.processing_to = proc_to;
entry.event = ev;
entry.location_label = s->location;
s->history.push_back(std::move(entry));
if (history_limit_ != 0 && s->history.size() > history_limit_) {
// Drop the oldest entry; vector shift is fine for the small caps
// we expect (default 256, typically a few dozen per substrate).
s->history.erase(s->history.begin());
}
}
std::map<std::string, Substrate> subs_;
LocationChangeHandler on_loc_;
ProcessingChangeHandler on_proc_;
std::size_t history_limit_ = 256;
bool persistent_ = false;
std::filesystem::path journal_dir_;
uint64_t next_seq_ = 0;
};
} // namespace secsgem::gem