From 570bf844bd0063510dceef29c2b008762b8e3969 Mon Sep 17 00:00:00 2001 From: Chee Bin Hoh Date: Sun, 30 Aug 2026 20:03:14 -0400 Subject: [PATCH 1/7] runtime-state-engine --- docs/specs/runtime-state-machine-plan.md | 132 +++++++ docs/specs/runtime-state-machine-spec.md | 481 +++++++++++++++++++++++ 2 files changed, 613 insertions(+) create mode 100644 docs/specs/runtime-state-machine-plan.md create mode 100644 docs/specs/runtime-state-machine-spec.md diff --git a/docs/specs/runtime-state-machine-plan.md b/docs/specs/runtime-state-machine-plan.md new file mode 100644 index 0000000..ce93913 --- /dev/null +++ b/docs/specs/runtime-state-machine-plan.md @@ -0,0 +1,132 @@ +# Implementation Plan: Runtime State Engine + +## 1. Goal + +Implement the runtime state engine feature as a singleton runtime-owned state machine manager built on the existing `dmn-runtime` and `dmn-state` components. + +## 2. Core Design Decisions + +### Decision 1: engine is singleton and runtime-owned +The engine follows the same singleton model as `Dmn_Runtime_Manager`. It owns the execution policy for state objects and routes state execution through the runtime scheduler. + +### Decision 2: state objects subclass `Dmn_State` +Each runtime state object remains a state machine, but adds async lifecycle metadata and `wait()` support. This keeps the base state semantics while adding runtime execution ownership. + +### Decision 3: all state execution is serialized +The engine does not allow independent parallel execution of state steps across runtime state objects. It posts work to the runtime scheduler in serialized form. + +### Decision 4: `run()` is async-only +The client never directly executes state logic in its own thread. `run()` only queues runtime tasks. The runtime executes each state step and then re-posts the next task until the machine is complete. + +## 3. Phase 1: Define the engine and object model + +### Tasks + +- define `Dmn_Runtime_State_Engine` singleton API +- define `Dmn_Runtime_State` subclass of `Dmn_State` +- add runtime lifecycle flags (`queued`, `running`, `completed`, `failed`, `cancelled`) +- add `wait()` synchronization primitive +- confirm object ownership and destroy semantics + +### Deliverables + +- public engine class declaration +- public state class declaration +- lifecycle state model +- wait mechanism design + +## 4. Phase 2: Integrate with runtime scheduler + +### Tasks + +- schedule state steps as runtime jobs using `Dmn_Runtime_Manager::addJob()` +- ensure `run()` posts work to runtime rather than executing directly +- serialize state object execution through the runtime queue +- implement continuation loop so each step schedules the next one until terminal state + +### Deliverables + +- runtime job adapter for state objects +- serialized engine dispatcher +- sequential execution loop + +## 5. Phase 3: State lifecycle and completion + +### Tasks + +- implement transitioned completion behavior +- implement failed state handling for thrown exceptions +- implement cancellation and shutdown handling +- notify waiters exactly once +- avoid re-enqueuing after terminal state + +### Deliverables + +- terminal-state semantics +- exception-safe cleanup +- completion synchronization contract + +## 6. Phase 4: API ergonomics and compatibility + +### Tasks + +- keep `Dmn_State` unchanged for synchronous scenarios +- provide a runtime-specific object for async execution +- preserve `setStateFnc()`, `setNext()`, `setEnd()`, and `runNext()` semantics +- document `wait()` semantics and restrictions + +### Deliverables + +- developer-facing API contract +- usage examples for initialization, run, and wait +- compatibility note for existing runtime and state users + +## 7. Phase 5: Validation + +### Tests to add + +- state object created from engine +- multiple state objects serialized in order +- `run()` enqueues runtime work and completes successfully +- waiting on completion returns after all steps finish +- exception sets failed terminal state +- repeated run is rejected or ignored as designed +- cancellation during queued or running state does not corrupt runtime + +### Validation commands + +- `cmake -B build -DCMAKE_BUILD_TYPE=Debug` +- `cmake --build build` +- `ctest --test-dir build --output-on-failure` + +## 8. Risks and Checkpoints + +### Risk: one state object re-enters itself +Checkpoint: ensure `run()` only posts a single pending job and does not recursively run a state before the previous task finishes. + +### Risk: wait deadlock +Checkpoint: `wait()` must never run inside the runtime async thread; it must block on a condition variable or equivalent external completion signal. + +### Risk: queue corruption during failure/cancel +Checkpoint: all failed/cancelled states must terminate the loop cleanly and never re-post further runtime tasks. + +## 9. Definition of Ready + +Implementation can begin once: + +- the engine singleton contract is approved +- the state object subclass behavior is approved +- the serialization rules are agreed +- `wait()` and failure semantics are documented + +## 10. Definition of Done + +The feature is done when: + +- the runtime state engine is implemented as a singleton +- runtime state objects subclass `Dmn_State` +- `run()` schedules async execution through `Dmn_Runtime_Manager` +- all state objects are serialized through the runtime engine +- client `wait()` supports async completion tracking +- failure and cancellation paths are verified by tests +- the library remains backward compatible with existing runtime/state APIs diff --git a/docs/specs/runtime-state-machine-spec.md b/docs/specs/runtime-state-machine-spec.md new file mode 100644 index 0000000..55b8226 --- /dev/null +++ b/docs/specs/runtime-state-machine-spec.md @@ -0,0 +1,481 @@ +# Feature Spec: Runtime State Engine + +Status: Draft + +## 1. Summary + +This feature introduces a new runtime-owned state execution engine that combines the existing `dmn-runtime` scheduler with the `dmn-state` finite-state helper. The result is a singleton runtime service that creates state objects for clients, serializes their execution through the runtime, and lets callers wait for completion without forcing the client thread to execute each state step directly. + +The new feature preserves the library’s current design philosophy: + +- `Dmn_Runtime_Manager` remains the process-wide scheduler and signal manager. +- `Dmn_State` remains the lightweight state-machine primitive. +- the new runtime state engine owns the scheduling and serialized execution policy. +- each state object is a runtime-managed, asynchronously executed state machine instance. + +The primary behavior is: + +1. client creates a state object from the `Dmn_Runtime_State_Engine` singleton; +2. client configures the state(s) on that object; +3. client calls `stateobject.run()`; +4. `run()` enqueues a runtime task into the runtime engine; +5. the runtime engine continues to repost tasks until the state object reaches its terminal condition; +6. all state objects created by the engine are executed in serialized order through `Dmn_Runtime_Manager`; +7. client may call `stateobject.wait()` to block until the runtime completes the state object. + +## 2. Design Objective + +The existing `dmn-state` component is synchronous and client-driven. It calls `runNext()` directly in the caller thread. That is useful for local control flow, but not for runtime-managed workflows. The new runtime state engine changes the ownership model: + +- the state object remains a state machine definition and execution state, +- the runtime engine owns when the state steps are executed, +- state execution is serialized within the runtime’s async context, +- the client receives an async completion signal via `wait()` rather than manually stepping the machine. + +This makes the feature a natural fit for handshake flows, retries, startup/teardown workflows, protocol states, network state transitions, and any runtime pipeline that must share the same scheduling semantics as other runtime jobs. + +## 3. Scope + +### In Scope + +- singleton runtime state engine class modeled after `Dmn_Runtime_Manager` +- state objects returned from the engine that subclass `Dmn_State` +- state registration API for user-defined states +- `run()` scheduling through the runtime engine +- serialized execution of all runtime state objects +- `wait()` completion API for async execution +- shutdown, error, cancellation, and terminal-state handling +- tests for startup, normal completion, errors, and cancellation + +### Out of Scope + +- distributed state replication +- persistence or recovery of state machines +- automatic consensus protocol orchestration +- general-purpose actor model features +- changes to existing `Dmn_State` sync API behavior + +## 4. Architectural Context + +### Existing Runtime Architecture + +The current runtime API is singleton-based and uses: + +- `Dmn_Runtime_Manager::createInstance()` for process-wide lifecycle +- `addJob()` / `addTimedJob()` for queued work +- `enterMainLoop()` / `exitMainLoop()` for the runtime loop +- `Dmn_Runtime_Job` with priority and coroutine execution support +- `Dmn_Runtime_Task` as the coroutine wrapper for job execution + +The runtime currently serializes work via its async execution model, and this is the correct execution context for the new state engine. + +### Existing State Machine Architecture + +`Dmn_State` has the following current semantics: + +- stores a vector of state functors +- uses `m_next` to drive execution +- supports `setStateFnc()`, `setNext()`, `setEnd()`, and `runNext()` +- defines init/finalize hooks +- uses `runNext()` directly in the caller context +- has no async lifecycle, no wait, and no runtime ownership + +### Feature Intent + +The runtime state engine is not a replacement for `Dmn_State`; it is a runtime-managed execution shell around it. Each state object created by the engine is still a `Dmn_State`, but with additional runtime lifetime controls and completion synchronization. + +## 5. Functional Requirements + +### FR-1: Runtime state engine existence + +A singleton class named `Dmn_Runtime_State_Engine` must exist and follow the same singleton creation conventions as `Dmn_Runtime_Manager`. + +The engine must: + +- provide `createInstance()` or equivalent singleton factory consistent with `Dmn_Singleton` +- own a runtime-managed execution queue for state objects +- ensure all state execution is scheduled through `Dmn_Runtime_Manager` + +### FR-2: Client-managed state object creation + +Clients must be able to create a state object from the runtime state engine. + +The resulting object must: + +- be a concrete state object type derived from `Dmn_State` +- be created from the runtime state engine singleton +- carry runtime-managed lifecycle metadata +- be configured by calling `setStateFnc()` or equivalent state registration methods + +### FR-3: State configuration + +A client must be able to define one or more states on the returned runtime state object via the compatible `Dmn_State` interface. + +The engine must support: + +- sequential state definition by appending state functors +- explicit transition to next state via `setNext()` and `setEnd()` semantics +- state functors that are valid in the same pattern as `Dmn_State` + +### FR-4: `run()` dispatches work to runtime + +The runtime state object must expose a public `run()` method. + +Behavior: + +- `run()` may be called from any client thread +- `run()` must schedule asynchronous work on the singleton `Dmn_Runtime_Manager` async thread +- the runtime work must execute the next state step of the state object +- after the step executes, the engine must post another runtime task to run the next state, continuing until no more states remain +- `run()` must not directly execute state logic in the caller thread + +### FR-5: Serialized execution across all state objects + +All state objects created from the runtime state engine must execute in serialized form through the shared runtime scheduler. + +Requirements: + +- no two state objects may run their next step concurrently in the same runtime engine +- state object execution order must follow runtime job ordering/priority semantics +- state step tasks must be single-threaded relative to engine execution + +### FR-6: Completion waiting via `wait()` + +Each runtime state object must support a `wait()` method. + +Behavior: + +- `wait()` blocks until the state object reaches its terminal state +- `wait()` must be safe to call from arbitrary client threads +- `wait()` must not race with runtime completion +- `wait()` returns when the state object has completed, failed, or been canceled + +### FR-7: Terminal state and completion semantics + +A runtime state object must expose completion behavior consistent with `Dmn_State` but with async runtime ownership. + +The object must track: + +- initialized state +- finalized state +- running state +- completed state +- failed state +- canceled state + +A state object is terminal when it has either: + +- reached the end via `setEnd()` or a terminal transition +- failed due to uncaught exception during a state step +- been canceled + +### FR-8: Cancellation and shutdown + +If runtime shutdown or cancellation occurs while a state object is queued or running, the engine must ensure the runtime state object is stopped deterministically and may invoke final cleanup hooks. + +The object must not leave the runtime in a partially queued state. + +### FR-9: Error propagation + +If a state callback throws while running inside the runtime-managed async thread, the runtime state engine must: + +- capture the exception +- set the state object to failed terminal state +- notify any waiting client via `wait()` +- avoid corrupting the runtime scheduler internal state + +## 6. Non-Functional Requirements + +### NFR-1: Singleton and runtime-thread ownership + +The runtime state engine must preserve the runtime’s model: client code may call APIs from any thread, but the actual state execution must be marshaled to the runtime async thread. + +### NFR-2: Backward compatibility + +This feature must not break the existing `Dmn_Runtime_Manager` or `Dmn_State` APIs. It is additive only. + +### NFR-3: Determinism + +State execution within one runtime engine must be deterministic with respect to queue ordering and posting order. + +### NFR-4: Controlled memory lifetime + +The runtime state engine must own or manage the lifecycle of runtime state objects so that they are not destroyed while their task is still pending. + +### NFR-5: Thread safety for wait semantics + +`wait()` must be implemented using synchronization primitives appropriate for cross-thread signaling (`std::condition_variable`, flag, or equivalent) and must not busy-spin. + +## 7. Proposed API Shape + +This API is proposed to match the existing library naming and runtime conventions. + +```cpp +namespace dmn { + +class Dmn_Runtime_State_Engine + : public Dmn_Singleton { +public: + static auto createInstance() -> Dmn_Runtime_State_Engine &; + + class Dmn_Runtime_State; + + Dmn_Runtime_State createState(std::string_view name); + void runState(Dmn_Runtime_State &state); +}; + +class Dmn_Runtime_State : public Dmn_State { +public: + using FncType = std::function; + + explicit Dmn_Runtime_State(std::string_view name); + + void setStateFnc(FncType fnc, int index = 0); + void setNext(int index); + void setNext(); + void setEnd(); + + void run(); + void wait(); + + bool isRunning() const; + bool isCompleted() const; + bool isFailed() const; + bool isCancelled() const; + +protected: + void onStarted(); + void onCompleted(); + void onFailed(std::exception_ptr ep); + void onCancelled(); + +private: + std::mutex m_waitMutex; + std::condition_variable m_waitCv; + std::atomic_bool m_running{false}; + std::atomic_bool m_completed{false}; + std::atomic_bool m_failed{false}; + std::atomic_bool m_cancelled{false}; + std::atomic_bool m_queued{false}; + std::atomic_bool m_waiting{false}; +}; + +} // namespace dmn +``` + +### API Notes + +- `Dmn_Runtime_State_Engine::createState()` returns a state object associated with the singleton runtime engine. +- `Dmn_Runtime_State` inherits from `Dmn_State` and adds async runtime lifecycle behavior. +- `run()` does not execute the state in caller thread; it only schedules runtime execution. +- `wait()` blocks until the state object completes, fails, or cancels. +- The engine serializes all created state objects through a common runtime queue. + +## 8. Execution Model + +### 8.1 State object lifecycle + +A runtime state object has the following lifecycle: + +1. Created by `Dmn_Runtime_State_Engine::createState()` +2. Configured by setting state functors (`setStateFnc`, etc.) +3. Idle before `run()` is called +4. Queued for runtime execution after `run()` +5. Running inside runtime async thread +6. Finalized once terminal condition reached +7. Client may call `wait()` at any time after submission + +### 8.2 `run()` exact semantics + +When `stateobject.run()` is called: + +1. the state object must be validated for legal execution +2. if not already running/completed, it is marked queued +3. a runtime job is scheduled in the runtime engine +4. the scheduled job executes the next state step using the state object +5. after the state step finishes, the runtime engine checks whether another state remains +6. if another state exists, the engine posts a new runtime job for the next step +7. if no state remains, the object transitions to completed terminal state +8. all waiting consumers are notified + +This loop continues until no more states to run. The runtime engine is responsible for re-posting tasks while the object remains active. + +### 8.3 Serialization requirement + +All state object tasks must be executed in serialized form through the runtime queue. That means: + +- no direct parallel execution of multiple runtime state objects +- step execution order is driven by runtime job posting order +- the runtime scheduler remains the single authority on dispatch + +This requirement is intentionally stricter than simply “each object runs in its own task”; it guarantees a predictable runtime execution model that matches the rest of the library. + +## 9. State Object Contract + +### 9.1 Subclassing `Dmn_State` + +The new runtime state object must subclass `Dmn_State` and preserve `Dmn_State` semantics while adding runtime lifecycle tracking. + +It must retain: + +- `setStateFnc()`, `setNext()`, `setEnd()`, `runNext()` semantics +- init/finalize behavior inherited from the base +- default state sequencing model + +The runtime layer adds async ownership and completion signaling on top of the base semantics. + +### 9.2 State execution in runtime context + +A runtime state object must execute its step logic inside the runtime async thread, not in the caller thread. This is required to guarantee serialized execution and consistent signal behavior. + +### 9.3 `wait()` behavior + +`wait()` should be implemented as follows: + +- if the state is already completed, return immediately +- if the state is still queued or running, block until the terminal condition is reached +- if the state fails or cancels, the wait returns after the failure/cancel status is finalized + +## 10. Detailed Behavior and Edge Cases + +### 10.1 No state configured + +If no state is defined before `run()`, the engine must not enqueue an invalid task. The object should transition to failed or finalized with a clear error condition. + +### 10.2 Repeated `run()` calls + +A runtime state object must not be run multiple times simultaneously. + +Behavior: + +- if `run()` is called while already queued or running, it should be ignored or return false +- repeated `run()` calls after completion should be rejected or treated as no-op depending on implementation policy + +Recommended behavior: reject with an error/exception in the engine if the state is already active. + +### 10.3 Finalized or canceled states + +Once finalized, failed, or canceled, no further state step may be scheduled. + +### 10.4 Exception propagation + +If a state callback throws while inside the runtime engine: + +- the current state machine transitions to failed +- the exception is captured in `std::exception_ptr` +- any waiters are notified +- the runtime queue remains valid + +### 10.5 Shutdown race + +If `exitMainLoop()` is called while a runtime state object is being processed, the state object should be finished or cancelled in a deterministic way without corrupting runtime scheduler state. + +## 11. Serialization and Scheduling Contract + +The engine is responsible for ensuring serialized processing across all state instances it creates. + +Core contract: + +- every runtime state object instance is queued as a runtime job +- runtime jobs are executed through `Dmn_Runtime_Manager` +- the runtime engine maintains a serialized dispatcher for all queued state jobs +- each runtime state job triggers exactly one state step, then posts another job if needed + +This design matches the rest of the runtime library while introducing a higher-level state lifecycle. + +## 12. Test Plan + +### Unit Tests + +- `create_state_from_runtime_engine` +- `state_run_posts_runtime_job` +- `state_wait_completes_after_all_steps` +- `state_serialization_maintains_order` +- `state_exception_marks_failed` +- `state_cancel_marks_cancelled` +- `state_no_state_defined_fails_cleanly` +- `run_is_rejected_when_state_already_active` + +### Integration Tests + +- run multiple runtime state objects in the same runtime engine +- verify that state steps are serialized in posting order +- verify `wait()` returns after sequence completion +- verify runtime jobs remain valid when the state object reaches terminal condition + +### Stress / Regression Tests + +- repeated create/run/wait cycles +- many state objects queued sequentially +- failure followed by cleanup and reuse +- shutdown while state objects are queued + +## 13. Acceptance Criteria + +The feature is accepted when all of the following are true: + +- clients can create runtime-managed state objects from a singleton engine +- state objects subclass `Dmn_State` and retain base state semantics +- `stateobject.run()` schedules work into the runtime engine instead of running directly in client thread +- state execution is serialized through the runtime engine +- `stateobject.wait()` blocks until runtime completion or terminal failure +- exceptions and cancellation leave the runtime in a valid state +- documentation and examples exist for typical runtime state workflow usage + +## 14. Risks and Mitigations + +### Risk: re-entrant scheduling loop +Mitigation: `run()` must schedule a single runtime task per state step and stop when the terminal condition is reached. No unbounded recursive scheduling loop is allowed. + +### Risk: wait deadlock +Mitigation: do not call `wait()` from inside the runtime async thread. The runtime state engine must document that `wait()` is a client-side synchronization method. + +### Risk: queued state object lifetime issues +Mitigation: the runtime engine must hold ownership or lifecycle references to queued state objects until they are completed or canceled. + +### Risk: serializer starvation +Mitigation: the runtime engine must keep job posting small, deterministic, and bounded; no state object should be allowed to monopolize the queue indefinitely. + +## 15. Implementation Notes + +This feature should be implemented as additive API layered on top of the existing runtime and state components, not as a rewrite of them. + +Implementation should reuse: + +- `Dmn_Runtime_Manager` for the process-wide scheduler +- `Dmn_State` for the state machine mechanics +- `Dmn_Runtime_Job` and `Dmn_Runtime_Task` for runtime dispatch + +The runtime state engine should primarily add: + +- state object lifecycle tracking +- queueing and serialization logic +- `wait()` synchronization +- terminal-state finalization + +## 16. Definition of Done + +The feature is complete when: + +- the singleton runtime state engine is designed and documented +- the runtime state object class is specified and matches the required async semantics +- `run()` and `wait()` behavior are documented and tested +- serialized execution through `Dmn_Runtime_Manager` is verified +- shutdown, failure, and cancellation semantics are validated +- example usage patterns are included in the documentation + +## 17. Recommended Milestones + +1. Create the engine singleton and state object base model +2. Add runtime scheduling and serialized execution loop +3. Add completion/failure/cancel tracking and `wait()` synchronization +4. Add tests for execution order, completion, and failures +5. Validate the runtime integration with `Dmn_Runtime_Manager` + +--- + +This specification is derived from current runtime and state abstractions in: + +- `include/dmn-runtime.hpp` +- `include/dmn-runtime-task.hpp` +- `include/dmn-state.hpp` From 59489ff2fdd9d633aa30dd8a7ba1835179b7ab0f Mon Sep 17 00:00:00 2001 From: Chee Bin HOH Date: Sun, 30 Aug 2026 20:33:14 -0400 Subject: [PATCH 2/7] spec: update runtime state engine spec - ownership, run(onError), cancel(), wait(timeout)/future, tests --- docs/specs/runtime-state-machine-spec.md | 307 ++++++++++++++--------- 1 file changed, 194 insertions(+), 113 deletions(-) diff --git a/docs/specs/runtime-state-machine-spec.md b/docs/specs/runtime-state-machine-spec.md index 55b8226..cc3f033 100644 --- a/docs/specs/runtime-state-machine-spec.md +++ b/docs/specs/runtime-state-machine-spec.md @@ -15,13 +15,13 @@ The new feature preserves the library’s current design philosophy: The primary behavior is: -1. client creates a state object from the `Dmn_Runtime_State_Engine` singleton; +1. client obtains a state handle from the `Dmn_Runtime_State_Engine` singleton; 2. client configures the state(s) on that object; -3. client calls `stateobject.run()`; -4. `run()` enqueues a runtime task into the runtime engine; -5. the runtime engine continues to repost tasks until the state object reaches its terminal condition; -6. all state objects created by the engine are executed in serialized order through `Dmn_Runtime_Manager`; -7. client may call `stateobject.wait()` to block until the runtime completes the state object. +3. client calls `statehandle->run()`; +4. `run()` enqueues a runtime task into the runtime engine and returns a boolean indicating whether the enqueue succeeded; +5. the runtime engine continues to repost tasks until the state object reaches its terminal condition; errors occurring in async execution invoke a client-provided onError callback (if supplied) and are captured on the state handle; +6. all state objects created by the engine are executed in serialized order through `Dmn_Runtime_Manager` (subject to the engine's serialization policy); +7. client may call `statehandle->wait()` or use the returned future to block or asynchronously observe completion. ## 2. Design Objective @@ -30,7 +30,7 @@ The existing `dmn-state` component is synchronous and client-driven. It calls `r - the state object remains a state machine definition and execution state, - the runtime engine owns when the state steps are executed, - state execution is serialized within the runtime’s async context, -- the client receives an async completion signal via `wait()` rather than manually stepping the machine. +- the client receives an async completion signal via `wait()` or a future rather than manually stepping the machine. This makes the feature a natural fit for handshake flows, retries, startup/teardown workflows, protocol states, network state transitions, and any runtime pipeline that must share the same scheduling semantics as other runtime jobs. @@ -39,13 +39,13 @@ This makes the feature a natural fit for handshake flows, retries, startup/teard ### In Scope - singleton runtime state engine class modeled after `Dmn_Runtime_Manager` -- state objects returned from the engine that subclass `Dmn_State` +- state objects returned from the engine as managed handles (shared ownership) and that subclass `Dmn_State` - state registration API for user-defined states -- `run()` scheduling through the runtime engine -- serialized execution of all runtime state objects -- `wait()` completion API for async execution +- `run()` scheduling through the runtime engine with onError callback forwarding to `dmn-runtime` semantics +- serialized execution of all runtime state objects (engine-global by default) +- `wait()` completion API for async execution with optional timeout and a future-based async alternative - shutdown, error, cancellation, and terminal-state handling -- tests for startup, normal completion, errors, and cancellation +- tests for startup, normal completion, errors, cancellation, and lifetime edge cases ### Out of Scope @@ -96,14 +96,21 @@ The engine must: - own a runtime-managed execution queue for state objects - ensure all state execution is scheduled through `Dmn_Runtime_Manager` -### FR-2: Client-managed state object creation +### FR-2: Client-managed state object creation and ownership -Clients must be able to create a state object from the runtime state engine. +Clients must be able to obtain a managed handle to a state object from the runtime state engine. + +Ownership model (required): + +- `createState()` MUST return a managed handle type: `std::shared_ptr` (or an alias) following the existing dmn pattern used by other components (for example, `dmn-dmesg` and other resource holders in the codebase). +- The engine must retain a `std::shared_ptr` to the state object while it is queued or running. This guarantees the object remains alive until it reaches a terminal state even if the client drops its handle. +- When the object becomes terminal (completed/failed/cancelled), the engine releases its internal shared_ptr; any remaining client-held shared_ptr keeps the object alive until all references are dropped. +- The spec must include sample usage showing how clients keep a handle, but clients may also intentionally let the engine own a state for fire-and-forget semantics. The resulting object must: - be a concrete state object type derived from `Dmn_State` -- be created from the runtime state engine singleton +- be created from the runtime state engine singleton and returned as a shared_ptr handle - carry runtime-managed lifecycle metadata - be configured by calling `setStateFnc()` or equivalent state registration methods @@ -117,38 +124,42 @@ The engine must support: - explicit transition to next state via `setNext()` and `setEnd()` semantics - state functors that are valid in the same pattern as `Dmn_State` -### FR-4: `run()` dispatches work to runtime +### FR-4: `run()` dispatches work to runtime and error callback forwarding -The runtime state object must expose a public `run()` method. +The runtime state handle must expose a public `run()` method. In addition, `run()` MUST accept an optional onError callback that matches the `dmn-runtime` onError callback signature so callers can receive asynchronous error notifications. Behavior: - `run()` may be called from any client thread - `run()` must schedule asynchronous work on the singleton `Dmn_Runtime_Manager` async thread -- the runtime work must execute the next state step of the state object +- `run()` returns `true` if the state was successfully queued (or started) and `false` if the enqueue failed (invalid state, already terminal, or internal error) +- an optional `onError` callback provided to `run()` is forwarded to the underlying `dmn-runtime` job so that asynchronous runtime failures invoke the client callback when the runtime job reports an error +- the runtime work must execute exactly one next state step of the state object per posted job - after the step executes, the engine must post another runtime task to run the next state, continuing until no more states remain - `run()` must not directly execute state logic in the caller thread ### FR-5: Serialized execution across all state objects -All state objects created from the runtime state engine must execute in serialized form through the shared runtime scheduler. +All state objects created from the runtime state engine must execute in serialized form through the shared runtime scheduler by default. Requirements: -- no two state objects may run their next step concurrently in the same runtime engine +- no two state objects may run their next step concurrently in the same runtime engine (default global serialization) - state object execution order must follow runtime job ordering/priority semantics - state step tasks must be single-threaded relative to engine execution +- the engine MAY provide configuration for relaxed concurrency (optional extension) but default behavior must be serialized to match the spec -### FR-6: Completion waiting via `wait()` +### FR-6: Completion waiting via `wait()` and async alternatives -Each runtime state object must support a `wait()` method. +Each runtime state object must support a `wait()` method and provide safer alternatives to avoid deadlocks. Behavior: - `wait()` blocks until the state object reaches its terminal state -- `wait()` must be safe to call from arbitrary client threads -- `wait()` must not race with runtime completion -- `wait()` returns when the state object has completed, failed, or been canceled +- `wait()` MUST support an overload `wait(std::chrono::milliseconds timeout)` (or templated duration) that returns a boolean indicating whether the wait observed terminal completion before the timeout +- `wait()` MUST NOT be called from the runtime async thread; calling from the runtime thread is undefined — implementations SHOULD assert or return immediately with an error when detected +- in addition to blocking wait(), the object MUST provide a `std::future getFuture()` (or equivalent) so callers can use non-blocking or async wait patterns on other threads or coroutines +- `wait()` and future completion must be signalled using a condition variable / promise and must handle spurious wakeups correctly ### FR-7: Terminal state and completion semantics @@ -171,17 +182,24 @@ A state object is terminal when it has either: ### FR-8: Cancellation and shutdown -If runtime shutdown or cancellation occurs while a state object is queued or running, the engine must ensure the runtime state object is stopped deterministically and may invoke final cleanup hooks. +The runtime state object MUST provide a `cancel()` method that is cooperative in nature. -The object must not leave the runtime in a partially queued state. +Semantics: -### FR-9: Error propagation +- `cancel()` is idempotent and sets the object's canceled flag and prevents further non-cooperative steps from being scheduled +- `cancel()` does NOT asynchronously preempt a currently executing state functor unless that functor explicitly checks a cancellation token exposed by the state and co-operates +- when the runtime task executes and detects the cancel flag is set, it MUST call `setEnd()` before invoking `runNext()` so that the state finalizes deterministically rather than executing further steps +- the engine must provide a way for state functors to query the cancel flag (e.g. `isCancelled()` or a cancellation token) so they can early-exit and perform graceful cleanup +- on shutdown, the engine must support at least two modes: graceful (finish current step and queued states) and immediate (mark queued as cancelled and notify waiters). The exact mode shall be selectable via engine shutdown API. + +### FR-9: Error propagation and onError callback If a state callback throws while running inside the runtime-managed async thread, the runtime state engine must: - capture the exception - set the state object to failed terminal state -- notify any waiting client via `wait()` +- notify any waiting client via `wait()` or future +- invoke the onError callback supplied by the client (if any) with the runtime's error details - avoid corrupting the runtime scheduler internal state ## 6. Non-Functional Requirements @@ -196,11 +214,11 @@ This feature must not break the existing `Dmn_Runtime_Manager` or `Dmn_State` AP ### NFR-3: Determinism -State execution within one runtime engine must be deterministic with respect to queue ordering and posting order. +State execution within one runtime engine must be deterministic with respect to queue ordering and posting order when preservation of ordering is requested by the client. -### NFR-4: Controlled memory lifetime +### NFR-4: Controlled memory lifetime (explicit) -The runtime state engine must own or manage the lifecycle of runtime state objects so that they are not destroyed while their task is still pending. +The engine MUST hold a `std::shared_ptr` to any queued/running state object until terminal state is reached. This mirrors existing dmn ownership patterns (see `dmn-dmesg` for a similar handle + engine-owned shared_ptr pattern). ### NFR-5: Thread safety for wait semantics @@ -208,7 +226,7 @@ The runtime state engine must own or manage the lifecycle of runtime state objec ## 7. Proposed API Shape -This API is proposed to match the existing library naming and runtime conventions. +This API is proposed to match the existing library naming and runtime conventions while reflecting the ownership and error/cancel semantics requested. ```cpp namespace dmn { @@ -220,24 +238,47 @@ public: class Dmn_Runtime_State; - Dmn_Runtime_State createState(std::string_view name); - void runState(Dmn_Runtime_State &state); + // handle type returned to clients. Follows existing dmn shared ownership pattern. + using DmnRuntimeStatePtr = std::shared_ptr; + + // createState returns a shared_ptr handle. Engine will also keep a shared_ptr while the + // state is queued or running to guarantee lifetime. + DmnRuntimeStatePtr createState(std::string_view name); + + // optional engine-level shutdown / configuration APIs omitted for brevity }; class Dmn_Runtime_State : public Dmn_State { public: using FncType = std::function; + using OnErrorFnc = std::function; // forwarded to runtime's onError explicit Dmn_Runtime_State(std::string_view name); + // state configuration (same as Dmn_State) void setStateFnc(FncType fnc, int index = 0); void setNext(int index); void setNext(); void setEnd(); - void run(); + // lifecycle APIs + // run: returns true when the enqueue succeeded; false on failure (already terminal, invalid, runtime busy) + // optional onError is forwarded to the runtime job so callers get notified of async job failure. + bool run(OnErrorFnc onError = nullptr); + + // cancel is cooperative and idempotent. If called before a step runs, the running task will + // observe the cancelled flag, call setEnd() and transition to terminal state instead of executing further steps. + void cancel(); + + // wait blocks until terminal state. wait(timeout) returns true if it observed terminal state before timeout. void wait(); + template + bool wait_for(const std::chrono::duration &timeout); + // future-based async alternative + std::future getFuture(); + + // introspection bool isRunning() const; bool isCompleted() const; bool isFailed() const; @@ -250,14 +291,21 @@ protected: void onCancelled(); private: + // synchronization / state std::mutex m_waitMutex; std::condition_variable m_waitCv; + std::promise m_completionPromise; // getFuture() returns m_completionPromise.get_future() + std::atomic_bool m_running{false}; std::atomic_bool m_completed{false}; std::atomic_bool m_failed{false}; std::atomic_bool m_cancelled{false}; std::atomic_bool m_queued{false}; std::atomic_bool m_waiting{false}; + + // captured failure for diagnostics + std::mutex m_failureMutex; + std::exception_ptr m_failureEp{nullptr}; }; } // namespace dmn @@ -265,11 +313,11 @@ private: ### API Notes -- `Dmn_Runtime_State_Engine::createState()` returns a state object associated with the singleton runtime engine. -- `Dmn_Runtime_State` inherits from `Dmn_State` and adds async runtime lifecycle behavior. -- `run()` does not execute the state in caller thread; it only schedules runtime execution. -- `wait()` blocks until the state object completes, fails, or cancels. -- The engine serializes all created state objects through a common runtime queue. +- `Dmn_Runtime_State_Engine::createState()` returns a `std::shared_ptr` handle. The engine retains a shared_ptr while the state is queued/running to ensure safe lifetime. +- `Dmn_Runtime_State::run(OnErrorFnc)` returns a boolean: true on successful enqueue, false if enqueue failed (e.g., already terminal or invalid state). +- `run()` accepts an optional onError callback that is forwarded to the `dmn-runtime` job plumbing; if the runtime reports an error, the callback will be invoked with the exception_ptr. +- `cancel()` is cooperative: it sets a cancelled flag. The runtime job, before calling `runNext()`, must check isCancelled() and call `setEnd()` if the state has been cancelled so that the state finalizes without executing further steps. +- `wait()` supports a timeout variant; a future is also available for non-blocking waits. ## 8. Execution Model @@ -277,38 +325,33 @@ private: A runtime state object has the following lifecycle: -1. Created by `Dmn_Runtime_State_Engine::createState()` +1. Created by `Dmn_Runtime_State_Engine::createState()` and returned as a shared_ptr handle 2. Configured by setting state functors (`setStateFnc`, etc.) 3. Idle before `run()` is called -4. Queued for runtime execution after `run()` +4. Queued for runtime execution after `run()` (engine retains shared_ptr) 5. Running inside runtime async thread -6. Finalized once terminal condition reached -7. Client may call `wait()` at any time after submission +6. Finalized once terminal condition reached (engine releases internal shared_ptr) +7. Client may call `wait()` at any time after submission or use the future returned by `getFuture()` ### 8.2 `run()` exact semantics -When `stateobject.run()` is called: +When `statehandle->run(onError)` is called: 1. the state object must be validated for legal execution -2. if not already running/completed, it is marked queued -3. a runtime job is scheduled in the runtime engine -4. the scheduled job executes the next state step using the state object -5. after the state step finishes, the runtime engine checks whether another state remains -6. if another state exists, the engine posts a new runtime job for the next step -7. if no state remains, the object transitions to completed terminal state -8. all waiting consumers are notified +2. if not already running/completed, it is marked queued and the engine stores an internal shared_ptr +3. a runtime job is scheduled in the runtime engine; the job is created with the supplied onError callback forwarded to the runtime +4. the scheduled job executes exactly one next state step using the state object +5. before calling `runNext()`, the runtime job checks the cancel flag; if cancelled, it must call `setEnd()` and finalize instead of running the step +6. after the state step finishes, the runtime engine checks whether another state remains +7. if another state exists and not cancelled, the engine posts a new runtime job for the next step +8. if no state remains, the object transitions to completed terminal state and the engine notifies waiters (condition variable and promise) +9. if a state callback throws, the exception is captured, the object transitions to failed, waiters are notified, and the optional onError callback is invoked This loop continues until no more states to run. The runtime engine is responsible for re-posting tasks while the object remains active. -### 8.3 Serialization requirement +### 8.3 Serialization requirement and optional config -All state object tasks must be executed in serialized form through the runtime queue. That means: - -- no direct parallel execution of multiple runtime state objects -- step execution order is driven by runtime job posting order -- the runtime scheduler remains the single authority on dispatch - -This requirement is intentionally stricter than simply “each object runs in its own task”; it guarantees a predictable runtime execution model that matches the rest of the library. +All state object tasks must be executed in serialized form through the runtime queue by default. This strict global serialization simplifies reasoning and matches the project's stated intent. Implementations SHOULD provide configuration for relaxed concurrency (e.g., per-engine worker count) as an optional extension, but that must be explicitly chosen and documented by the caller. ## 9. State Object Contract @@ -322,25 +365,26 @@ It must retain: - init/finalize behavior inherited from the base - default state sequencing model -The runtime layer adds async ownership and completion signaling on top of the base semantics. - -### 9.2 State execution in runtime context +The runtime layer adds async ownership, completion signaling, cancellation token, and error forwarding on top of the base semantics. -A runtime state object must execute its step logic inside the runtime async thread, not in the caller thread. This is required to guarantee serialized execution and consistent signal behavior. +### 9.2 Cancellation contract -### 9.3 `wait()` behavior +- `cancel()` sets the cancellation flag and is safe to call from any thread. +- The runtime job must observe `isCancelled()` before executing `runNext()` and call `setEnd()` to force deterministic finalization. +- State functors may query `isCancelled()` if they want to implement cooperative cancellation. -`wait()` should be implemented as follows: +### 9.3 `wait()` behavior and deadlock avoidance -- if the state is already completed, return immediately -- if the state is still queued or running, block until the terminal condition is reached -- if the state fails or cancels, the wait returns after the failure/cancel status is finalized +- `wait()` blocks until the object is terminal. +- Implementations must detect (or document) that `wait()` MUST NOT be called from the runtime async thread. Preferably, `wait()` asserts or returns an error when invoked from runtime thread context. +- `wait_for(timeout)` returns a bool indicating whether the wait observed terminal completion before the timeout expired. +- `getFuture()` provides an async, non-blocking alternative which is safe to use from the runtime thread if the runtime supports async continuation semantics (otherwise the caller should still avoid awaiting it on the runtime thread). ## 10. Detailed Behavior and Edge Cases ### 10.1 No state configured -If no state is defined before `run()`, the engine must not enqueue an invalid task. The object should transition to failed or finalized with a clear error condition. +If no state is defined before `run()`, the engine must not enqueue an invalid task. The object should either immediately transition to failed with a clear exception captured (and optional onError invoked), or the run() call should return false indicating invalid configuration. ### 10.2 Repeated `run()` calls @@ -348,44 +392,43 @@ A runtime state object must not be run multiple times simultaneously. Behavior: -- if `run()` is called while already queued or running, it should be ignored or return false -- repeated `run()` calls after completion should be rejected or treated as no-op depending on implementation policy +- if `run()` is called while already queued or running, it MUST return false +- repeated `run()` calls after completion MUST return false -Recommended behavior: reject with an error/exception in the engine if the state is already active. +This explicit boolean return covers the recommended behavior and aligns with the request. ### 10.3 Finalized or canceled states Once finalized, failed, or canceled, no further state step may be scheduled. -### 10.4 Exception propagation +### 10.4 Exception propagation and onError If a state callback throws while inside the runtime engine: -- the current state machine transitions to failed -- the exception is captured in `std::exception_ptr` -- any waiters are notified -- the runtime queue remains valid +- capture the exception in `std::exception_ptr` stored on the object +- transition object to failed terminal state +- set the promise / notify the condition variable so waiters/future observers are notified +- invoke the optional onError callback provided to `run()` with the captured exception +- ensure runtime queue remains healthy -### 10.5 Shutdown race +### 10.5 Shutdown race and modes -If `exitMainLoop()` is called while a runtime state object is being processed, the state object should be finished or cancelled in a deterministic way without corrupting runtime scheduler state. +Engine shutdown must support at least two modes (configurable when shutting down): -## 11. Serialization and Scheduling Contract +- graceful: do not accept new state runs; let currently running state steps complete and drain queued states before finalizing them (clients may call wait() to observe completion) +- immediate: cancel queued (but not yet running) states and mark them cancelled; running steps proceed to completion or observe cancellation cooperatively -The engine is responsible for ensuring serialized processing across all state instances it creates. +The spec requires tests for both modes. -Core contract: +## 11. Serialization and Scheduling Contract -- every runtime state object instance is queued as a runtime job -- runtime jobs are executed through `Dmn_Runtime_Manager` -- the runtime engine maintains a serialized dispatcher for all queued state jobs -- each runtime state job triggers exactly one state step, then posts another job if needed +The engine is responsible for ensuring serialized processing across all state instances it creates (by default). Each queued job should trigger exactly one `runNext()` invocation and then repost if required. -This design matches the rest of the runtime library while introducing a higher-level state lifecycle. +Provide clear priority mapping between engine jobs and other runtime jobs. The engine must not starve other runtime jobs; use the runtime's priority scheme and document how engine tasks are enqueued. -## 12. Test Plan +## 12. Test Plan (expanded) -### Unit Tests +### Unit Tests (additions focusing on the requested gaps) - `create_state_from_runtime_engine` - `state_run_posts_runtime_job` @@ -396,12 +439,22 @@ This design matches the rest of the runtime library while introducing a higher-l - `state_no_state_defined_fails_cleanly` - `run_is_rejected_when_state_already_active` +Additional edge-case tests (required): + +- `destructor_while_queued`: verify engine-held shared_ptr keeps object alive while queued and that finalization occurs correctly when the client drops its handle +- `wait_from_runtime_thread_detected`: ensure wait() from runtime thread either asserts or returns error +- `cancel_before_run_or_queued`: calling cancel() before a queued step prevents runNext() and finalizes the state +- `cancel_during_queued_execution`: cancellation while queued leads the runtime job to call setEnd() rather than executing further steps +- `run_onerror_callback_invoked`: ensure onError passed to run() is invoked when the runtime job fails +- `shutdown_graceful_vs_immediate`: test both shutdown modes and their effects on queued and running states + ### Integration Tests - run multiple runtime state objects in the same runtime engine - verify that state steps are serialized in posting order -- verify `wait()` returns after sequence completion +- verify wait() and future complete after sequence completion - verify runtime jobs remain valid when the state object reaches terminal condition +- stress test many queued states to measure the effect of global serialization ### Stress / Regression Tests @@ -414,31 +467,32 @@ This design matches the rest of the runtime library while introducing a higher-l The feature is accepted when all of the following are true: -- clients can create runtime-managed state objects from a singleton engine +- clients can create runtime-managed state objects from a singleton engine using a shared_ptr handle - state objects subclass `Dmn_State` and retain base state semantics -- `stateobject.run()` schedules work into the runtime engine instead of running directly in client thread -- state execution is serialized through the runtime engine -- `stateobject.wait()` blocks until runtime completion or terminal failure -- exceptions and cancellation leave the runtime in a valid state -- documentation and examples exist for typical runtime state workflow usage +- `statehandle->run(onError)` schedules work into the runtime engine and returns true/false to indicate success +- state execution is serialized through the runtime engine by default +- `statehandle->wait()` and `wait_for()` block until runtime completion or terminal failure and `getFuture()` is available for async waiting +- `cancel()` cooperatively finalizes execution and prevents future steps; cancel semantics are documented and tested +- exceptions and cancellation leave the runtime in a valid state and optional onError callbacks are invoked +- documentation, examples and tests exist for typical runtime state workflow usage and lifetime edge cases -## 14. Risks and Mitigations +## 14. Risks and Mitigations (updated) ### Risk: re-entrant scheduling loop -Mitigation: `run()` must schedule a single runtime task per state step and stop when the terminal condition is reached. No unbounded recursive scheduling loop is allowed. +Mitigation: `run()` must schedule a single runtime task per state step and stop when the terminal condition is reached. No unbounded recursive scheduling loop is allowed. Atomic queued flag prevents duplicate enqueues. ### Risk: wait deadlock -Mitigation: do not call `wait()` from inside the runtime async thread. The runtime state engine must document that `wait()` is a client-side synchronization method. +Mitigation: do not call `wait()` from inside the runtime async thread. Document and assert this in debug builds; prefer future-based wait from runtime thread contexts. ### Risk: queued state object lifetime issues -Mitigation: the runtime engine must hold ownership or lifecycle references to queued state objects until they are completed or canceled. +Mitigation: engine must hold a `std::shared_ptr` while queued. Returning a shared_ptr to clients and holding an internal shared_ptr mirrors the existing dmn pattern used in other subsystems (see `dmn-dmesg`). ### Risk: serializer starvation -Mitigation: the runtime engine must keep job posting small, deterministic, and bounded; no state object should be allowed to monopolize the queue indefinitely. +Mitigation: the runtime engine must keep job postings small, deterministic, and bounded; the runtime's priority and scheduling mechanisms should be used to avoid starvation. Consider adding a configurable fairness mechanism for long-running state functors. ## 15. Implementation Notes -This feature should be implemented as additive API layered on top of the existing runtime and state components, not as a rewrite of them. +This feature should be implemented as an additive API layered on top of the existing runtime and state components. Implementation should reuse: @@ -448,17 +502,44 @@ Implementation should reuse: The runtime state engine should primarily add: -- state object lifecycle tracking +- state object lifecycle tracking using shared_ptr handles - queueing and serialization logic -- `wait()` synchronization -- terminal-state finalization +- `wait()` synchronization (condition_variable + promise/future) +- terminal-state finalization and onError callback forwarding +- cooperative cancel() semantics + +Sample usage (illustrative): + +```cpp +using StatePtr = dmn::Dmn_Runtime_State_Engine::DmnRuntimeStatePtr; + +// create +StatePtr s = dmn::Dmn_Runtime_State_Engine::createInstance().createState("example"); + +// configure +s->setStateFnc([](dmn::Dmn_Runtime_State &st){ /* step 0 */ }, 0); +s->setNext(); +s->setStateFnc([](dmn::Dmn_Runtime_State &st){ /* step 1 */ }, 1); +s->setEnd(); + +// run with onError callback +bool ok = s->run([](std::exception_ptr ep){ /* log or inspect error */ }); +if (!ok) { /* handle enqueue failure */ } + +// wait (blocking) +s->wait(); + +// or async +auto fut = s->getFuture(); +fut.wait(); +``` ## 16. Definition of Done The feature is complete when: -- the singleton runtime state engine is designed and documented -- the runtime state object class is specified and matches the required async semantics +- the singleton runtime state engine is designed and documented with shared_ptr handle ownership +- the runtime state object class is specified and matches the required async semantics (run returning bool, onError forwarding, cancel, wait/timeout/future) - `run()` and `wait()` behavior are documented and tested - serialized execution through `Dmn_Runtime_Manager` is verified - shutdown, failure, and cancellation semantics are validated @@ -466,10 +547,10 @@ The feature is complete when: ## 17. Recommended Milestones -1. Create the engine singleton and state object base model -2. Add runtime scheduling and serialized execution loop -3. Add completion/failure/cancel tracking and `wait()` synchronization -4. Add tests for execution order, completion, and failures +1. Create the engine singleton and state object base model (shared_ptr handle) +2. Add runtime scheduling and serialized execution loop with onError forwarding +3. Add completion/failure/cancel tracking, wait(timeout), and future support +4. Add tests for execution order, completion, failures, destructor-while-queued, and shutdown modes 5. Validate the runtime integration with `Dmn_Runtime_Manager` --- From 7f592f5f5158f87dac443992d10dd21953cdec34 Mon Sep 17 00:00:00 2001 From: Chee Bin HOH Date: Sun, 30 Aug 2026 20:53:34 -0400 Subject: [PATCH 3/7] spec: align run/wait/cancel/ownership with runtime conventions; add priority/timed variants; use shared_future; runtime-thread policy; tests --- docs/specs/runtime-state-machine-spec.md | 188 +++++++++++------------ 1 file changed, 93 insertions(+), 95 deletions(-) diff --git a/docs/specs/runtime-state-machine-spec.md b/docs/specs/runtime-state-machine-spec.md index cc3f033..3d343ec 100644 --- a/docs/specs/runtime-state-machine-spec.md +++ b/docs/specs/runtime-state-machine-spec.md @@ -17,11 +17,11 @@ The primary behavior is: 1. client obtains a state handle from the `Dmn_Runtime_State_Engine` singleton; 2. client configures the state(s) on that object; -3. client calls `statehandle->run()`; +3. client calls `statehandle->run()` (optionally with priority / delay / onError handler); 4. `run()` enqueues a runtime task into the runtime engine and returns a boolean indicating whether the enqueue succeeded; 5. the runtime engine continues to repost tasks until the state object reaches its terminal condition; errors occurring in async execution invoke a client-provided onError callback (if supplied) and are captured on the state handle; 6. all state objects created by the engine are executed in serialized order through `Dmn_Runtime_Manager` (subject to the engine's serialization policy); -7. client may call `statehandle->wait()` or use the returned future to block or asynchronously observe completion. +7. client may call `statehandle->wait()` or use the returned shared_future to block or asynchronously observe completion. ## 2. Design Objective @@ -30,7 +30,7 @@ The existing `dmn-state` component is synchronous and client-driven. It calls `r - the state object remains a state machine definition and execution state, - the runtime engine owns when the state steps are executed, - state execution is serialized within the runtime’s async context, -- the client receives an async completion signal via `wait()` or a future rather than manually stepping the machine. +- the client receives an async completion signal via `wait()` or a shared_future rather than manually stepping the machine. This makes the feature a natural fit for handshake flows, retries, startup/teardown workflows, protocol states, network state transitions, and any runtime pipeline that must share the same scheduling semantics as other runtime jobs. @@ -43,7 +43,7 @@ This makes the feature a natural fit for handshake flows, retries, startup/teard - state registration API for user-defined states - `run()` scheduling through the runtime engine with onError callback forwarding to `dmn-runtime` semantics - serialized execution of all runtime state objects (engine-global by default) -- `wait()` completion API for async execution with optional timeout and a future-based async alternative +- `wait()` completion API for async execution with optional timeout and a shared_future-based async alternative - shutdown, error, cancellation, and terminal-state handling - tests for startup, normal completion, errors, cancellation, and lifetime edge cases @@ -57,32 +57,13 @@ This makes the feature a natural fit for handshake flows, retries, startup/teard ## 4. Architectural Context -### Existing Runtime Architecture +Refer to `include/dmn-runtime.hpp`, `include/dmn-runtime-task.hpp`, and `include/dmn-state.hpp` for the runtime and state primitives that will be reused. -The current runtime API is singleton-based and uses: +Key runtime types and semantics reused: -- `Dmn_Runtime_Manager::createInstance()` for process-wide lifecycle -- `addJob()` / `addTimedJob()` for queued work -- `enterMainLoop()` / `exitMainLoop()` for the runtime loop -- `Dmn_Runtime_Job` with priority and coroutine execution support -- `Dmn_Runtime_Task` as the coroutine wrapper for job execution - -The runtime currently serializes work via its async execution model, and this is the correct execution context for the new state engine. - -### Existing State Machine Architecture - -`Dmn_State` has the following current semantics: - -- stores a vector of state functors -- uses `m_next` to drive execution -- supports `setStateFnc()`, `setNext()`, `setEnd()`, and `runNext()` -- defines init/finalize hooks -- uses `runNext()` directly in the caller context -- has no async lifecycle, no wait, and no runtime ownership - -### Feature Intent - -The runtime state engine is not a replacement for `Dmn_State`; it is a runtime-managed execution shell around it. Each state object created by the engine is still a `Dmn_State`, but with additional runtime lifetime controls and completion synchronization. +- `Dmn_Runtime_Job::Priority` and `Dmn_Runtime_Manager::addJob()` / `addTimedJob()` +- `Dmn_Runtime_Job::OnErrorFncType` (signature: `std::function`) — the onError callback type used by runtime jobs +- `Dmn_Runtime_Manager` owns the singleton async thread and provides `isRunInAsyncThread()` to detect runtime-thread context ## 5. Functional Requirements @@ -102,10 +83,10 @@ Clients must be able to obtain a managed handle to a state object from the runti Ownership model (required): -- `createState()` MUST return a managed handle type: `std::shared_ptr` (or an alias) following the existing dmn pattern used by other components (for example, `dmn-dmesg` and other resource holders in the codebase). -- The engine must retain a `std::shared_ptr` to the state object while it is queued or running. This guarantees the object remains alive until it reaches a terminal state even if the client drops its handle. +- `createState()` MUST return a managed handle type: `std::shared_ptr` (alias `DmnRuntimeStatePtr`) following the existing dmn pattern used by other components (for example, `dmn-dmesg`). +- The engine MUST retain a `std::shared_ptr` to the state object while it is queued or running. This guarantees the object remains alive until it reaches a terminal state even if the client drops its handle. - When the object becomes terminal (completed/failed/cancelled), the engine releases its internal shared_ptr; any remaining client-held shared_ptr keeps the object alive until all references are dropped. -- The spec must include sample usage showing how clients keep a handle, but clients may also intentionally let the engine own a state for fire-and-forget semantics. +- Clients may intentionally drop their handle to rely on engine ownership for fire-and-forget semantics. The resulting object must: @@ -126,17 +107,25 @@ The engine must support: ### FR-4: `run()` dispatches work to runtime and error callback forwarding -The runtime state handle must expose a public `run()` method. In addition, `run()` MUST accept an optional onError callback that matches the `dmn-runtime` onError callback signature so callers can receive asynchronous error notifications. +The runtime state handle must expose a public `run()` method. In addition, `run()` MUST accept optional parameters for priority, delay (timed variant), and an onError callback that matches the runtime's `Dmn_Runtime_Job::OnErrorFncType` signature. Behavior: -- `run()` may be called from any client thread -- `run()` must schedule asynchronous work on the singleton `Dmn_Runtime_Manager` async thread -- `run()` returns `true` if the state was successfully queued (or started) and `false` if the enqueue failed (invalid state, already terminal, or internal error) -- an optional `onError` callback provided to `run()` is forwarded to the underlying `dmn-runtime` job so that asynchronous runtime failures invoke the client callback when the runtime job reports an error -- the runtime work must execute exactly one next state step of the state object per posted job -- after the step executes, the engine must post another runtime task to run the next state, continuing until no more states remain -- `run()` must not directly execute state logic in the caller thread +- `run()` may be called from any client thread EXCEPT the runtime async thread (calls from the runtime thread are disallowed — see `run()` thread policy below). +- `run()` must schedule asynchronous work on the singleton `Dmn_Runtime_Manager` async thread by calling `addJob()` or `addTimedJob()` as appropriate. +- `run()` returns `true` if the state was successfully queued (or started) and `false` if the enqueue failed (invalid state, already terminal, or internal error). +- `run()` is a one-shot operation for each state handle: subsequent `run()` calls after the first successful enqueue MUST be no-ops and MUST return `false`. +- The optional `onError` callback provided to `run()` MUST be forwarded to the underlying `dmn-runtime` job so that asynchronous runtime failures invoke the client callback when the runtime job reports an error. Use `Dmn_Runtime_Job::OnErrorFncType` as the canonical type. +- The runtime work must execute exactly one next state step of the state object per posted job; after the step executes, the engine reposts another job (or finalizes) until terminal. +- `run()` MUST NOT execute state logic synchronously in the caller thread. + +Priority and timed variants: + +- `run()` must accept an optional `Dmn_Runtime_Job::Priority` parameter and an optional timeout/delay parameter so callers can control priority and optionally schedule timed runs. The engine will map these directly to `Dmn_Runtime_Manager::addJob()` (immediate) or `addTimedJob()` (timed). + +Thread policy for `run()` and `wait()`: + +- If `run()` or `wait()` is called from inside the runtime async thread (detected via `Dmn_Runtime_Manager::isRunInAsyncThread()`), the implementation MUST assert in debug builds and throw `std::runtime_error` (or similar) in non-debug builds. This prevents deadlocks and enforces the rule that runtime-internal callbacks should not block the runtime. ### FR-5: Serialized execution across all state objects @@ -156,10 +145,10 @@ Each runtime state object must support a `wait()` method and provide safer alter Behavior: - `wait()` blocks until the state object reaches its terminal state -- `wait()` MUST support an overload `wait(std::chrono::milliseconds timeout)` (or templated duration) that returns a boolean indicating whether the wait observed terminal completion before the timeout -- `wait()` MUST NOT be called from the runtime async thread; calling from the runtime thread is undefined — implementations SHOULD assert or return immediately with an error when detected -- in addition to blocking wait(), the object MUST provide a `std::future getFuture()` (or equivalent) so callers can use non-blocking or async wait patterns on other threads or coroutines -- `wait()` and future completion must be signalled using a condition variable / promise and must handle spurious wakeups correctly +- `wait()` MUST support an overload `wait_for(std::chrono::duration<...> timeout)` that returns a boolean indicating whether the wait observed terminal completion before the timeout +- `wait()` MUST NOT be called from the runtime async thread; calling from the runtime thread is undefined — implementations MUST assert (debug) and throw (release) when detected +- The object MUST provide a `std::shared_future getFuture()` so callers can use non-blocking or async wait patterns. The shared_future MUST be available immediately after state creation (before run()) and MUST become ready on terminal state regardless of whether run() was ever called. +- Multiple threads/callers MAY wait concurrently on the same shared_future or call `wait()` concurrently; the implementation must support multiple waiters. ### FR-7: Terminal state and completion semantics @@ -198,8 +187,8 @@ If a state callback throws while running inside the runtime-managed async thread - capture the exception - set the state object to failed terminal state -- notify any waiting client via `wait()` or future -- invoke the onError callback supplied by the client (if any) with the runtime's error details +- notify any waiting client via `wait()` or shared_future +- invoke the onError callback supplied to `run()` (if any) with the runtime's error details using `Dmn_Runtime_Job::OnErrorFncType` - avoid corrupting the runtime scheduler internal state ## 6. Non-Functional Requirements @@ -222,11 +211,11 @@ The engine MUST hold a `std::shared_ptr` to any queued/running state object unti ### NFR-5: Thread safety for wait semantics -`wait()` must be implemented using synchronization primitives appropriate for cross-thread signaling (`std::condition_variable`, flag, or equivalent) and must not busy-spin. +`wait()` must be implemented using synchronization primitives appropriate for cross-thread signaling (`std::condition_variable`, flag, or equivalent) and must not busy-spin. Use of a `std::shared_future` simplifies multi-waiter semantics. ## 7. Proposed API Shape -This API is proposed to match the existing library naming and runtime conventions while reflecting the ownership and error/cancel semantics requested. +This API is proposed to match the existing library naming and runtime conventions while reflecting the ownership, error/cancel, priority/timed semantics and thread-safety rules requested. ```cpp namespace dmn { @@ -251,7 +240,7 @@ public: class Dmn_Runtime_State : public Dmn_State { public: using FncType = std::function; - using OnErrorFnc = std::function; // forwarded to runtime's onError + using OnErrorFnc = Dmn_Runtime_Job::OnErrorFncType; // std::function explicit Dmn_Runtime_State(std::string_view name); @@ -264,21 +253,25 @@ public: // lifecycle APIs // run: returns true when the enqueue succeeded; false on failure (already terminal, invalid, runtime busy) // optional onError is forwarded to the runtime job so callers get notified of async job failure. - bool run(OnErrorFnc onError = nullptr); + // priority and timed overloads are supported and map to addJob()/addTimedJob(). + bool run(Dmn_Runtime_Job::Priority priority = Dmn_Runtime_Job::Priority::kMedium, + const std::chrono::steady_clock::duration &delay = std::chrono::steady_clock::duration::zero(), + OnErrorFnc onError = {}); // cancel is cooperative and idempotent. If called before a step runs, the running task will // observe the cancelled flag, call setEnd() and transition to terminal state instead of executing further steps. void cancel(); - // wait blocks until terminal state. wait(timeout) returns true if it observed terminal state before timeout. + // wait blocks until terminal state. wait_for(timeout) returns true if it observed terminal state before timeout. void wait(); template bool wait_for(const std::chrono::duration &timeout); - // future-based async alternative - std::future getFuture(); + // future-based async alternative (shared_future supports multiple waiters and callers). + std::shared_future getFuture(); // introspection + // isRunning() indicates the handle has been queued or is actively running inside the engine bool isRunning() const; bool isCompleted() const; bool isFailed() const; @@ -294,14 +287,16 @@ private: // synchronization / state std::mutex m_waitMutex; std::condition_variable m_waitCv; - std::promise m_completionPromise; // getFuture() returns m_completionPromise.get_future() - std::atomic_bool m_running{false}; - std::atomic_bool m_completed{false}; - std::atomic_bool m_failed{false}; - std::atomic_bool m_cancelled{false}; - std::atomic_bool m_queued{false}; - std::atomic_bool m_waiting{false}; + // promise/future pair: keep a shared_future so multiple waiters are supported + std::promise m_completionPromise; + std::shared_future m_completionSharedFuture; // initialized from m_completionPromise.get_future().share() + + std::atomic_bool m_running{false}; // set when a runtime task is active + std::atomic_bool m_completed{false}; // terminal + std::atomic_bool m_failed{false}; // terminal + std::atomic_bool m_cancelled{false}; // set by cancel() + std::atomic_bool m_queued{false}; // set before enqueue to avoid duplicates // captured failure for diagnostics std::mutex m_failureMutex; @@ -314,10 +309,12 @@ private: ### API Notes - `Dmn_Runtime_State_Engine::createState()` returns a `std::shared_ptr` handle. The engine retains a shared_ptr while the state is queued/running to ensure safe lifetime. -- `Dmn_Runtime_State::run(OnErrorFnc)` returns a boolean: true on successful enqueue, false if enqueue failed (e.g., already terminal or invalid state). -- `run()` accepts an optional onError callback that is forwarded to the `dmn-runtime` job plumbing; if the runtime reports an error, the callback will be invoked with the exception_ptr. -- `cancel()` is cooperative: it sets a cancelled flag. The runtime job, before calling `runNext()`, must check isCancelled() and call `setEnd()` if the state has been cancelled so that the state finalizes without executing further steps. -- `wait()` supports a timeout variant; a future is also available for non-blocking waits. +- `Dmn_Runtime_State::run(priority, delay, onError)` returns a boolean: true on successful enqueue, false if enqueue failed (e.g., already terminal or invalid state). If `delay` is non-zero, the engine must use `addTimedJob()`; otherwise `addJob()` is used. +- `run()` accepts an optional onError callback that uses the runtime's `Dmn_Runtime_Job::OnErrorFncType` signature and is forwarded to the runtime job. +- `run()` is one-shot: a successful `run()` prevents subsequent `run()` calls from enqueueing again; such subsequent calls return `false` (no-op). This avoids duplicate enqueues across threads. +- `cancel()` is cooperative: it sets a cancelled flag. The runtime job, before calling `runNext()`, must check `isCancelled()` and call `setEnd()` if the state has been cancelled so that the state finalizes without executing further steps. +- `wait()` supports a timeout variant and `getFuture()` returns a `std::shared_future` that can be used by multiple waiters. The shared_future is valid immediately after createState() is called and resolves when the state reaches a terminal condition. +- Calls to `run()` or `wait()` from inside the runtime async thread must assert/throw. Use `Dmn_Runtime_Manager::isRunInAsyncThread()` to detect and enforce this. ## 8. Execution Model @@ -328,23 +325,23 @@ A runtime state object has the following lifecycle: 1. Created by `Dmn_Runtime_State_Engine::createState()` and returned as a shared_ptr handle 2. Configured by setting state functors (`setStateFnc`, etc.) 3. Idle before `run()` is called -4. Queued for runtime execution after `run()` (engine retains shared_ptr) +4. Queued for runtime execution after `run()` (engine retains shared_ptr and sets `m_queued` atomically) 5. Running inside runtime async thread 6. Finalized once terminal condition reached (engine releases internal shared_ptr) -7. Client may call `wait()` at any time after submission or use the future returned by `getFuture()` +7. Client may call `wait()` at any time after submission or use the shared_future returned by `getFuture()` -### 8.2 `run()` exact semantics +### 8.2 `run()` exact semantics and atomic queued flag -When `statehandle->run(onError)` is called: +When `statehandle->run(priority, delay, onError)` is called: 1. the state object must be validated for legal execution -2. if not already running/completed, it is marked queued and the engine stores an internal shared_ptr -3. a runtime job is scheduled in the runtime engine; the job is created with the supplied onError callback forwarded to the runtime -4. the scheduled job executes exactly one next state step using the state object -5. before calling `runNext()`, the runtime job checks the cancel flag; if cancelled, it must call `setEnd()` and finalize instead of running the step -6. after the state step finishes, the runtime engine checks whether another state remains -7. if another state exists and not cancelled, the engine posts a new runtime job for the next step -8. if no state remains, the object transitions to completed terminal state and the engine notifies waiters (condition variable and promise) +2. the implementation MUST atomically set the `m_queued` flag (e.g., using compare_exchange) to avoid races where multiple threads try to enqueue simultaneously; only the thread that successfully sets `m_queued` proceeds to request the engine to retain an internal shared_ptr and submit the runtime job +3. if `m_queued` was already true or the object is terminal, `run()` returns false (no-op) +4. the engine stores a shared_ptr to the object (ensuring lifetime) and schedules a runtime job via `addJob()` or `addTimedJob()` depending on `delay` +5. the scheduled job executes exactly one next state step using the state object +6. before calling `runNext()`, the runtime job MUST check the cancel flag; if cancelled, it MUST call `setEnd()` and finalize instead of running the step +7. after the state step finishes, the runtime engine checks whether another state remains and reposts a new runtime job if appropriate +8. if no state remains, the object transitions to completed terminal state, the engine notifies waiters (set promise) and releases its internal shared_ptr 9. if a state callback throws, the exception is captured, the object transitions to failed, waiters are notified, and the optional onError callback is invoked This loop continues until no more states to run. The runtime engine is responsible for re-posting tasks while the object remains active. @@ -370,15 +367,15 @@ The runtime layer adds async ownership, completion signaling, cancellation token ### 9.2 Cancellation contract - `cancel()` sets the cancellation flag and is safe to call from any thread. -- The runtime job must observe `isCancelled()` before executing `runNext()` and call `setEnd()` to force deterministic finalization. -- State functors may query `isCancelled()` if they want to implement cooperative cancellation. +- The runtime job MUST observe `isCancelled()` before executing `runNext()` and call `setEnd()` to force deterministic finalization. +- State functors MAY query `isCancelled()` if they want to implement cooperative cancellation. ### 9.3 `wait()` behavior and deadlock avoidance - `wait()` blocks until the object is terminal. -- Implementations must detect (or document) that `wait()` MUST NOT be called from the runtime async thread. Preferably, `wait()` asserts or returns an error when invoked from runtime thread context. +- Implementations MUST detect calls from the runtime async thread and assert/throw as described. - `wait_for(timeout)` returns a bool indicating whether the wait observed terminal completion before the timeout expired. -- `getFuture()` provides an async, non-blocking alternative which is safe to use from the runtime thread if the runtime supports async continuation semantics (otherwise the caller should still avoid awaiting it on the runtime thread). +- `getFuture()` returns a `std::shared_future` available immediately after creation and resolves on terminal state. ## 10. Detailed Behavior and Edge Cases @@ -392,10 +389,8 @@ A runtime state object must not be run multiple times simultaneously. Behavior: -- if `run()` is called while already queued or running, it MUST return false -- repeated `run()` calls after completion MUST return false - -This explicit boolean return covers the recommended behavior and aligns with the request. +- The first successful `run()` enqueues the object and returns true. +- Subsequent `run()` calls (concurrent or later) MUST return false (no-op). This avoids duplicate enqueues and is thread-safe due to the atomic `m_queued` guard. ### 10.3 Finalized or canceled states @@ -407,8 +402,8 @@ If a state callback throws while inside the runtime engine: - capture the exception in `std::exception_ptr` stored on the object - transition object to failed terminal state -- set the promise / notify the condition variable so waiters/future observers are notified -- invoke the optional onError callback provided to `run()` with the captured exception +- set the completion promise and notify any shared_future waiters +- invoke the optional onError callback provided to `run()` with the captured exception (using `Dmn_Runtime_Job::OnErrorFncType`) - ensure runtime queue remains healthy ### 10.5 Shutdown race and modes @@ -442,17 +437,19 @@ Provide clear priority mapping between engine jobs and other runtime jobs. The e Additional edge-case tests (required): - `destructor_while_queued`: verify engine-held shared_ptr keeps object alive while queued and that finalization occurs correctly when the client drops its handle -- `wait_from_runtime_thread_detected`: ensure wait() from runtime thread either asserts or returns error +- `wait_from_runtime_thread_detected`: ensure wait() from runtime thread asserts/throws - `cancel_before_run_or_queued`: calling cancel() before a queued step prevents runNext() and finalizes the state - `cancel_during_queued_execution`: cancellation while queued leads the runtime job to call setEnd() rather than executing further steps - `run_onerror_callback_invoked`: ensure onError passed to run() is invoked when the runtime job fails - `shutdown_graceful_vs_immediate`: test both shutdown modes and their effects on queued and running states +- `getFuture_shared_waiters_before_run`: verify multiple waiters using getFuture() before run() all get signaled on terminal condition +- `run_priority_and_timed_variant`: verify run(priority, delay) maps to addJob/addTimedJob and honors priority ordering ### Integration Tests - run multiple runtime state objects in the same runtime engine - verify that state steps are serialized in posting order -- verify wait() and future complete after sequence completion +- verify wait() and shared_future complete after sequence completion - verify runtime jobs remain valid when the state object reaches terminal condition - stress test many queued states to measure the effect of global serialization @@ -469,9 +466,9 @@ The feature is accepted when all of the following are true: - clients can create runtime-managed state objects from a singleton engine using a shared_ptr handle - state objects subclass `Dmn_State` and retain base state semantics -- `statehandle->run(onError)` schedules work into the runtime engine and returns true/false to indicate success +- `statehandle->run(priority, delay, onError)` schedules work into the runtime engine and returns true/false to indicate success - state execution is serialized through the runtime engine by default -- `statehandle->wait()` and `wait_for()` block until runtime completion or terminal failure and `getFuture()` is available for async waiting +- `statehandle->wait()` and `wait_for()` block until runtime completion or terminal failure and `getFuture()` is available for async waiting (shared_future) - `cancel()` cooperatively finalizes execution and prevents future steps; cancel semantics are documented and tested - exceptions and cancellation leave the runtime in a valid state and optional onError callbacks are invoked - documentation, examples and tests exist for typical runtime state workflow usage and lifetime edge cases @@ -482,7 +479,7 @@ The feature is accepted when all of the following are true: Mitigation: `run()` must schedule a single runtime task per state step and stop when the terminal condition is reached. No unbounded recursive scheduling loop is allowed. Atomic queued flag prevents duplicate enqueues. ### Risk: wait deadlock -Mitigation: do not call `wait()` from inside the runtime async thread. Document and assert this in debug builds; prefer future-based wait from runtime thread contexts. +Mitigation: do not call `wait()` from inside the runtime async thread. Document and assert this in debug builds; throw in release builds. Prefer future-based wait from runtime thread contexts. ### Risk: queued state object lifetime issues Mitigation: engine must hold a `std::shared_ptr` while queued. Returning a shared_ptr to clients and holding an internal shared_ptr mirrors the existing dmn pattern used in other subsystems (see `dmn-dmesg`). @@ -504,7 +501,7 @@ The runtime state engine should primarily add: - state object lifecycle tracking using shared_ptr handles - queueing and serialization logic -- `wait()` synchronization (condition_variable + promise/future) +- `wait()` synchronization (condition_variable + promise/shared_future) - terminal-state finalization and onError callback forwarding - cooperative cancel() semantics @@ -522,8 +519,9 @@ s->setNext(); s->setStateFnc([](dmn::Dmn_Runtime_State &st){ /* step 1 */ }, 1); s->setEnd(); -// run with onError callback -bool ok = s->run([](std::exception_ptr ep){ /* log or inspect error */ }); +// run with onError callback, medium priority, immediate +bool ok = s->run(Dmn_Runtime_Job::Priority::kMedium, std::chrono::steady_clock::duration::zero(), + [](std::exception_ptr &ep){ /* log or inspect error */ }); if (!ok) { /* handle enqueue failure */ } // wait (blocking) @@ -539,7 +537,7 @@ fut.wait(); The feature is complete when: - the singleton runtime state engine is designed and documented with shared_ptr handle ownership -- the runtime state object class is specified and matches the required async semantics (run returning bool, onError forwarding, cancel, wait/timeout/future) +- the runtime state object class is specified and matches the required async semantics (run returning bool, onError forwarding, cancel, wait/timeout/shared_future) - `run()` and `wait()` behavior are documented and tested - serialized execution through `Dmn_Runtime_Manager` is verified - shutdown, failure, and cancellation semantics are validated @@ -548,8 +546,8 @@ The feature is complete when: ## 17. Recommended Milestones 1. Create the engine singleton and state object base model (shared_ptr handle) -2. Add runtime scheduling and serialized execution loop with onError forwarding -3. Add completion/failure/cancel tracking, wait(timeout), and future support +2. Add runtime scheduling and serialized execution loop with onError forwarding, priority and timed variants +3. Add completion/failure/cancel tracking, wait(timeout), and shared_future support 4. Add tests for execution order, completion, failures, destructor-while-queued, and shutdown modes 5. Validate the runtime integration with `Dmn_Runtime_Manager` From c22ffc5335d6f0ab32073f070147edb4aed9ff8b Mon Sep 17 00:00:00 2001 From: Chee Bin HOH Date: Sun, 30 Aug 2026 20:59:33 -0400 Subject: [PATCH 4/7] feat(runtime-state): add public header for runtime state engine API --- include/dmn-runtime-state.hpp | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 include/dmn-runtime-state.hpp diff --git a/include/dmn-runtime-state.hpp b/include/dmn-runtime-state.hpp new file mode 100644 index 0000000..44b3396 --- /dev/null +++ b/include/dmn-runtime-state.hpp @@ -0,0 +1,99 @@ +# Runtime State Engine - Public Header + +#ifndef DMN_RUNTIME_STATE_HPP_ +#define DMN_RUNTIME_STATE_HPP_ + +#include "dmn-runtime.hpp" +#include "dmn-state.hpp" + +#include +#include +#include +#include +#include + +namespace dmn { + +class Dmn_Runtime_State_Engine; + +class Dmn_Runtime_State : public Dmn_State { +public: + using FncType = std::function; + using OnErrorFnc = Dmn_Runtime_Job::OnErrorFncType; // std::function + + explicit Dmn_Runtime_State(std::string_view name); + virtual ~Dmn_Runtime_State() noexcept; + + // State configuration (inherited semantics from Dmn_State) + void setStateFnc(FncType fnc, int index = 0); + void setNext(int index); + void setNext(); + void setEnd(); + + // Lifecycle API + // Enqueue this state for runtime execution. Returns true if enqueue succeeded. + // Subsequent successful calls are no-ops and return false. + // Priority maps to Dmn_Runtime_Job::Priority; delay of zero means immediate addJob(). + bool run(Dmn_Runtime_Job::Priority priority = Dmn_Runtime_Job::Priority::kMedium, + const std::chrono::steady_clock::duration &delay = std::chrono::steady_clock::duration::zero(), + OnErrorFnc onError = {}); + + // Cooperative cancel. Safe to call from any thread. Idempotent. + void cancel(); + + // Blocking wait until terminal state. Throws if called from runtime thread. + void wait(); + + // Timed wait; returns true if terminal observed before timeout. + template + bool wait_for(const std::chrono::duration &timeout); + + // Async alternative: a shared_future that becomes ready when the state is terminal. + // The shared_future is available immediately after createState() and supports multiple waiters. + std::shared_future getFuture(); + + // Introspection + bool isRunning() const; // queued or actively running + bool isCompleted() const; // terminal success + bool isFailed() const; // terminal failure + bool isCancelled() const; // cancelled flag set + +protected: + // Hooks to allow derived implementations to react to lifecycle events. + virtual void onStarted(); + virtual void onCompleted(); + virtual void onFailed(std::exception_ptr ep); + virtual void onCancelled(); + +private: + // Implementation details (opaque to public header) should be placed in the + // corresponding .cpp. These members are intentionally documented in the + // spec but left to the implementation to manage. + + // Note: keep the header minimal to avoid exposing internal synchronization + // primitives; real implementation may add mutexes, atomic flags and + // promise/shared_future wiring. +}; + +class Dmn_Runtime_State_Engine : public Dmn_Singleton { +public: + using DmnRuntimeStatePtr = std::shared_ptr; + + static auto createInstance() -> Dmn_Runtime_State_Engine &; + + // Create a runtime-managed state object handle. + // The returned shared_ptr may be kept by the client; the engine will also + // retain a shared_ptr while the state is queued or running. + DmnRuntimeStatePtr createState(std::string_view name); + + // Optional engine-level configuration and shutdown APIs are implemented + // in the source file as needed. + +protected: + Dmn_Runtime_State_Engine(); + virtual ~Dmn_Runtime_State_Engine() noexcept; +}; + +} // namespace dmn + +#endif // DMN_RUNTIME_STATE_HPP_ From 39b7b32ee4dab6c42a0917b586d6077b7940e5ea Mon Sep 17 00:00:00 2001 From: Chee Bin HOH Date: Sun, 30 Aug 2026 21:01:50 -0400 Subject: [PATCH 5/7] docs: add TDD implementation plan for runtime-state-engine --- docs/IMPLEMENTATION_PLAN_runtime-state.md | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/IMPLEMENTATION_PLAN_runtime-state.md diff --git a/docs/IMPLEMENTATION_PLAN_runtime-state.md b/docs/IMPLEMENTATION_PLAN_runtime-state.md new file mode 100644 index 0000000..2d9053d --- /dev/null +++ b/docs/IMPLEMENTATION_PLAN_runtime-state.md @@ -0,0 +1,94 @@ +# Runtime State Engine Implementation Plan + +This document is a step-by-step TDD-first implementation plan for the runtime state engine feature. It maps spec items to phased implementation tasks. Follow the phases sequentially and run unit tests after each phase. + +Repository layout assumptions +- include/: public headers +- src/: library implementation +- test/: unit tests +- CMake macros like ADD_TEST_EXECUTABLE are available and used for registering test executables. + +Phase 0: Already completed (spec & header) +- `docs/specs/runtime-state-machine-spec.md` updated with ownership, run(onError), cancel, wait(timeout)/shared_future, priority/timed variants and tests. +- Public header `include/dmn-runtime-state.hpp` added declaring the API. + +Phase 1: Add test skeletons and minimal stubs to compile +- Add test: `test/dmn-test-runtime-state.cpp` (skeleton) +- Add CMake: include `dmn-test-runtime-state` in `test/CMakeLists.txt` +- Add Phase-1 stub: `src/dmn-runtime-state.cpp` implementing minimal methods (compile-friendly): + - Dmn_Runtime_State ctor/dtor + - run() returns false + - cancel() is a no-op + - wait() returns immediately + - getFuture() returns ready shared_future + +Verify: +- cmake -B build -DCMAKE_BUILD_TYPE=Debug +- cmake --build build +- ctest --test-dir build --output-on-failure + +Phase 2: Basic runtime enqueue & single-step execution +- Implement run() to atomically set queued flag and enqueue a Dmn_Runtime_Job to `Dmn_Runtime_Manager::addJob()` (immediate) or `addTimedJob()` (delay). Use Dmn_Runtime_Job::Priority. +- Engine will retain an internal shared_ptr to the state while queued; store it in `std::unordered_map> m_pendingStates;` keyed by pointer or generated id. +- The job's m_fnc must create a coroutine task (TaskFncType) that: + - locks a weak_ptr to the state + - checks isCancelled(); if set, call setEnd() and finalize + - calls runNext() once (in try/catch) + - if still active, repost by calling addJob() again + - if terminal, set completion promise and erase engine internal shared_ptr +- Wire `m_completionPromise` and `m_completionSharedFuture` so getFuture() returns `m_completionSharedFuture`. + +Tests expected to pass after this phase: +- RuntimeState_BasicFlow +- RuntimeState_GetFuture_PreRun_MultipleWaiters (shared_future works) + +Phase 3: Exception capture and onError forwarding +- Wrap runNext() call in try/catch inside the runtime job. +- On exception: + - store `std::current_exception()` in the state + - set `m_failed` flag + - set `m_completionPromise` (set_exception or set_value, but capture ep for diagnostics) + - invoke onError callback forwarded via job.m_onErrorFnc +- Update run() to forward client-provided onError into the runtime job creation + +Tests expected to pass: +- RuntimeState_RunOnErrorCallback +- state_exception_marks_failed + +Phase 4: Cancel semantics & destructor-while-queued +- Implement cancel() to set atomic m_cancelled. +- Ensure runtime job checks m_cancelled before runNext() and calls setEnd() if true. +- Ensure engine internal shared_ptr map is created when run() enqueues; it must hold the shared_ptr until terminal. +- Implement destructor_while_queued test to validate engine holds state alive. + +Phase 5: Priority/timed behavior and fairness +- Implement run(priority, delay) mapping to addJob/addTimedJob. If delay > 0 use addTimedJob. +- Add tests verifying that priority ordering affects execution order. +- Consider fairness: ensure engine uses runtime priority queues and doesn't monopolize the runtime. + +Phase 6: Runtime-thread detection & runtime safety +- Detect runtime context using `Dmn_Runtime_Manager::isRunInAsyncThread()`. +- In run() and wait(), if called on runtime thread: assert in debug builds and throw `std::runtime_error` in release builds. +- Implement safe unit/integration tests for detection (special harness that posts a runtime job which attempts to call wait() and expects an exception). + +Phase 7: Polish, stress tests, documentation +- Add stress tests, runtime integration tests, and code comments. +- Document known limitations and example usage. + +Developer checklist for each commit +- Keep commits small and focused. +- Run `cmake -B build -DCMAKE_BUILD_TYPE=Debug` and `cmake --build build` locally before pushing. +- Run `ctest --test-dir build --output-on-failure` after each phase and fix failing tests or update the Phase implementation accordingly. + +Notes and gotchas +- Use weak_ptr in runtime job to avoid reference cycles; the engine's internal shared_ptr keeps the object alive while queued. +- Use atomic compare_exchange to set queued flag and avoid races for multiple-concurrent run() calls. +- Use std::shared_future to support multiple waiters. +- Be careful to release engine internal shared_ptr only after the completion promise is fulfilled and after finalization is complete. +- Use runtime's addJob/addTimedJob APIs and forward onError callback using Dmn_Runtime_Job::OnErrorFncType. + +Example commands +- Configure & build: cmake -B build -DCMAKE_BUILD_TYPE=Debug +- Build: cmake --build build -j$(nproc) +- Run tests: ctest --test-dir build --output-on-failure + From 692d61552b63c850c35b2373e4a5a1207a795cbc Mon Sep 17 00:00:00 2001 From: Chee Bin HOH Date: Sun, 30 Aug 2026 21:04:33 -0400 Subject: [PATCH 6/7] docs: add header copyright, brief, author and doxygen comments for runtime-state API --- include/dmn-runtime-state.hpp | 185 ++++++++++++++++++++++++++++------ 1 file changed, 153 insertions(+), 32 deletions(-) diff --git a/include/dmn-runtime-state.hpp b/include/dmn-runtime-state.hpp index 44b3396..b8c361c 100644 --- a/include/dmn-runtime-state.hpp +++ b/include/dmn-runtime-state.hpp @@ -1,4 +1,19 @@ -# Runtime State Engine - Public Header +/** + * Copyright © 2026 Chee Bin HOH. All rights reserved. + * + * @file dmn-runtime-state.hpp + * @brief Public API for the Runtime State Engine: a runtime-owned state + * execution engine that composes the existing dmn-runtime scheduler + * with the dmn-state finite-state helper. + * + * @author Chee Bin HOH + * @date 2026-08-31 + * + * This header declares the public types used by clients to create and + * manage runtime-managed state machine instances. Implementation details + * are intentionally omitted from the header; refer to the implementation + * (.cpp) and specification documents for full behavior. + */ #ifndef DMN_RUNTIME_STATE_HPP_ #define DMN_RUNTIME_STATE_HPP_ @@ -14,81 +29,187 @@ namespace dmn { -class Dmn_Runtime_State_Engine; - +/** + * @class Dmn_Runtime_State + * @brief A runtime-managed state machine instance. + * + * Dmn_Runtime_State subclasses Dmn_State and adds asynchronous runtime + * ownership semantics: a client obtains a shared_ptr handle from the + * engine, configures state functors using the same Dmn_State API, then + * calls run() to schedule execution on the global runtime thread. + * + * Important behavior (summary): + * - createState() returns std::shared_ptr — the engine + * also holds a shared_ptr while the state is queued or running to + * guarantee lifetime. + * - run(priority, delay, onError) enqueues the state for execution; it + * returns true when enqueue succeeded and false otherwise. The optional + * onError callback uses Dmn_Runtime_Job::OnErrorFncType and is forwarded + * into the runtime job. + * - cancel() is cooperative: runtime tasks must observe isCancelled() and + * call setEnd() before runNext() when cancelled. + * - wait(), wait_for(), and getFuture() provide blocking and non-blocking + * completion waiting. getFuture() returns a std::shared_future so + * multiple waiters are supported. + * - Calling run() or wait() from inside the runtime async thread is + * disallowed: implementations MUST assert/throw when detected. + */ class Dmn_Runtime_State : public Dmn_State { public: using FncType = std::function; using OnErrorFnc = Dmn_Runtime_Job::OnErrorFncType; // std::function + /** + * @brief Construct a runtime-managed state object with a human-readable name. + * @param name Human-readable name used for diagnostics. + */ explicit Dmn_Runtime_State(std::string_view name); + + /** + * @brief Virtual destructor. Implementation should ensure safe teardown. + */ virtual ~Dmn_Runtime_State() noexcept; - // State configuration (inherited semantics from Dmn_State) + /* State configuration (inherited semantics from Dmn_State) */ + + /** + * @brief Set the functor for a state slot. + * @param fnc The functor to be called for the state step. + * @param index 1-based index for the state slot (0 uses default behavior). + * + * This method preserves Dmn_State semantics and allows clients to define + * the machine steps. + */ void setStateFnc(FncType fnc, int index = 0); + + /** + * @brief Set the next user state by index (1-based). + * @param index 1..m_states.size() selects the next state. + */ void setNext(int index); + + /** + * @brief Convenience: set the next state to the sequential next slot. + */ void setNext(); + + /** + * @brief Mark the machine to finalize after the current step. + */ void setEnd(); - // Lifecycle API - // Enqueue this state for runtime execution. Returns true if enqueue succeeded. - // Subsequent successful calls are no-ops and return false. - // Priority maps to Dmn_Runtime_Job::Priority; delay of zero means immediate addJob(). + /* Lifecycle API */ + + /** + * @brief Schedule this state handle for runtime execution. + * + * @param priority Job priority to use when enqueuing (maps to Dmn_Runtime_Job::Priority). + * @param delay If non-zero, the job is scheduled via addTimedJob() after this delay. + * @param onError Optional error callback forwarded to the runtime job. The + * type matches Dmn_Runtime_Job::OnErrorFncType. + * @return true if the state was successfully queued; false if enqueue + * failed (already terminal, duplicate run, invalid configuration). + * + * Notes: + * - run() is one-shot for a given handle: the first successful call enqueues + * the state; subsequent calls return false. + * - Calling run() from inside the runtime async thread is disallowed and + * will assert/throw. + */ bool run(Dmn_Runtime_Job::Priority priority = Dmn_Runtime_Job::Priority::kMedium, const std::chrono::steady_clock::duration &delay = std::chrono::steady_clock::duration::zero(), OnErrorFnc onError = {}); - // Cooperative cancel. Safe to call from any thread. Idempotent. + /** + * @brief Request cooperative cancellation of this state. + * + * The cancellation is idempotent and thread-safe. It does NOT preempt a + * currently-running functor unless that functor explicitly observes the + * cancel flag via isCancelled(). The runtime job must check isCancelled() + * and call setEnd() prior to invoking runNext() if cancellation is set. + */ void cancel(); - // Blocking wait until terminal state. Throws if called from runtime thread. + /** + * @brief Block until the state reaches a terminal condition. + * + * Calling wait() from the runtime async thread is disallowed; implementations + * should assert or throw if detected. See getFuture() for async waiting. + */ void wait(); - // Timed wait; returns true if terminal observed before timeout. + /** + * @brief Block until terminal or timeout. + * @return true if terminal observed before timeout, false otherwise. + */ template bool wait_for(const std::chrono::duration &timeout); - // Async alternative: a shared_future that becomes ready when the state is terminal. - // The shared_future is available immediately after createState() and supports multiple waiters. + /** + * @brief Return a shared_future that becomes ready when the state reaches + * a terminal condition (completed/failed/cancelled). + * + * The shared_future is available immediately after the state is created + * so callers may register waiters before run() is called. + */ std::shared_future getFuture(); - // Introspection - bool isRunning() const; // queued or actively running - bool isCompleted() const; // terminal success - bool isFailed() const; // terminal failure - bool isCancelled() const; // cancelled flag set + /** + * @brief Introspection helpers. + * + * isRunning(): queued or actively running inside the engine. + * isCompleted(): terminal success. + * isFailed(): terminal failure. + * isCancelled(): cancellation requested. + */ + bool isRunning() const; + bool isCompleted() const; + bool isFailed() const; + bool isCancelled() const; protected: - // Hooks to allow derived implementations to react to lifecycle events. + /** + * @brief Lifecycle hooks for derived implementations. + * + * Subclasses may override these to observe state lifecycle transitions. The + * default implementations are no-ops. + */ virtual void onStarted(); virtual void onCompleted(); virtual void onFailed(std::exception_ptr ep); virtual void onCancelled(); private: - // Implementation details (opaque to public header) should be placed in the - // corresponding .cpp. These members are intentionally documented in the - // spec but left to the implementation to manage. - - // Note: keep the header minimal to avoid exposing internal synchronization - // primitives; real implementation may add mutexes, atomic flags and - // promise/shared_future wiring. + // Implementation details are intentionally private and placed in the + // corresponding .cpp. Refer to the specification for the concurrency and + // lifecycle invariants the implementation must satisfy. }; +/** + * @class Dmn_Runtime_State_Engine + * @brief Singleton factory and manager for runtime-managed states. + * + * Responsibilities: + * - provide createState() returning a shared_ptr handle to a Dmn_Runtime_State + * - retain a shared_ptr to queued/running state objects to guarantee lifetime + * - integrate with Dmn_Runtime_Manager by creating runtime jobs for state steps + */ class Dmn_Runtime_State_Engine : public Dmn_Singleton { public: using DmnRuntimeStatePtr = std::shared_ptr; + /** + * @brief Obtain the singleton engine instance. + */ static auto createInstance() -> Dmn_Runtime_State_Engine &; - // Create a runtime-managed state object handle. - // The returned shared_ptr may be kept by the client; the engine will also - // retain a shared_ptr while the state is queued or running. + /** + * @brief Create a new runtime-managed state object and return a shared_ptr + * handle to the client. The engine will also hold a shared_ptr while + * the state is queued or running. + */ DmnRuntimeStatePtr createState(std::string_view name); - // Optional engine-level configuration and shutdown APIs are implemented - // in the source file as needed. - protected: Dmn_Runtime_State_Engine(); virtual ~Dmn_Runtime_State_Engine() noexcept; From 4074950ca55375c2384f0980acd72b0d034f1fdd Mon Sep 17 00:00:00 2001 From: Chee Bin Hoh Date: Sun, 30 Aug 2026 21:05:50 -0400 Subject: [PATCH 7/7] clang-format header --- include/dmn-runtime-state.hpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/include/dmn-runtime-state.hpp b/include/dmn-runtime-state.hpp index b8c361c..1a8c372 100644 --- a/include/dmn-runtime-state.hpp +++ b/include/dmn-runtime-state.hpp @@ -57,7 +57,8 @@ namespace dmn { class Dmn_Runtime_State : public Dmn_State { public: using FncType = std::function; - using OnErrorFnc = Dmn_Runtime_Job::OnErrorFncType; // std::function + using OnErrorFnc = Dmn_Runtime_Job::OnErrorFncType; // std::function /** * @brief Construct a runtime-managed state object with a human-readable name. @@ -103,8 +104,10 @@ class Dmn_Runtime_State : public Dmn_State { /** * @brief Schedule this state handle for runtime execution. * - * @param priority Job priority to use when enqueuing (maps to Dmn_Runtime_Job::Priority). - * @param delay If non-zero, the job is scheduled via addTimedJob() after this delay. + * @param priority Job priority to use when enqueuing (maps to + * Dmn_Runtime_Job::Priority). + * @param delay If non-zero, the job is scheduled via addTimedJob() after this + * delay. * @param onError Optional error callback forwarded to the runtime job. The * type matches Dmn_Runtime_Job::OnErrorFncType. * @return true if the state was successfully queued; false if enqueue @@ -116,9 +119,11 @@ class Dmn_Runtime_State : public Dmn_State { * - Calling run() from inside the runtime async thread is disallowed and * will assert/throw. */ - bool run(Dmn_Runtime_Job::Priority priority = Dmn_Runtime_Job::Priority::kMedium, - const std::chrono::steady_clock::duration &delay = std::chrono::steady_clock::duration::zero(), - OnErrorFnc onError = {}); + bool + run(Dmn_Runtime_Job::Priority priority = Dmn_Runtime_Job::Priority::kMedium, + const std::chrono::steady_clock::duration &delay = + std::chrono::steady_clock::duration::zero(), + OnErrorFnc onError = {}); /** * @brief Request cooperative cancellation of this state. @@ -194,7 +199,8 @@ class Dmn_Runtime_State : public Dmn_State { * - retain a shared_ptr to queued/running state objects to guarantee lifetime * - integrate with Dmn_Runtime_Manager by creating runtime jobs for state steps */ -class Dmn_Runtime_State_Engine : public Dmn_Singleton { +class Dmn_Runtime_State_Engine + : public Dmn_Singleton { public: using DmnRuntimeStatePtr = std::shared_ptr;