Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ cmake_install.cmake
compile_commands.json
Build
build/
.github/
181 changes: 134 additions & 47 deletions docs/IMPLEMENTATION_PLAN_runtime-state.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,181 @@
# Runtime State Engine Implementation Plan
# Runtime State Manager 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.
This document is a step-by-step TDD-first implementation plan for the runtime state manager 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
Phase 0: API contract and header preparation
- `docs/specs/runtime-state-machine-spec.md` defines ownership, run(onError),
cancel, wait(timeout)/shared_future, priority/timed variants, and tests.
- `Dmn_Runtime_State_Manager` uses the inherited
`Dmn_Singleton<Dmn_Runtime_State_Manager>::createInstance()` factory and
therefore returns `std::shared_ptr<Dmn_Runtime_State_Manager>`. It must not
declare a conflicting reference-returning factory.
- `Dmn_Runtime_Manager::isRunInAsyncThread()` is part of the public runtime
API so runtime-state can reject run()/wait()/wait_for() calls from the async
thread.
- Update `include/dmn-runtime-state.hpp` to reflect these contracts before
adding its implementation.
- The selected lifecycle contract is: a pre-run shared future remains pending;
no configured state makes run() return false without terminalizing; cancel()
before run() terminalizes as cancelled; failed futures rethrow the captured
exception from get(); and runtime-thread run()/wait()/wait_for() calls throw
in all build configurations.

Phase 1: Construct the singleton manager (complete)
- Correct `include/dmn-runtime-state.hpp` so
`Dmn_Runtime_State_Manager` has a protected constructor, public destructor,
and friends `Dmn_Singleton<Dmn_Runtime_State_Manager>`. Do not declare a
conflicting `createInstance()` method; use the inherited shared-pointer
factory. The public destructor is required by the singleton's default
`std::shared_ptr` deleter.
- Add `src/dmn-runtime-state.cpp` containing the manager constructor and
destructor definitions.
- Add `test/dmn-test-runtime-state.cpp`, with a focused unit test that
calls `Dmn_Runtime_State_Manager::createInstance()`, verifies the returned
shared pointer is non-null, and verifies repeated calls return the same
manager address.
- Add `dmn-test-runtime-state` to `test/CMakeLists.txt`.
- Add `src/dmn-runtime-state.cpp` and `include/dmn-runtime-state.hpp` to the
`dmn` target in
`src/CMakeLists.txt`. Add the header to `include/dmn.hpp`.

Verify:
- cmake -B build -DCMAKE_BUILD_TYPE=Debug
- cmake --build build
- ctest --test-dir build --output-on-failure
- ctest --test-dir build -R dmn-test-runtime-state --output-on-failure

Completed follow-on increment: State-handle creation
- Add the `DmnRuntimeStatePtr` alias for
`std::shared_ptr<Dmn_Runtime_State>`.
- Implement `Dmn_Runtime_State_Manager::createState(std::string_view)` to
construct and return a new `Dmn_Runtime_State`.
- Define the runtime-state constructor, destructor, and default no-op
lifecycle hooks required to link the concrete polymorphic type.
- Extend `dmn-test-runtime-state` to verify `createState()` returns a
non-null handle, that `Dmn_Runtime_State` derives from `Dmn_State`, and
that inherited state configuration and stepping remain accessible for
compatibility testing.
- Remove the shadowing `Dmn_Runtime_State` declarations of `setStateFnc()`,
`setNext()`, and `setEnd()`. `runNext()` is re-exposed as protected and is
available to `Dmn_Runtime_State_Manager` through friendship.
- Add `Dmn_State::hasStateFncs()` as a public query for whether the client
configured at least one state function, excluding the internal
initialization function.
- Do not retain created states in the manager yet. Retention begins only when
a later `run()` implementation queues a state.

Phase 2: Terminal-state primitive and lifecycle unit tests (complete)
- Do not make the manager advance state transitions implicitly: it controls
when `runNext()` executes, while a state function uses its `Dmn_State &`
parameter to call `setNext()` or `setEnd()`.
- Require clients to finish configuring state functions before successful
submission, because configuration is not synchronized with runtime execution.
- Implement the completion promise/shared_future pair, terminal flags, and a
single idempotent terminal transition helper.
- Implement the selected no-state and cancel-before-run behavior.
- Add targeted tests for a pending pre-run future, rejected unconfigured
run(), and cancel-before-run terminalization.

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<void*, std::shared_ptr<Dmn_Runtime_State>> m_pendingStates;` keyed by pointer or generated id.
- The job's m_fnc must create a coroutine task (TaskFncType) that:
Verify:
- cmake -B build -DCMAKE_BUILD_TYPE=Debug
- cmake --build build
- ctest --test-dir build -R dmn-test-runtime-state --output-on-failure

Phase 3: Basic runtime enqueue & single-step execution (complete)
- Implement run() to set the mutex-protected queued flag and enqueue a
Dmn_Runtime_Job to `Dmn_Runtime_Manager::addJob()` (immediate) or
`addTimedJob()` (initial delay). Use Dmn_Runtime_Job::Priority.
- The manager retains an internal shared_ptr to the state while queued or
running in `std::unordered_map<const Dmn_Runtime_State *,
DmnRuntimeStatePtr> m_pendingStates`.
- The job's m_fnc creates 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
- if still active, repost immediately by calling addJob() again
- if terminal, set completion promise and erase manager 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
Phase 4: Exception capture and onError forwarding (complete)
- 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)
- set `m_completionPromise` with the captured exception so
`getFuture().get()` rethrows it
- 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.
Phase 5: Complete lifecycle and scheduling coverage (complete)
- Added focused named Google Test cases for singleton/state creation,
unconfigured and pre-run cancellation behavior, normal execution, failure
propagation, queued cancellation, manager-retained lifetime, priority
ordering, delayed initial submission, and runtime-thread rejection.
- The queued-cancellation test verifies no user-defined step executes after
cancellation and that the inherited `Dmn_State` is finalized.
- The retained-lifetime test verifies a client can release its handle after
submission and that the manager releases its final ownership after terminal
completion.
- The priority and delay tests verify `run(priority, delay, onError)` maps
correctly to runtime scheduling behavior.
- The runtime-thread test verifies `run()`, `wait()`, and `wait_for()` throw
`std::runtime_error` from the runtime async thread.

Phase 6: Drain-and-cancel manager shutdown (complete)
- Added `Dmn_Runtime_State_Manager::shutdown()`, which permanently rejects new
state submissions while allowing callers to create non-runnable handles.
- Shutdown snapshots the manager-retained handles, requests cooperative
cancellation outside the manager mutex, and waits for all captured states to
reach terminal cancellation before returning.
- A state step already executing may finish its callback, but its terminal
outcome is cancellation when shutdown requested it. Queued states finalize
without running another user-defined callback.
- Shutdown is idempotent and rejects calls from the runtime async thread to
avoid deadlock.
- Added coverage for drain waiting, running and queued state cancellation, and
rejection of a post-shutdown submission.

Phase 7: Integration, stress, and documentation (complete)
- Added multi-state integration coverage for serialized execution and runtime
async-thread affinity, plus failure-isolation coverage for independent
queued states.
- Added concurrent client coverage for create/run/cancel/getFuture/wait
operations across 24 states, and shutdown stress coverage for 32 queued
states behind a running callback.
- Added a public usage example that documents runtime initialization from the
main thread, explicit state-manager shutdown while the runtime loop is
active, and runtime shutdown only after state draining completes.

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 weak_ptr in runtime job to avoid reference cycles; the manager's internal shared_ptr keeps the object alive while queued.
- Use the state mutex to set the 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.
- Be careful to release manager 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.
- The manager exposes one drain-and-cancel shutdown mode and no
concurrency-configuration API; state steps execute in the process-wide
runtime async context.

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

Loading
Loading