phronomy 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +3 -4
- data/CHANGELOG.md +200 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +152 -0
- data/docs/architecture/persistence.md +175 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +82 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/README.md +172 -0
- data/docs/features.md +31 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +145 -72
- data/lib/phronomy/agent/base.rb +388 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +336 -102
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime.rb +77 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
|
@@ -5,13 +5,15 @@ require "securerandom"
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
# Execution boundary for compiled Workflows.
|
|
7
7
|
#
|
|
8
|
-
# WorkflowRunner separates three identities:
|
|
8
|
+
# WorkflowRunner separates three identities/responsibilities:
|
|
9
9
|
# - application session_id remains caller/tracing metadata;
|
|
10
|
-
# -
|
|
11
|
-
# -
|
|
10
|
+
# - workflow_instance_id identifies durable Workflow state;
|
|
11
|
+
# - each concrete FSMSession owns a runtime-only fsm_session_id;
|
|
12
|
+
# - Runtime admission uses a separate opaque owner token.
|
|
12
13
|
#
|
|
13
14
|
# Workflow persistence is synchronous at the repository contract but is always
|
|
14
|
-
# invoked through Runtime's OffloadPool
|
|
15
|
+
# invoked through Runtime's OffloadPool. Durable terminal save results return
|
|
16
|
+
# to the same FSMSession before that session becomes halted/completed.
|
|
15
17
|
# @api private
|
|
16
18
|
class WorkflowRunner
|
|
17
19
|
include Phronomy::Runnable
|
|
@@ -20,23 +22,52 @@ module Phronomy
|
|
|
20
22
|
|
|
21
23
|
Execution = Data.define(
|
|
22
24
|
:context,
|
|
23
|
-
:
|
|
24
|
-
:
|
|
25
|
+
:workflow_instance_id,
|
|
26
|
+
:owner_token,
|
|
25
27
|
:recursion_limit,
|
|
26
28
|
:repository,
|
|
27
29
|
:persist,
|
|
28
30
|
:expected_revision
|
|
29
31
|
)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
# API/callback -> EventLoop control messages.
|
|
34
|
+
StartCommand = Data.define(
|
|
32
35
|
:runner,
|
|
36
|
+
:input,
|
|
37
|
+
:config,
|
|
38
|
+
:stable_observer,
|
|
33
39
|
:result_task,
|
|
34
|
-
:
|
|
35
|
-
:
|
|
36
|
-
:
|
|
37
|
-
|
|
40
|
+
:workflow_instance_id,
|
|
41
|
+
:owner_token,
|
|
42
|
+
:explicit_workflow_instance_id
|
|
43
|
+
)
|
|
44
|
+
ResumeCommand = Data.define(
|
|
45
|
+
:runner,
|
|
46
|
+
:state,
|
|
47
|
+
:input,
|
|
48
|
+
:event_name,
|
|
49
|
+
:current_phase,
|
|
50
|
+
:result_task,
|
|
51
|
+
:workflow_instance_id,
|
|
52
|
+
:owner_token
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# EventLoop -> Offload operation-specific immutable/value snapshots.
|
|
56
|
+
WorkflowLoadCommand = Data.define(:repository, :workflow_instance_id)
|
|
57
|
+
WorkflowTerminalPersistenceCommand = Data.define(
|
|
58
|
+
:repository,
|
|
59
|
+
:workflow_instance_id,
|
|
60
|
+
:expected_revision,
|
|
61
|
+
:snapshot
|
|
38
62
|
)
|
|
39
63
|
|
|
64
|
+
# Offload -> EventLoop/FSMSession operation results.
|
|
65
|
+
WorkflowLoadResult = Data.define(:repository, :snapshot, :revision)
|
|
66
|
+
WorkflowTerminalPersistenceResult = Data.define(:outcome, :revision, :error)
|
|
67
|
+
|
|
68
|
+
StartLoadReady = Data.define(:runner, :request, :result, :error)
|
|
69
|
+
ResumeLoadReady = Data.define(:runner, :request, :result, :error)
|
|
70
|
+
|
|
40
71
|
def initialize(
|
|
41
72
|
state_class:,
|
|
42
73
|
entry_actions:,
|
|
@@ -71,14 +102,7 @@ module Phronomy
|
|
|
71
102
|
|
|
72
103
|
def invoke(input, config: {})
|
|
73
104
|
ensure_blocking_call_allowed!(:invoke, :invoke_async)
|
|
74
|
-
|
|
75
|
-
caller_meta[:user_id] = config[:user_id] if config[:user_id]
|
|
76
|
-
caller_meta[:session_id] = config[:session_id] if config[:session_id]
|
|
77
|
-
|
|
78
|
-
trace("workflow.invoke", input: input.inspect, **caller_meta) do |_span|
|
|
79
|
-
result = start_new_execution(input, config).wait_result
|
|
80
|
-
[result, nil]
|
|
81
|
-
end
|
|
105
|
+
start_new_execution(input, config).wait_result
|
|
82
106
|
end
|
|
83
107
|
|
|
84
108
|
def invoke_deferred(input, config: {})
|
|
@@ -100,9 +124,9 @@ module Phronomy
|
|
|
100
124
|
ensure_blocking_call_allowed!(:send_event, :signal)
|
|
101
125
|
current_phase = state.phase.to_sym
|
|
102
126
|
event_name = resolve_resume_event(current_phase, event)
|
|
103
|
-
|
|
104
|
-
unless
|
|
105
|
-
raise ArgumentError, "Halted WorkflowContext has no
|
|
127
|
+
workflow_instance_id = state.workflow_instance_id
|
|
128
|
+
unless workflow_instance_id
|
|
129
|
+
raise ArgumentError, "Halted WorkflowContext has no workflow_instance_id"
|
|
106
130
|
end
|
|
107
131
|
|
|
108
132
|
start_resume_execution(
|
|
@@ -114,10 +138,11 @@ module Phronomy
|
|
|
114
138
|
end
|
|
115
139
|
|
|
116
140
|
# Posts an application-defined event to the currently live Workflow owner.
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
# EventLoop resolves workflow_instance_id to the current concrete
|
|
142
|
+
# fsm_session_id. The opaque admission owner token is deliberately not a
|
|
143
|
+
# routing identity and is not exposed here.
|
|
144
|
+
def signal(workflow_instance_id:, event:, payload: nil)
|
|
145
|
+
raise ArgumentError, "workflow_instance_id is required" if workflow_instance_id.nil?
|
|
121
146
|
|
|
122
147
|
event_name = event.to_sym
|
|
123
148
|
unless @external_events.key?(event_name)
|
|
@@ -127,28 +152,59 @@ module Phronomy
|
|
|
127
152
|
end
|
|
128
153
|
|
|
129
154
|
Phronomy::Runtime.instance.event_loop.post_to_workflow(
|
|
130
|
-
|
|
155
|
+
workflow_instance_id: workflow_instance_id,
|
|
131
156
|
event: event_name,
|
|
132
157
|
payload: payload
|
|
133
158
|
)
|
|
134
159
|
end
|
|
135
160
|
|
|
136
|
-
#
|
|
137
|
-
|
|
161
|
+
# Every Workflow control message is delivered by EventLoop. Admission,
|
|
162
|
+
# hydration apply, FSMSession creation/binding, and terminal live-state
|
|
163
|
+
# progression therefore remain EventLoop-owned.
|
|
164
|
+
# @api private
|
|
165
|
+
def deliver_on_event_loop(command)
|
|
138
166
|
event_loop = Phronomy::Runtime.instance.event_loop
|
|
139
|
-
event_loop
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
assert_event_loop!(event_loop)
|
|
168
|
+
|
|
169
|
+
case command
|
|
170
|
+
when StartCommand
|
|
171
|
+
begin_start_on_event_loop(command)
|
|
172
|
+
when ResumeCommand
|
|
173
|
+
begin_resume_on_event_loop(command)
|
|
174
|
+
when StartLoadReady
|
|
175
|
+
apply_start_load_on_event_loop(command)
|
|
176
|
+
when ResumeLoadReady
|
|
177
|
+
apply_resume_load_on_event_loop(command)
|
|
145
178
|
else
|
|
146
|
-
|
|
179
|
+
raise Phronomy::Error, "unknown Workflow control command: #{command.class}"
|
|
147
180
|
end
|
|
148
181
|
end
|
|
149
182
|
|
|
150
183
|
private
|
|
151
184
|
|
|
185
|
+
def workflow_trace_metadata(config)
|
|
186
|
+
metadata = {}
|
|
187
|
+
metadata[:user_id] = config[:user_id] if config[:user_id]
|
|
188
|
+
metadata[:session_id] = config[:session_id] if config[:session_id]
|
|
189
|
+
if (invocation_context = config[:invocation_context])
|
|
190
|
+
metadata[:task_id] = invocation_context.task_id if invocation_context.task_id
|
|
191
|
+
if invocation_context.parent_task_id
|
|
192
|
+
metadata[:parent_task_id] = invocation_context.parent_task_id
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
metadata
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# CG-01 is a clean break. Reject the removed Workflow config key instead of
|
|
199
|
+
# silently generating a new Workflow identity and branching durable history.
|
|
200
|
+
# This is a migration error path, not a compatibility alias.
|
|
201
|
+
def reject_legacy_workflow_identity_key!(config)
|
|
202
|
+
return unless config.key?(:thread_id) || config.key?("thread_id")
|
|
203
|
+
|
|
204
|
+
raise ArgumentError,
|
|
205
|
+
"Workflow config key :thread_id was removed; use :workflow_instance_id"
|
|
206
|
+
end
|
|
207
|
+
|
|
152
208
|
def ensure_blocking_call_allowed!(method_name, async_alternative)
|
|
153
209
|
return unless Phronomy::Runtime.instance.event_loop.current?
|
|
154
210
|
|
|
@@ -158,149 +214,135 @@ module Phronomy
|
|
|
158
214
|
end
|
|
159
215
|
|
|
160
216
|
def start_new_execution(input, config, stable_observer: nil)
|
|
217
|
+
reject_legacy_workflow_identity_key!(config)
|
|
161
218
|
runtime = Phronomy::Runtime.instance
|
|
162
|
-
event_loop = runtime.event_loop
|
|
163
219
|
result_task = Phronomy::Task.deferred(name: "workflow:preparing")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
220
|
+
explicit_workflow_instance_id = !config[:workflow_instance_id].nil?
|
|
221
|
+
workflow_instance_id = (config[:workflow_instance_id] || SecureRandom.uuid).to_s.freeze
|
|
222
|
+
Phronomy::Tracing::Automatic.observe_task(
|
|
223
|
+
result_task,
|
|
224
|
+
"workflow.execution",
|
|
225
|
+
input: input,
|
|
226
|
+
workflow_instance_id: workflow_instance_id,
|
|
227
|
+
mode: stable_observer ? :stream : :invoke,
|
|
228
|
+
**workflow_trace_metadata(config)
|
|
170
229
|
)
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
command = StartCommand.new(
|
|
231
|
+
runner: self,
|
|
232
|
+
input: input,
|
|
233
|
+
config: config.dup.freeze,
|
|
234
|
+
stable_observer: stable_observer,
|
|
235
|
+
result_task: result_task,
|
|
236
|
+
workflow_instance_id: workflow_instance_id,
|
|
237
|
+
owner_token: Object.new.freeze,
|
|
238
|
+
explicit_workflow_instance_id: explicit_workflow_instance_id
|
|
239
|
+
)
|
|
240
|
+
fail_task(result_task, runtime_rejected_error(:start)) unless post_control(runtime, command)
|
|
241
|
+
result_task
|
|
242
|
+
rescue => error
|
|
243
|
+
fail_task(result_task, error) if defined?(result_task) && result_task
|
|
244
|
+
result_task || failed_task("workflow:preparation", error)
|
|
245
|
+
end
|
|
173
246
|
|
|
247
|
+
def start_resume_execution(state, input:, event_name:, current_phase:)
|
|
248
|
+
runtime = Phronomy::Runtime.instance
|
|
249
|
+
workflow_instance_id = state.workflow_instance_id.to_s.freeze
|
|
250
|
+
result_task = Phronomy::Task.deferred(name: "workflow-resume:#{workflow_instance_id}")
|
|
251
|
+
Phronomy::Tracing::Automatic.observe_task(
|
|
252
|
+
result_task,
|
|
253
|
+
"workflow.execution",
|
|
254
|
+
input: input,
|
|
255
|
+
workflow_instance_id: workflow_instance_id,
|
|
256
|
+
mode: :resume,
|
|
257
|
+
event: event_name
|
|
258
|
+
)
|
|
259
|
+
command = ResumeCommand.new(
|
|
260
|
+
runner: self,
|
|
261
|
+
state: state,
|
|
262
|
+
input: input,
|
|
263
|
+
event_name: event_name.to_sym,
|
|
264
|
+
current_phase: current_phase.to_sym,
|
|
265
|
+
result_task: result_task,
|
|
266
|
+
workflow_instance_id: workflow_instance_id,
|
|
267
|
+
owner_token: Object.new.freeze
|
|
268
|
+
)
|
|
269
|
+
fail_task(result_task, runtime_rejected_error(:resume)) unless post_control(runtime, command)
|
|
270
|
+
result_task
|
|
271
|
+
rescue => error
|
|
272
|
+
fail_task(result_task, error) if defined?(result_task) && result_task
|
|
273
|
+
result_task || failed_task("workflow:resume-preparation", error)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def begin_start_on_event_loop(request)
|
|
277
|
+
runtime = Phronomy::Runtime.instance
|
|
278
|
+
event_loop = runtime.event_loop
|
|
279
|
+
admitted = false
|
|
174
280
|
event_loop.admit_workflow(
|
|
175
|
-
|
|
176
|
-
|
|
281
|
+
request.workflow_instance_id,
|
|
282
|
+
owner_token: request.owner_token
|
|
177
283
|
)
|
|
284
|
+
admitted = true
|
|
178
285
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
next
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
begin
|
|
192
|
-
execution = build_new_execution(
|
|
193
|
-
input,
|
|
194
|
-
thread_id: thread_id,
|
|
195
|
-
fsm_session_id: fsm_session_id,
|
|
196
|
-
recursion_limit: recursion_limit,
|
|
197
|
-
repository: repository,
|
|
198
|
-
persist: true,
|
|
199
|
-
record: record
|
|
200
|
-
)
|
|
201
|
-
register_execution(
|
|
202
|
-
execution,
|
|
203
|
-
result_task,
|
|
204
|
-
stable_observer: stable_observer
|
|
205
|
-
)
|
|
206
|
-
rescue => preparation_error
|
|
207
|
-
release_and_fail(
|
|
208
|
-
event_loop,
|
|
209
|
-
result_task,
|
|
210
|
-
thread_id,
|
|
211
|
-
fsm_session_id,
|
|
212
|
-
preparation_error
|
|
213
|
-
)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
else
|
|
286
|
+
recursion_limit = request.config.fetch(
|
|
287
|
+
:recursion_limit,
|
|
288
|
+
Phronomy.configuration.recursion_limit
|
|
289
|
+
)
|
|
290
|
+
repository = configured_repository
|
|
291
|
+
persist = request.explicit_workflow_instance_id && !repository.nil?
|
|
292
|
+
|
|
293
|
+
unless persist
|
|
217
294
|
execution = build_new_execution(
|
|
218
|
-
input,
|
|
219
|
-
|
|
220
|
-
|
|
295
|
+
request.input,
|
|
296
|
+
workflow_instance_id: request.workflow_instance_id,
|
|
297
|
+
owner_token: request.owner_token,
|
|
221
298
|
recursion_limit: recursion_limit,
|
|
222
299
|
repository: repository,
|
|
223
300
|
persist: false,
|
|
224
|
-
|
|
301
|
+
loaded_snapshot: nil,
|
|
302
|
+
expected_revision: nil
|
|
225
303
|
)
|
|
226
304
|
register_execution(
|
|
227
305
|
execution,
|
|
228
|
-
result_task,
|
|
229
|
-
stable_observer: stable_observer
|
|
306
|
+
request.result_task,
|
|
307
|
+
stable_observer: request.stable_observer
|
|
230
308
|
)
|
|
309
|
+
return
|
|
231
310
|
end
|
|
232
|
-
|
|
311
|
+
|
|
312
|
+
submit_workflow_load(
|
|
313
|
+
runtime,
|
|
314
|
+
repository,
|
|
315
|
+
request.workflow_instance_id,
|
|
316
|
+
StartLoadReady,
|
|
317
|
+
request
|
|
318
|
+
)
|
|
233
319
|
rescue => error
|
|
234
|
-
if
|
|
235
|
-
defined?(thread_id) && defined?(fsm_session_id)
|
|
320
|
+
if admitted
|
|
236
321
|
event_loop.release_workflow(
|
|
237
|
-
|
|
238
|
-
|
|
322
|
+
request.workflow_instance_id,
|
|
323
|
+
owner_token: request.owner_token
|
|
239
324
|
)
|
|
240
325
|
end
|
|
241
|
-
fail_task(result_task, error)
|
|
242
|
-
result_task || failed_task("workflow:preparation", error)
|
|
326
|
+
fail_task(request.result_task, error)
|
|
243
327
|
end
|
|
244
328
|
|
|
245
|
-
def
|
|
329
|
+
def begin_resume_on_event_loop(request)
|
|
246
330
|
runtime = Phronomy::Runtime.instance
|
|
247
331
|
event_loop = runtime.event_loop
|
|
248
|
-
|
|
249
|
-
fsm_session_id = SecureRandom.uuid
|
|
250
|
-
repository = configured_repository
|
|
251
|
-
result_task = Phronomy::Task.deferred(name: "workflow-resume:#{thread_id}")
|
|
252
|
-
|
|
332
|
+
admitted = false
|
|
253
333
|
event_loop.admit_workflow(
|
|
254
|
-
|
|
255
|
-
|
|
334
|
+
request.workflow_instance_id,
|
|
335
|
+
owner_token: request.owner_token
|
|
256
336
|
)
|
|
337
|
+
admitted = true
|
|
257
338
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
262
|
-
load_operation.on_complete do |record, error|
|
|
263
|
-
if error
|
|
264
|
-
release_and_fail(
|
|
265
|
-
event_loop, result_task, thread_id, fsm_session_id, error
|
|
266
|
-
)
|
|
267
|
-
next
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
begin
|
|
271
|
-
expected_revision = validate_resume_snapshot!(state, record)
|
|
272
|
-
context = input ? state.merge(input) : state
|
|
273
|
-
execution = Execution.new(
|
|
274
|
-
context: context,
|
|
275
|
-
thread_id: thread_id,
|
|
276
|
-
fsm_session_id: fsm_session_id,
|
|
277
|
-
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
278
|
-
repository: repository,
|
|
279
|
-
persist: true,
|
|
280
|
-
expected_revision: expected_revision
|
|
281
|
-
)
|
|
282
|
-
register_execution(
|
|
283
|
-
execution,
|
|
284
|
-
result_task,
|
|
285
|
-
resume_event: event_name,
|
|
286
|
-
resume_phase: current_phase
|
|
287
|
-
)
|
|
288
|
-
rescue => preparation_error
|
|
289
|
-
release_and_fail(
|
|
290
|
-
event_loop,
|
|
291
|
-
result_task,
|
|
292
|
-
thread_id,
|
|
293
|
-
fsm_session_id,
|
|
294
|
-
preparation_error
|
|
295
|
-
)
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
else
|
|
299
|
-
context = input ? state.merge(input) : state
|
|
339
|
+
repository = configured_repository
|
|
340
|
+
unless repository
|
|
341
|
+
context = request.input ? request.state.merge(request.input) : request.state
|
|
300
342
|
execution = Execution.new(
|
|
301
343
|
context: context,
|
|
302
|
-
|
|
303
|
-
|
|
344
|
+
workflow_instance_id: request.workflow_instance_id,
|
|
345
|
+
owner_token: request.owner_token,
|
|
304
346
|
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
305
347
|
repository: nil,
|
|
306
348
|
persist: false,
|
|
@@ -308,35 +350,130 @@ module Phronomy
|
|
|
308
350
|
)
|
|
309
351
|
register_execution(
|
|
310
352
|
execution,
|
|
311
|
-
result_task,
|
|
312
|
-
resume_event: event_name,
|
|
313
|
-
resume_phase: current_phase
|
|
353
|
+
request.result_task,
|
|
354
|
+
resume_event: request.event_name,
|
|
355
|
+
resume_phase: request.current_phase
|
|
314
356
|
)
|
|
357
|
+
return
|
|
315
358
|
end
|
|
316
|
-
|
|
359
|
+
|
|
360
|
+
submit_workflow_load(
|
|
361
|
+
runtime,
|
|
362
|
+
repository,
|
|
363
|
+
request.workflow_instance_id,
|
|
364
|
+
ResumeLoadReady,
|
|
365
|
+
request
|
|
366
|
+
)
|
|
317
367
|
rescue => error
|
|
318
|
-
if
|
|
319
|
-
defined?(thread_id) && defined?(fsm_session_id)
|
|
368
|
+
if admitted
|
|
320
369
|
event_loop.release_workflow(
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
request.workflow_instance_id,
|
|
371
|
+
owner_token: request.owner_token
|
|
323
372
|
)
|
|
324
373
|
end
|
|
325
|
-
fail_task(result_task, error)
|
|
326
|
-
|
|
374
|
+
fail_task(request.result_task, error)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def submit_workflow_load(runtime, repository, workflow_instance_id, ready_class, request)
|
|
378
|
+
operation = WorkflowLoadCommand.new(
|
|
379
|
+
repository: repository,
|
|
380
|
+
workflow_instance_id: workflow_instance_id.to_s.freeze
|
|
381
|
+
)
|
|
382
|
+
task = runtime.offload.submit(on_full: :raise) do
|
|
383
|
+
record = operation.repository.load(operation.workflow_instance_id)
|
|
384
|
+
WorkflowLoadResult.new(
|
|
385
|
+
repository: operation.repository,
|
|
386
|
+
snapshot: deep_immutable_copy(record_value(record, :snapshot)),
|
|
387
|
+
revision: record_value(record, :revision)
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
task.on_complete do |result, error|
|
|
391
|
+
ready = ready_class.new(
|
|
392
|
+
runner: self,
|
|
393
|
+
request: request,
|
|
394
|
+
result: result,
|
|
395
|
+
error: error
|
|
396
|
+
)
|
|
397
|
+
fail_task(request.result_task, runtime_rejected_error(:load_result)) unless
|
|
398
|
+
post_control(runtime, ready)
|
|
399
|
+
end
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def apply_start_load_on_event_loop(ready)
|
|
404
|
+
request = ready.request
|
|
405
|
+
if ready.error
|
|
406
|
+
release_and_fail(request, ready.error)
|
|
407
|
+
return
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
recursion_limit = request.config.fetch(
|
|
411
|
+
:recursion_limit,
|
|
412
|
+
Phronomy.configuration.recursion_limit
|
|
413
|
+
)
|
|
414
|
+
execution = build_new_execution(
|
|
415
|
+
request.input,
|
|
416
|
+
workflow_instance_id: request.workflow_instance_id,
|
|
417
|
+
owner_token: request.owner_token,
|
|
418
|
+
recursion_limit: recursion_limit,
|
|
419
|
+
repository: ready.result.repository,
|
|
420
|
+
persist: true,
|
|
421
|
+
loaded_snapshot: ready.result.snapshot,
|
|
422
|
+
expected_revision: ready.result.revision
|
|
423
|
+
)
|
|
424
|
+
register_execution(
|
|
425
|
+
execution,
|
|
426
|
+
request.result_task,
|
|
427
|
+
stable_observer: request.stable_observer
|
|
428
|
+
)
|
|
429
|
+
rescue => error
|
|
430
|
+
release_and_fail(request, error)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def apply_resume_load_on_event_loop(ready)
|
|
434
|
+
request = ready.request
|
|
435
|
+
if ready.error
|
|
436
|
+
release_and_fail(request, ready.error)
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
expected_revision = validate_resume_snapshot!(
|
|
441
|
+
request.state,
|
|
442
|
+
ready.result.snapshot,
|
|
443
|
+
ready.result.revision
|
|
444
|
+
)
|
|
445
|
+
context = request.input ? request.state.merge(request.input) : request.state
|
|
446
|
+
execution = Execution.new(
|
|
447
|
+
context: context,
|
|
448
|
+
workflow_instance_id: request.workflow_instance_id,
|
|
449
|
+
owner_token: request.owner_token,
|
|
450
|
+
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
451
|
+
repository: ready.result.repository,
|
|
452
|
+
persist: true,
|
|
453
|
+
expected_revision: expected_revision
|
|
454
|
+
)
|
|
455
|
+
register_execution(
|
|
456
|
+
execution,
|
|
457
|
+
request.result_task,
|
|
458
|
+
resume_event: request.event_name,
|
|
459
|
+
resume_phase: request.current_phase
|
|
460
|
+
)
|
|
461
|
+
rescue => error
|
|
462
|
+
release_and_fail(request, error)
|
|
327
463
|
end
|
|
328
464
|
|
|
329
465
|
def build_new_execution(
|
|
330
466
|
input,
|
|
331
|
-
|
|
332
|
-
|
|
467
|
+
workflow_instance_id:,
|
|
468
|
+
owner_token:,
|
|
333
469
|
recursion_limit:,
|
|
334
470
|
repository:,
|
|
335
471
|
persist:,
|
|
336
|
-
|
|
472
|
+
loaded_snapshot:,
|
|
473
|
+
expected_revision:
|
|
337
474
|
)
|
|
338
|
-
|
|
339
|
-
|
|
475
|
+
stored_fields = loaded_snapshot &&
|
|
476
|
+
(loaded_snapshot[:fields] || loaded_snapshot["fields"])
|
|
340
477
|
initial_fields = if stored_fields
|
|
341
478
|
stored_fields
|
|
342
479
|
.transform_keys(&:to_sym)
|
|
@@ -346,29 +483,29 @@ module Phronomy
|
|
|
346
483
|
end
|
|
347
484
|
|
|
348
485
|
context = @state_class.new(**initial_fields)
|
|
349
|
-
context.set_graph_metadata(
|
|
486
|
+
context.set_graph_metadata(workflow_instance_id: workflow_instance_id)
|
|
350
487
|
|
|
351
488
|
Execution.new(
|
|
352
489
|
context: context,
|
|
353
|
-
|
|
354
|
-
|
|
490
|
+
workflow_instance_id: workflow_instance_id,
|
|
491
|
+
owner_token: owner_token,
|
|
355
492
|
recursion_limit: recursion_limit,
|
|
356
493
|
repository: repository,
|
|
357
494
|
persist: persist,
|
|
358
|
-
expected_revision:
|
|
495
|
+
expected_revision: expected_revision
|
|
359
496
|
)
|
|
360
497
|
end
|
|
361
498
|
|
|
362
|
-
def validate_resume_snapshot!(state,
|
|
363
|
-
return nil unless
|
|
499
|
+
def validate_resume_snapshot!(state, durable_snapshot, durable_revision)
|
|
500
|
+
return nil unless durable_snapshot
|
|
364
501
|
|
|
365
|
-
|
|
502
|
+
normalized_durable = normalize_snapshot(durable_snapshot)
|
|
366
503
|
local_snapshot = normalize_snapshot(snapshot_for(state))
|
|
367
|
-
return
|
|
504
|
+
return durable_revision if normalized_durable == local_snapshot
|
|
368
505
|
|
|
369
506
|
raise Phronomy::Persistence::ConflictError,
|
|
370
507
|
"Workflow state changed since the supplied halted context for " \
|
|
371
|
-
"
|
|
508
|
+
"workflow_instance_id #{state.workflow_instance_id.inspect}; explicit reload/reconciliation is required"
|
|
372
509
|
end
|
|
373
510
|
|
|
374
511
|
def normalize_snapshot(snapshot)
|
|
@@ -396,6 +533,22 @@ module Phronomy
|
|
|
396
533
|
end
|
|
397
534
|
end
|
|
398
535
|
|
|
536
|
+
def deep_immutable_copy(value)
|
|
537
|
+
case value
|
|
538
|
+
when Hash
|
|
539
|
+
value.each_with_object({}) do |(key, child), result|
|
|
540
|
+
copied_key = key.is_a?(String) ? key.dup.freeze : key
|
|
541
|
+
result[copied_key] = deep_immutable_copy(child)
|
|
542
|
+
end.freeze
|
|
543
|
+
when Array
|
|
544
|
+
value.map { |child| deep_immutable_copy(child) }.freeze
|
|
545
|
+
when String
|
|
546
|
+
value.dup.freeze
|
|
547
|
+
else
|
|
548
|
+
value
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
399
552
|
def record_value(record, key)
|
|
400
553
|
return nil unless record
|
|
401
554
|
record.key?(key) ? record[key] : record[key.to_s]
|
|
@@ -414,10 +567,22 @@ module Phronomy
|
|
|
414
567
|
stable_observer: nil
|
|
415
568
|
)
|
|
416
569
|
runtime = Phronomy::Runtime.instance
|
|
417
|
-
|
|
418
|
-
|
|
570
|
+
event_loop = runtime.event_loop
|
|
571
|
+
assert_event_loop!(event_loop)
|
|
572
|
+
session = build_session_for(
|
|
573
|
+
execution: execution,
|
|
574
|
+
runtime: runtime,
|
|
575
|
+
resume_event: resume_event,
|
|
576
|
+
resume_phase: resume_phase,
|
|
577
|
+
stable_observer: stable_observer
|
|
578
|
+
)
|
|
579
|
+
event_loop.bind_workflow_session(
|
|
580
|
+
execution.workflow_instance_id,
|
|
581
|
+
owner_token: execution.owner_token,
|
|
582
|
+
fsm_session_id: session.id
|
|
419
583
|
)
|
|
420
584
|
|
|
585
|
+
source_task = Phronomy::Task.deferred(name: "workflow-source:#{session.id}")
|
|
421
586
|
source_task.on_complete do |result, error|
|
|
422
587
|
finalize_execution(
|
|
423
588
|
execution: execution,
|
|
@@ -426,99 +591,110 @@ module Phronomy
|
|
|
426
591
|
error: error
|
|
427
592
|
)
|
|
428
593
|
end
|
|
429
|
-
|
|
430
|
-
session = build_session_for(
|
|
431
|
-
execution: execution,
|
|
432
|
-
runtime: runtime,
|
|
433
|
-
resume_event: resume_event,
|
|
434
|
-
resume_phase: resume_phase,
|
|
435
|
-
stable_observer: stable_observer
|
|
436
|
-
)
|
|
437
|
-
runtime.event_loop.register(session, completion: source_task)
|
|
594
|
+
event_loop.register(session, completion: source_task)
|
|
438
595
|
result_task
|
|
439
596
|
rescue => error
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
597
|
+
if event_loop&.current?
|
|
598
|
+
event_loop&.release_workflow(
|
|
599
|
+
execution.workflow_instance_id,
|
|
600
|
+
owner_token: execution.owner_token
|
|
601
|
+
)
|
|
602
|
+
end
|
|
603
|
+
raise error
|
|
446
604
|
end
|
|
447
605
|
|
|
448
|
-
# Called from source_task completion on EventLoop
|
|
606
|
+
# Called from source_task completion on EventLoop, after the FSMSession has
|
|
607
|
+
# accepted the durable terminal result (when a durable barrier is required).
|
|
449
608
|
def finalize_execution(execution:, result_task:, result:, error:)
|
|
450
609
|
event_loop = Phronomy::Runtime.instance.event_loop
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
unless execution.repository && execution.persist
|
|
461
|
-
event_loop.release_workflow(
|
|
462
|
-
execution.thread_id,
|
|
463
|
-
owner_fsm_session_id: execution.fsm_session_id
|
|
464
|
-
)
|
|
465
|
-
complete_task(result_task, result)
|
|
466
|
-
return
|
|
467
|
-
end
|
|
610
|
+
assert_event_loop!(event_loop)
|
|
611
|
+
event_loop.release_workflow(
|
|
612
|
+
execution.workflow_instance_id,
|
|
613
|
+
owner_token: execution.owner_token
|
|
614
|
+
)
|
|
615
|
+
error ? fail_task(result_task, error) : complete_task(result_task, result)
|
|
616
|
+
end
|
|
468
617
|
|
|
469
|
-
|
|
618
|
+
def begin_terminal_persistence_on_event_loop(
|
|
619
|
+
execution,
|
|
620
|
+
terminal_type:,
|
|
621
|
+
context:,
|
|
622
|
+
event_sink:
|
|
623
|
+
)
|
|
470
624
|
runtime = Phronomy::Runtime.instance
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
625
|
+
event_loop = runtime.event_loop
|
|
626
|
+
assert_event_loop!(event_loop)
|
|
627
|
+
event_loop.mark_workflow_admission(
|
|
628
|
+
execution.workflow_instance_id,
|
|
629
|
+
owner_token: execution.owner_token,
|
|
630
|
+
state: :persisting_terminal
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
operation = WorkflowTerminalPersistenceCommand.new(
|
|
634
|
+
repository: execution.repository,
|
|
635
|
+
workflow_instance_id: execution.workflow_instance_id.to_s.freeze,
|
|
636
|
+
expected_revision: execution.expected_revision,
|
|
637
|
+
snapshot: deep_immutable_copy(snapshot_for(context))
|
|
638
|
+
)
|
|
639
|
+
task = runtime.offload.submit(on_full: :raise) do
|
|
640
|
+
revision = operation.repository.save(
|
|
641
|
+
operation.workflow_instance_id,
|
|
642
|
+
expected_revision: operation.expected_revision,
|
|
643
|
+
snapshot: operation.snapshot
|
|
476
644
|
)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
result_task,
|
|
482
|
-
result,
|
|
483
|
-
persistence_error,
|
|
484
|
-
execution.thread_id,
|
|
485
|
-
execution.fsm_session_id
|
|
645
|
+
WorkflowTerminalPersistenceResult.new(
|
|
646
|
+
outcome: :success,
|
|
647
|
+
revision: revision,
|
|
648
|
+
error: nil
|
|
486
649
|
)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
650
|
+
rescue Phronomy::Persistence::ConflictError,
|
|
651
|
+
Phronomy::Persistence::NotFoundError,
|
|
652
|
+
Phronomy::Persistence::SerializationError,
|
|
653
|
+
Phronomy::Persistence::UnsupportedBackendError => error
|
|
654
|
+
WorkflowTerminalPersistenceResult.new(
|
|
655
|
+
outcome: :known_failure,
|
|
656
|
+
revision: nil,
|
|
657
|
+
error: error
|
|
658
|
+
)
|
|
659
|
+
rescue => error
|
|
660
|
+
# The Backend SPI does not currently provide a portable commit-outcome
|
|
661
|
+
# classifier for arbitrary storage/transport failures. If non-commit is
|
|
662
|
+
# not guaranteed by the Phronomy error contract, fail closed as F1.
|
|
663
|
+
WorkflowTerminalPersistenceResult.new(
|
|
664
|
+
outcome: :outcome_unknown,
|
|
665
|
+
revision: nil,
|
|
666
|
+
error: error
|
|
493
667
|
)
|
|
494
|
-
settle_persistence_without_event_loop(command, event_loop) unless posted
|
|
495
668
|
end
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
669
|
+
task.on_complete do |result, operation_error|
|
|
670
|
+
delivery = if operation_error
|
|
671
|
+
WorkflowTerminalPersistenceResult.new(
|
|
672
|
+
outcome: :outcome_unknown,
|
|
673
|
+
revision: nil,
|
|
674
|
+
error: operation_error
|
|
675
|
+
)
|
|
676
|
+
else
|
|
677
|
+
result
|
|
678
|
+
end
|
|
679
|
+
accepted = event_sink.post(:workflow_terminal_persistence_result, delivery)
|
|
680
|
+
unless accepted
|
|
681
|
+
Phronomy.configuration.logger&.warn(
|
|
682
|
+
"[Phronomy] EventLoop rejected Workflow terminal persistence result " \
|
|
683
|
+
"for #{execution.workflow_instance_id.inspect}"
|
|
684
|
+
)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
terminal_type
|
|
514
688
|
end
|
|
515
689
|
|
|
516
|
-
def release_and_fail(
|
|
690
|
+
def release_and_fail(request, error)
|
|
691
|
+
event_loop = Phronomy::Runtime.instance.event_loop
|
|
692
|
+
assert_event_loop!(event_loop)
|
|
517
693
|
event_loop.release_workflow(
|
|
518
|
-
|
|
519
|
-
|
|
694
|
+
request.workflow_instance_id,
|
|
695
|
+
owner_token: request.owner_token
|
|
520
696
|
)
|
|
521
|
-
fail_task(result_task, error)
|
|
697
|
+
fail_task(request.result_task, error)
|
|
522
698
|
end
|
|
523
699
|
|
|
524
700
|
def snapshot_for(context)
|
|
@@ -535,10 +711,22 @@ module Phronomy
|
|
|
535
711
|
resume_phase: nil,
|
|
536
712
|
stable_observer: nil
|
|
537
713
|
)
|
|
714
|
+
terminal_barrier = if execution.repository && execution.persist
|
|
715
|
+
->(terminal_type:, context:, event_sink:) {
|
|
716
|
+
begin_terminal_persistence_on_event_loop(
|
|
717
|
+
execution,
|
|
718
|
+
terminal_type: terminal_type,
|
|
719
|
+
context: context,
|
|
720
|
+
event_sink: event_sink
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
end
|
|
724
|
+
|
|
538
725
|
Phronomy::FSMSession.new(
|
|
539
|
-
id: execution.fsm_session_id,
|
|
540
|
-
graph_thread_id: execution.thread_id,
|
|
541
726
|
context: execution.context,
|
|
727
|
+
context_metadata: {
|
|
728
|
+
workflow_instance_id: execution.workflow_instance_id
|
|
729
|
+
},
|
|
542
730
|
entry_point: @entry_point,
|
|
543
731
|
entry_actions: @entry_actions,
|
|
544
732
|
auto_state_set: @auto_state_set,
|
|
@@ -550,7 +738,8 @@ module Phronomy
|
|
|
550
738
|
event_loop: runtime.event_loop,
|
|
551
739
|
resume_event: resume_event,
|
|
552
740
|
resume_phase: resume_phase,
|
|
553
|
-
stable_observer: stable_observer
|
|
741
|
+
stable_observer: stable_observer,
|
|
742
|
+
terminal_barrier: terminal_barrier
|
|
554
743
|
)
|
|
555
744
|
end
|
|
556
745
|
|
|
@@ -576,6 +765,29 @@ module Phronomy
|
|
|
576
765
|
"No external event registered for state #{current_phase.inspect}"
|
|
577
766
|
end
|
|
578
767
|
|
|
768
|
+
def post_control(runtime, command)
|
|
769
|
+
runtime.event_loop.post(
|
|
770
|
+
Phronomy::Event.new(
|
|
771
|
+
type: :workflow_control,
|
|
772
|
+
target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
|
|
773
|
+
payload: {command: command}
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def runtime_rejected_error(action)
|
|
779
|
+
Phronomy::RuntimeShutdownError.new(
|
|
780
|
+
"Runtime rejected Workflow #{action} while shutting down"
|
|
781
|
+
)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def assert_event_loop!(event_loop)
|
|
785
|
+
return if event_loop.current?
|
|
786
|
+
|
|
787
|
+
raise Phronomy::Error,
|
|
788
|
+
"Workflow live-state progression must run on EventLoop"
|
|
789
|
+
end
|
|
790
|
+
|
|
579
791
|
def complete_task(task, value)
|
|
580
792
|
task.complete(value)
|
|
581
793
|
end
|