feat(gem): multi-observer state-change handlers via HandlerSlot
The single-slot set_*_handler pattern was a structural blocker, hit twice: the daemon could not observe control-state changes because register_default_handlers owns the slot, forcing GetControlState to read the FSM cross-thread (a data race), and blocking WatchHealth and the Subscribe stream's ControlStateChange variant. HandlerSlot<Args...> keeps a primary slot with exact legacy semantics (set_ replaces — one existing test depends on replacement) plus an append-only observer list (add_) that survives set_ calls. Fire sites are textually unchanged (operator bool / operator() / assign-from-function). Applied to ControlStateMachine + ProcessJobStore + ControlJobStore (the roadmap-critical three; the remaining single-slot classes follow the same 3-line pattern as needed). EquipmentRuntime gains an atomic control-state mirror registered as an observer — control_state() is now safe from any thread, retiring the GetControlState race — plus add_control_state_observer and add_link_observer (selected/closed fan-out), the hooks WatchHealth and Subscribe need. Tests: observer ordering, set-replaces-primary-but-observers-survive, observers-without-primary, PJ-store coexistence, and the runtime scenario that was previously impossible (mirror + observer + default-handlers set_). Core 464/464 (2816 assertions), daemon 16/16, live GEM300 demo passes with single-fire control-state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -17,6 +17,14 @@ EquipmentRuntime::EquipmentRuntime(const Config& cfg)
|
||||
descriptor_ = config::load_equipment(cfg.equipment_yaml, *model_);
|
||||
auto sm_cfg = config::load_control_state(cfg.control_state_yaml);
|
||||
sm_ = std::make_shared<ControlStateMachine>(sm_cfg.table, sm_cfg.initial);
|
||||
// Keep an atomic mirror of the control state so control_state() is safe
|
||||
// from any thread. Registered as an observer (add_), so it survives the
|
||||
// primary handler register_default_handlers installs later.
|
||||
control_state_cache_.store(sm_->state(), std::memory_order_relaxed);
|
||||
sm_->add_state_change_handler(
|
||||
[this](ControlState, ControlState to, ControlEvent) {
|
||||
control_state_cache_.store(to, std::memory_order_relaxed);
|
||||
});
|
||||
|
||||
auto pj = config::load_process_job_state(cfg.process_job_yaml);
|
||||
auto cj = config::load_control_job_state(cfg.control_job_yaml);
|
||||
@@ -107,10 +115,14 @@ void EquipmentRuntime::clear_alarm(uint32_t alid) {
|
||||
void EquipmentRuntime::wire_connection() {
|
||||
server_->on_connection([this](std::shared_ptr<Connection> conn) {
|
||||
*active_conn_ = conn;
|
||||
conn->set_closed_handler([this](const std::string&) { active_conn_->reset(); });
|
||||
conn->set_closed_handler([this](const std::string&) {
|
||||
active_conn_->reset();
|
||||
for (const auto& o : link_observers_) o(false);
|
||||
});
|
||||
|
||||
conn->set_selected_handler([this]() {
|
||||
log(std::string("host is online; control=") + control_state_name(sm_->state()));
|
||||
for (const auto& o : link_observers_) o(true);
|
||||
if (model_->spool.size() == 0) return;
|
||||
asio::post(io_, [this]() {
|
||||
auto c = active_conn_->lock();
|
||||
|
||||
Reference in New Issue
Block a user