Class: Phronomy::WorkflowRunner Private
- Inherits:
-
Object
- Object
- Phronomy::WorkflowRunner
- Includes:
- Runnable
- Defined in:
- lib/phronomy/workflow_runner.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Execution engine for compiled workflows. Manages state entry/exit action execution, phase transitions, halt/resume, and wait states. Instantiated by Phronomy::Workflow and used internally.
Design principle
State transitions are driven entirely by state_machines. The PhaseTracker
holds a reference to the current WorkflowContext via attr_accessor :context,
and guard lambdas evaluate m.context (the WorkflowContext) rather than
the PhaseTracker itself. This ensures that "what happens next" is always
determined by the declared state machine topology, never by Phronomy internals.
Entry and exit actions are registered as state_machines after_transition to:
and before_transition from: callbacks respectively. Entry actions may either
mutate the context in place or return a new context (e.g. via s.merge(...)).
When an entry action returns a Phronomy::WorkflowContext, that value replaces
the current context; otherwise the return value is ignored.
Exit actions are always mutation-in-place; their return value is ignored.
The sole exception is the initial state: state_machines does not fire transition callbacks on initialization, so the entry action for the entry point is invoked directly by WorkflowRunner before the main execution loop begins.
Two transition categories registered in PhaseTracker
1. state_completed — all auto-fire transitions (with or without guards).
Fired when an action state's action completes.
Guards are evaluated in declaration order; first match wins.
(declared with +transition from: :foo, to: :bar+ or
+transition from: :foo, guard: ..., to: :bar+)
2. <event_name> — external events triggered by human input, originating
from wait states
(declared with +transition from: :awaiting, on: :approve, to: :run+)
Constant Summary collapse
- FINISH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Sentinel value for the terminal state of a workflow.
:__end__
Instance Method Summary collapse
-
#initialize(state_class:, entry_actions:, declared_states:, auto_transitions:, external_events:, entry_point:, exit_actions: {}, wait_state_names: [], state_store: nil, action_timeouts: {}) ⇒ WorkflowRunner
constructor
private
A new instance of WorkflowRunner.
-
#invoke(input, config: {}) ⇒ Object
private
Executes the workflow from the initial state.
-
#invoke_deferred(input, config: {}) ⇒ Phronomy::Task
private
Registers the workflow with the EventLoop and returns a Task immediately without blocking the caller.
-
#resume(state:, input: nil) ⇒ Object
private
Generic resume.
-
#send_event(state:, event:, input: nil) ⇒ Object
private
Fires a named event to advance a halted workflow.
-
#stream(input, config: {}) {|Hash| ... } ⇒ Object
private
Streaming execution.
Methods included from Runnable
Constructor Details
#initialize(state_class:, entry_actions:, declared_states:, auto_transitions:, external_events:, entry_point:, exit_actions: {}, wait_state_names: [], state_store: nil, action_timeouts: {}) ⇒ WorkflowRunner
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a new instance of WorkflowRunner.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/phronomy/workflow_runner.rb', line 48 def initialize(state_class:, entry_actions:, declared_states:, auto_transitions:, external_events:, entry_point:, exit_actions: {}, wait_state_names: [], state_store: nil, action_timeouts: {}) @state_class = state_class @entry_actions = entry_actions # { state_name => [callable, ...] } @declared_states = declared_states # Lookup set: states with at least one auto-fire transition declared. @auto_state_set = auto_transitions.each_with_object({}) { |t, h| h[t[:from]] = true } @external_events = external_events # { name => [{from:, to:, guard:}, ...] } @entry_point = entry_point @wait_state_names = wait_state_names @state_store = state_store @action_timeouts = action_timeouts # { state_name => seconds } @phase_machine_class = Workflow::PhaseMachineBuilder.new( entry_point: @entry_point, declared_states: @declared_states, wait_state_names: @wait_state_names, external_events: @external_events, entry_actions: @entry_actions, action_timeouts: @action_timeouts, auto_transitions: auto_transitions, exit_actions: exit_actions ).build end |
Instance Method Details
#invoke(input, config: {}) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Executes the workflow from the initial state.
76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/phronomy/workflow_runner.rb', line 76 def invoke(input, config: {}) = {} [:user_id] = config[:user_id] if config[:user_id] [:session_id] = config[:session_id] if config[:session_id] trace("workflow.invoke", input: input.inspect, **) do |_span| state, thread_id, recursion_limit, store = _build_initial_context(input, config) result = run_via_event_loop(state, recursion_limit: recursion_limit) store&.save(thread_id, {fields: result.to_h, phase: result.phase.to_s}) if config[:thread_id] [result, nil] end end |
#invoke_deferred(input, config: {}) ⇒ Phronomy::Task
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Registers the workflow with the EventLoop and returns a Task immediately without blocking the caller. The task resolves with the final context when the workflow finishes.
This is the EventLoop-driven equivalent of spawning a thread around #invoke. No extra OS thread is created; the EventLoop's existing thread drives the execution.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/phronomy/workflow_runner.rb', line 101 def invoke_deferred(input, config: {}) state, thread_id, recursion_limit, store = _build_initial_context(input, config) result_task = Phronomy::Task.deferred(name: "workflow-async:#{thread_id}") Phronomy::EventLoop.instance.start session = build_session_for(context: state, recursion_limit: recursion_limit) if store && config[:thread_id] # Wrap so that state is persisted when the task resolves. persist_task = Phronomy::Task.deferred(name: "workflow-async-persist:#{thread_id}") Phronomy::EventLoop.instance.register(session, completion: persist_task) persist_task.on_complete do |result, error| store.save(thread_id, {fields: result.to_h, phase: result.phase.to_s}) unless error if error result_task.backend.unblock(nil, error) result_task.transition!(:failed, error: error) else result_task.backend.unblock(result, nil) result_task.transition!(:completed, value: result) end end else Phronomy::EventLoop.instance.register(session, completion: result_task) end result_task end |
#resume(state:, input: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Generic resume. Equivalent to send_event(state:, event: :resume, input:).
131 132 133 |
# File 'lib/phronomy/workflow_runner.rb', line 131 def resume(state:, input: nil) send_event(state: state, event: :resume, input: input) end |
#send_event(state:, event:, input: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Fires a named event to advance a halted workflow.
The special event :resume selects the first external event registered
for the current wait state and fires it.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/phronomy/workflow_runner.rb', line 145 def send_event(state:, event:, input: nil) state = state.merge(input) if input event = event.to_sym current_phase = state.phase ev_to_fire = if event == :resume # Find the first external event that can originate from the current wait state. name, = @external_events.find { |_, ts| ts.any? { |t| t[:from] == current_phase } } unless name raise ArgumentError, "No external event registered for wait state #{current_phase.inspect}" end name else unless @external_events.key?(event) raise ArgumentError, "Unknown event #{event.inspect}. Valid events: #{@external_events.keys.inspect}" end event end run_via_event_loop(state, recursion_limit: Phronomy.configuration.recursion_limit, resume_event: ev_to_fire, resume_phase: current_phase) end |
#stream(input, config: {}) {|Hash| ... } ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Streaming execution. Yields { state: Symbol, context: Object } after each state action completes.
177 178 179 180 181 182 183 |
# File 'lib/phronomy/workflow_runner.rb', line 177 def stream(input, config: {}, &block) thread_id = config[:thread_id] || SecureRandom.uuid recursion_limit = config.fetch(:recursion_limit, Phronomy.configuration.recursion_limit) state = @state_class.new(**input) state.(thread_id: thread_id) run_workflow(state, recursion_limit: recursion_limit, &block) end |