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
94 changes: 94 additions & 0 deletions docs/IMPLEMENTATION_PLAN_runtime-state.md
Original file line number Diff line number Diff line change
@@ -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<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:
- 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

132 changes: 132 additions & 0 deletions docs/specs/runtime-state-machine-plan.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading