#pragma once #include #include #include #include #include #include #include #include "secsgem/gem/process_job_state.hpp" namespace secsgem::gem { // One Process Job record. The FSM is heap-allocated through unique_ptr so // the per-PJ state-change handler can capture a stable pointer to // `this`-style state without invalidation on map rehash. struct ProcessJob { std::string prjobid; std::string ppid; // recipe identifier std::vector mtrloutspec; // material identifiers bool alert_enabled = true; // S16F9 alerts on/off std::unique_ptr fsm; }; class ProcessJobStore { public: using TransitionTableFactory = std::function; using StateChangeHandler = std::function; ProcessJobStore() : factory_([] { return ProcessJobTransitionTable::default_table(); }) {} // The per-PJ FSM closes over `this`, so the store must keep a stable // address. unique_ptr makes copying impossible anyway; moves would // silently dangle the per-PJ lambdas — disallow them explicitly. ProcessJobStore(const ProcessJobStore&) = delete; ProcessJobStore& operator=(const ProcessJobStore&) = delete; ProcessJobStore(ProcessJobStore&&) = delete; ProcessJobStore& operator=(ProcessJobStore&&) = delete; void set_table_factory(TransitionTableFactory f) { factory_ = std::move(f); } void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } enum class CreateResult { Created, Denied_AlreadyExists, Denied_InvalidPpid, }; // Validate `ppid` against the optional callback; if accepted, create // the PJ in Queued and fire the Created -> Queued change handler. CreateResult create(std::string prjobid, std::string ppid, std::vector materials, const std::function& ppid_exists = [](const std::string&) { return true; }) { if (jobs_.count(prjobid)) return CreateResult::Denied_AlreadyExists; if (!ppid_exists(ppid)) return CreateResult::Denied_InvalidPpid; auto fsm = std::make_unique(factory_(), ProcessJobState::Queued); // Dispatch through *this so a later set_state_change_handler() takes // effect for existing PJs (the captured-at-create snapshot pattern // would otherwise pin the old handler on jobs already in the map). fsm->set_state_change_handler( [this, id = prjobid](ProcessJobState from, ProcessJobState to, ProcessJobEvent trig) { if (on_change_) on_change_(id, from, to, trig); }); order_.push_back(prjobid); jobs_.emplace(prjobid, ProcessJob{prjobid, std::move(ppid), std::move(materials), true, std::move(fsm)}); // Synthetic NoState -> Queued so subscribers observe creation. The // server filters this so it doesn't emit a bogus S16F9 for a PJ // that's still being acked. if (on_change_) { on_change_(jobs_.find(prjobid)->first, ProcessJobState::NoState, ProcessJobState::Queued, ProcessJobEvent::Created); } return CreateResult::Created; } bool has(const std::string& prjobid) const { return jobs_.count(prjobid) > 0; } const ProcessJob* get(const std::string& prjobid) const { auto it = jobs_.find(prjobid); return it == jobs_.end() ? nullptr : &it->second; } ProcessJob* get(const std::string& prjobid) { auto it = jobs_.find(prjobid); return it == jobs_.end() ? nullptr : &it->second; } ProcessJobState state(const std::string& prjobid) const { auto it = jobs_.find(prjobid); return it == jobs_.end() ? ProcessJobState::NoState : it->second.fsm->state(); } // Host-initiated S16F5 PRJobCommand. Returns InvalidObject for unknown // PJs; CannotDoNow when the current PJ state has no row for the event. // For HOQ the FSM gates legality (Queued only) and the store performs // the actual reorder so the next CJ Select picks this PJ first. HostCmdAck on_host_command(const std::string& prjobid, ProcessJobEvent event) { auto* pj = get(prjobid); if (!pj) return HostCmdAck::InvalidObject; HostCmdAck ack = pj->fsm->on_host_command(event); if (ack == HostCmdAck::Accept && event == ProcessJobEvent::HeadOfQueue) { move_to_head(prjobid); } return ack; } // Position of `prjobid` in the queue order (insertion order; HOQ rewrites // it). Returns -1 if unknown. Exposed for tests and CJ promotion logic. int position(const std::string& prjobid) const { auto it = std::find(order_.begin(), order_.end(), prjobid); return it == order_.end() ? -1 : static_cast(it - order_.begin()); } // Internal events from the application (recipe runner etc.). bool fire_internal(const std::string& prjobid, ProcessJobEvent event) { auto* pj = get(prjobid); if (!pj) return false; return pj->fsm->on_internal(event); } // Dequeue S16F13: only legal while QUEUED. HostCmdAck dequeue(const std::string& prjobid) { auto it = jobs_.find(prjobid); if (it == jobs_.end()) return HostCmdAck::InvalidObject; if (it->second.fsm->state() != ProcessJobState::Queued) return HostCmdAck::CannotDoNow; erase_from_order(prjobid); jobs_.erase(it); return HostCmdAck::Accept; } // Remove a terminal job from the store (caller responsibility to ensure // ProcessComplete before deleting). Used after CJ Completed cleanup. bool remove(const std::string& prjobid) { erase_from_order(prjobid); return jobs_.erase(prjobid) > 0; } // Per-PJ S16F9 alert gate. E40 §10.3 leaves PRALERT control to the host // (S16F1/F2 in the full multi-create form, which we don't model yet); // exposed here so application code can toggle alerts directly. bool set_alert(const std::string& prjobid, bool enabled) { auto* pj = get(prjobid); if (!pj) return false; pj->alert_enabled = enabled; return true; } std::size_t size() const { return jobs_.size(); } // Insertion-order list of PRJOBIDs, mutated by HOQ. Used by CJ promotion // logic and by tests that need to assert ordering. const std::vector& ids() const { return order_; } private: void erase_from_order(const std::string& prjobid) { auto it = std::find(order_.begin(), order_.end(), prjobid); if (it != order_.end()) order_.erase(it); } void move_to_head(const std::string& prjobid) { auto it = std::find(order_.begin(), order_.end(), prjobid); if (it == order_.end() || it == order_.begin()) return; std::rotate(order_.begin(), it, it + 1); } std::map jobs_; std::vector order_; // queue position (E40 HOQ-aware) TransitionTableFactory factory_; StateChangeHandler on_change_; }; } // namespace secsgem::gem