phronomy 0.18.0 → 0.19.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/CHANGELOG.md +25 -0
- data/README.md +2 -0
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +7 -1
- data/docs/getting-started.md +37 -1
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +93 -2
- data/lib/phronomy/agent/base.rb +81 -36
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +420 -249
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/configuration.rb +2 -1
- data/lib/phronomy/engine/event_loop.rb +86 -8
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime.rb +7 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +361 -95
- data/lib/phronomy.rb +3 -0
- metadata +4 -4
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
|
@@ -10,9 +10,13 @@ module Phronomy
|
|
|
10
10
|
].freeze
|
|
11
11
|
KNOWLEDGE_RESET_KINDS = %i[knowledge_cleared context_reset].freeze
|
|
12
12
|
|
|
13
|
-
def initialize(
|
|
13
|
+
def initialize(agent_root:, persistence: nil, records: nil)
|
|
14
14
|
@persistence = persistence
|
|
15
15
|
@agent_root = agent_root
|
|
16
|
+
@records = records&.dup&.freeze
|
|
17
|
+
if @records.nil? && @persistence.nil?
|
|
18
|
+
raise ArgumentError, "JournalProjection requires persistence: or records:"
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def transcript_records
|
|
@@ -16,7 +16,7 @@ module Phronomy
|
|
|
16
16
|
attr_accessor :trace_pii
|
|
17
17
|
attr_accessor :logger
|
|
18
18
|
attr_accessor :event_loop_stop_grace_seconds
|
|
19
|
-
attr_accessor :
|
|
19
|
+
attr_accessor :persistence
|
|
20
20
|
attr_accessor :tool_result_max_size
|
|
21
21
|
attr_accessor :llm_adapter
|
|
22
22
|
attr_accessor :event_loop_starvation_threshold_seconds
|
|
@@ -53,6 +53,7 @@ module Phronomy
|
|
|
53
53
|
@authorization_pool_size = 4
|
|
54
54
|
@authorization_queue_size = 100
|
|
55
55
|
@authorization_timeout = 5
|
|
56
|
+
@persistence = nil
|
|
56
57
|
end
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -29,6 +29,7 @@ module Phronomy
|
|
|
29
29
|
@fsms = {}
|
|
30
30
|
@waiting = {}
|
|
31
31
|
@admitted_session_ids = Set.new
|
|
32
|
+
@workflow_admissions = {}
|
|
32
33
|
|
|
33
34
|
@lifecycle_mutex = Mutex.new
|
|
34
35
|
@idle_cond = ConditionVariable.new
|
|
@@ -99,7 +100,7 @@ module Phronomy
|
|
|
99
100
|
rescue
|
|
100
101
|
@admitted_session_ids.delete(fsm_session.id)
|
|
101
102
|
@outstanding_sessions -= 1
|
|
102
|
-
@idle_cond.broadcast if
|
|
103
|
+
@idle_cond.broadcast if runtime_idle_locked?
|
|
103
104
|
raise
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -152,6 +153,73 @@ module Phronomy
|
|
|
152
153
|
true
|
|
153
154
|
end
|
|
154
155
|
|
|
156
|
+
# Reserves one logical Workflow thread for one concrete FSMSession execution.
|
|
157
|
+
# thread_id is durable Workflow identity; owner_fsm_session_id is the
|
|
158
|
+
# Runtime-only identity of the invocation/resume that currently owns it.
|
|
159
|
+
def admit_workflow(thread_id, owner_fsm_session_id:)
|
|
160
|
+
key = thread_id.to_s
|
|
161
|
+
owner = owner_fsm_session_id.to_s
|
|
162
|
+
raise ArgumentError, "thread_id must not be empty" if key.empty?
|
|
163
|
+
raise ArgumentError, "owner_fsm_session_id must not be empty" if owner.empty?
|
|
164
|
+
|
|
165
|
+
@lifecycle_mutex.synchronize do
|
|
166
|
+
ensure_accepting_registrations!
|
|
167
|
+
current_owner = @workflow_admissions[key]
|
|
168
|
+
if current_owner
|
|
169
|
+
raise Phronomy::Error,
|
|
170
|
+
"Workflow thread #{key.inspect} is already owned by " \
|
|
171
|
+
"FSMSession #{current_owner.inspect}"
|
|
172
|
+
end
|
|
173
|
+
@workflow_admissions[key] = owner
|
|
174
|
+
end
|
|
175
|
+
true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Releases a Workflow reservation only when the caller is its current owner.
|
|
179
|
+
# A failed competing admission can therefore never release another session's
|
|
180
|
+
# reservation during cleanup.
|
|
181
|
+
def release_workflow(thread_id, owner_fsm_session_id:)
|
|
182
|
+
key = thread_id.to_s
|
|
183
|
+
owner = owner_fsm_session_id.to_s
|
|
184
|
+
@lifecycle_mutex.synchronize do
|
|
185
|
+
next false unless @workflow_admissions[key] == owner
|
|
186
|
+
|
|
187
|
+
@workflow_admissions.delete(key)
|
|
188
|
+
@idle_cond.broadcast if runtime_idle_locked?
|
|
189
|
+
true
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def workflow_admission_owner(thread_id)
|
|
194
|
+
@lifecycle_mutex.synchronize { @workflow_admissions[thread_id.to_s] }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Resolves durable Workflow identity to the currently owning FSMSession and
|
|
198
|
+
# enqueues the event atomically with that ownership check.
|
|
199
|
+
def post_to_workflow(thread_id:, event:, payload: nil)
|
|
200
|
+
queued_depth = nil
|
|
201
|
+
posted_event = nil
|
|
202
|
+
accepted = @lifecycle_mutex.synchronize do
|
|
203
|
+
next false unless accepting_events?
|
|
204
|
+
|
|
205
|
+
owner = @workflow_admissions[thread_id.to_s]
|
|
206
|
+
next false unless owner
|
|
207
|
+
next false unless @admitted_session_ids.include?(owner)
|
|
208
|
+
|
|
209
|
+
posted_event = Phronomy::Event.new(
|
|
210
|
+
type: event.to_sym,
|
|
211
|
+
target_id: owner,
|
|
212
|
+
payload: payload
|
|
213
|
+
)
|
|
214
|
+
queued_depth = enqueue([posted_event, monotonic_nanoseconds])
|
|
215
|
+
true
|
|
216
|
+
end
|
|
217
|
+
return false unless accepted
|
|
218
|
+
|
|
219
|
+
check_queue_backlog(queued_depth, posted_event)
|
|
220
|
+
true
|
|
221
|
+
end
|
|
222
|
+
|
|
155
223
|
# Interrupts the queue wait so EventLoop can recompute the next timer deadline.
|
|
156
224
|
def wake
|
|
157
225
|
@queue.push(WAKE)
|
|
@@ -180,12 +248,12 @@ module Phronomy
|
|
|
180
248
|
end
|
|
181
249
|
|
|
182
250
|
def idle?
|
|
183
|
-
@lifecycle_mutex.synchronize {
|
|
251
|
+
@lifecycle_mutex.synchronize { runtime_idle_locked? }
|
|
184
252
|
end
|
|
185
253
|
|
|
186
254
|
def wait_until_idle(deadline)
|
|
187
255
|
@lifecycle_mutex.synchronize do
|
|
188
|
-
until
|
|
256
|
+
until runtime_idle_locked?
|
|
189
257
|
remaining = deadline - monotonic_now
|
|
190
258
|
return false if remaining <= 0
|
|
191
259
|
@idle_cond.wait(@lifecycle_mutex, remaining)
|
|
@@ -287,7 +355,9 @@ module Phronomy
|
|
|
287
355
|
session_id = event.payload.fetch(:session_id)
|
|
288
356
|
session = @fsms.delete(session_id)
|
|
289
357
|
waiter = @waiting.delete(session_id)
|
|
290
|
-
# decrement before waking caller so wait_until_idle sees
|
|
358
|
+
# decrement before waking caller so wait_until_idle sees the control-plane
|
|
359
|
+
# session count immediately; Workflow durable admission may intentionally
|
|
360
|
+
# keep Runtime non-idle until its terminal save completes.
|
|
291
361
|
decrement_outstanding if session
|
|
292
362
|
complete_waiter(waiter, event.payload.fetch(:result))
|
|
293
363
|
when :start
|
|
@@ -297,10 +367,11 @@ module Phronomy
|
|
|
297
367
|
@waiting[session.id] = waiter if waiter
|
|
298
368
|
session.start
|
|
299
369
|
when :agent_terminal_ready
|
|
300
|
-
# Existing Agent terminalisation is retained in this proposal. A separate
|
|
301
|
-
# follow-up may move it to an Agent-owned terminalising FSM state.
|
|
302
370
|
cmd = event.payload.fetch(:command)
|
|
303
371
|
cmd.coordinator.deliver_on_event_loop(cmd)
|
|
372
|
+
when :workflow_persistence_ready
|
|
373
|
+
cmd = event.payload.fetch(:command)
|
|
374
|
+
cmd.runner.deliver_persistence_on_event_loop(cmd)
|
|
304
375
|
end
|
|
305
376
|
end
|
|
306
377
|
|
|
@@ -314,7 +385,7 @@ module Phronomy
|
|
|
314
385
|
def begin_stopping_if_idle
|
|
315
386
|
@lifecycle_mutex.synchronize do
|
|
316
387
|
return false unless @state == :draining
|
|
317
|
-
return false unless
|
|
388
|
+
return false unless runtime_idle_locked?
|
|
318
389
|
|
|
319
390
|
@state = :stopping
|
|
320
391
|
@queue.push(STOP)
|
|
@@ -337,6 +408,7 @@ module Phronomy
|
|
|
337
408
|
@fsms.clear
|
|
338
409
|
@lifecycle_mutex.synchronize do
|
|
339
410
|
@admitted_session_ids.clear
|
|
411
|
+
@workflow_admissions.clear
|
|
340
412
|
@outstanding_sessions = 0
|
|
341
413
|
@idle_cond.broadcast
|
|
342
414
|
end
|
|
@@ -356,6 +428,7 @@ module Phronomy
|
|
|
356
428
|
@lifecycle_mutex.synchronize do
|
|
357
429
|
@state = :failed
|
|
358
430
|
@admitted_session_ids.clear
|
|
431
|
+
@workflow_admissions.clear
|
|
359
432
|
@idle_cond.broadcast
|
|
360
433
|
end
|
|
361
434
|
cleanup_abandoned_work(error)
|
|
@@ -375,10 +448,14 @@ module Phronomy
|
|
|
375
448
|
def decrement_outstanding
|
|
376
449
|
@lifecycle_mutex.synchronize do
|
|
377
450
|
@outstanding_sessions -= 1 if @outstanding_sessions.positive?
|
|
378
|
-
@idle_cond.broadcast if
|
|
451
|
+
@idle_cond.broadcast if runtime_idle_locked?
|
|
379
452
|
end
|
|
380
453
|
end
|
|
381
454
|
|
|
455
|
+
def runtime_idle_locked?
|
|
456
|
+
@outstanding_sessions.zero? && @workflow_admissions.empty?
|
|
457
|
+
end
|
|
458
|
+
|
|
382
459
|
def join_until(deadline)
|
|
383
460
|
remaining = deadline - monotonic_now
|
|
384
461
|
return if remaining <= 0
|
|
@@ -391,6 +468,7 @@ module Phronomy
|
|
|
391
468
|
@lifecycle_mutex.synchronize do
|
|
392
469
|
@state = :terminated
|
|
393
470
|
@admitted_session_ids.clear
|
|
471
|
+
@workflow_admissions.clear
|
|
394
472
|
@thread = nil unless @thread&.alive?
|
|
395
473
|
@idle_cond.broadcast
|
|
396
474
|
end
|
|
@@ -25,9 +25,11 @@ module Phronomy
|
|
|
25
25
|
event_loop:,
|
|
26
26
|
resume_event: nil,
|
|
27
27
|
resume_phase: nil,
|
|
28
|
-
stable_observer: nil
|
|
28
|
+
stable_observer: nil,
|
|
29
|
+
graph_thread_id: nil
|
|
29
30
|
)
|
|
30
31
|
@id = id
|
|
32
|
+
@graph_thread_id = graph_thread_id || id
|
|
31
33
|
@ctx = context
|
|
32
34
|
@context = context
|
|
33
35
|
@entry_point = entry_point
|
|
@@ -217,7 +219,7 @@ module Phronomy
|
|
|
217
219
|
return if @done
|
|
218
220
|
|
|
219
221
|
@done = true
|
|
220
|
-
@ctx.set_graph_metadata(thread_id: @
|
|
222
|
+
@ctx.set_graph_metadata(thread_id: @graph_thread_id, phase: :__end__)
|
|
221
223
|
post_terminal_event(:finished, @ctx)
|
|
222
224
|
end
|
|
223
225
|
|
|
@@ -225,7 +227,7 @@ module Phronomy
|
|
|
225
227
|
return if @done
|
|
226
228
|
|
|
227
229
|
@done = true
|
|
228
|
-
@ctx.set_graph_metadata(thread_id: @
|
|
230
|
+
@ctx.set_graph_metadata(thread_id: @graph_thread_id, phase: @current_state)
|
|
229
231
|
post_terminal_event(:halted, @ctx)
|
|
230
232
|
end
|
|
231
233
|
|
|
@@ -248,7 +250,7 @@ module Phronomy
|
|
|
248
250
|
|
|
249
251
|
Phronomy.configuration.logger&.warn(
|
|
250
252
|
"[Phronomy::FSMSession] EventLoop rejected terminal event " \
|
|
251
|
-
|
|
253
|
+
"#{type.inspect} for #{@id}"
|
|
252
254
|
)
|
|
253
255
|
end
|
|
254
256
|
|
|
@@ -69,6 +69,7 @@ module Phronomy
|
|
|
69
69
|
@pool_registry = Phronomy::Concurrency::PoolRegistry.new(
|
|
70
70
|
timer_queue_provider: -> { timer_queue }
|
|
71
71
|
)
|
|
72
|
+
@agent_activations = Phronomy::Agent::ActivationRegistry.new
|
|
72
73
|
@lifecycle_mutex = Mutex.new
|
|
73
74
|
@shutdown_mutex = Mutex.new
|
|
74
75
|
@state = :running
|
|
@@ -107,6 +108,12 @@ module Phronomy
|
|
|
107
108
|
@timer_service.timer_queue
|
|
108
109
|
end
|
|
109
110
|
|
|
111
|
+
# Process-local live Agent executions. Activations are transient runtime
|
|
112
|
+
# state and deliberately do not belong to Persistence.
|
|
113
|
+
def __agent_activations
|
|
114
|
+
@agent_activations
|
|
115
|
+
end
|
|
116
|
+
|
|
110
117
|
def event_loop
|
|
111
118
|
@lifecycle_mutex.synchronize do
|
|
112
119
|
case @state
|
|
@@ -66,7 +66,7 @@ module Phronomy
|
|
|
66
66
|
|
|
67
67
|
def save(agent_id, expected_revision:, root:)
|
|
68
68
|
@owner.synchronize do
|
|
69
|
-
current =
|
|
69
|
+
current = @owner.state[:agents].fetch(agent_id.to_s) { raise NotFoundError, "agent not found: #{agent_id}" }
|
|
70
70
|
unless current.agent_revision == expected_revision
|
|
71
71
|
raise ConflictError,
|
|
72
72
|
"agent revision conflict: expected #{expected_revision}, actual #{current.agent_revision}"
|
|
@@ -162,7 +162,7 @@ module Phronomy
|
|
|
162
162
|
|
|
163
163
|
def save(execution_id, expected_revision:, execution:)
|
|
164
164
|
@owner.synchronize do
|
|
165
|
-
current =
|
|
165
|
+
current = @owner.state[:executions].fetch(execution_id.to_s) { raise NotFoundError, "execution not found: #{execution_id}" }
|
|
166
166
|
unless current.execution_revision == expected_revision
|
|
167
167
|
raise ConflictError,
|
|
168
168
|
"execution revision conflict: expected #{expected_revision}, actual #{current.execution_revision}"
|
|
@@ -209,31 +209,112 @@ module Phronomy
|
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
class WorkflowStates
|
|
213
|
+
def initialize(owner) = @owner = owner
|
|
214
|
+
|
|
215
|
+
def load(thread_id)
|
|
216
|
+
@owner.synchronize do
|
|
217
|
+
record = @owner.workflow_state_data[thread_id.to_s]
|
|
218
|
+
next nil unless record
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
snapshot: @owner.deep_dup_workflow_value(record.fetch(:snapshot)),
|
|
222
|
+
revision: record.fetch(:revision)
|
|
223
|
+
}.freeze
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def save(thread_id, expected_revision:, snapshot:)
|
|
228
|
+
@owner.synchronize do
|
|
229
|
+
key = thread_id.to_s
|
|
230
|
+
current = @owner.workflow_state_data[key]
|
|
231
|
+
actual_revision = current&.fetch(:revision)
|
|
232
|
+
unless actual_revision == expected_revision
|
|
233
|
+
raise ConflictError,
|
|
234
|
+
"workflow state revision conflict for #{key}: " \
|
|
235
|
+
"expected #{expected_revision.inspect}, actual #{actual_revision.inspect}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
next_revision = actual_revision ? actual_revision + 1 : 1
|
|
239
|
+
@owner.workflow_state_data[key] = {
|
|
240
|
+
snapshot: @owner.deep_dup_workflow_value(snapshot),
|
|
241
|
+
revision: next_revision
|
|
242
|
+
}
|
|
243
|
+
next_revision
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def delete(thread_id, expected_revision:)
|
|
248
|
+
@owner.synchronize do
|
|
249
|
+
key = thread_id.to_s
|
|
250
|
+
current = @owner.workflow_state_data[key]
|
|
251
|
+
actual_revision = current&.fetch(:revision)
|
|
252
|
+
unless actual_revision == expected_revision
|
|
253
|
+
raise ConflictError,
|
|
254
|
+
"workflow state revision conflict for #{key}: " \
|
|
255
|
+
"expected #{expected_revision.inspect}, actual #{actual_revision.inspect}"
|
|
256
|
+
end
|
|
257
|
+
@owner.workflow_state_data.delete(key)
|
|
258
|
+
end
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
attr_reader :state, :workflow_state_data
|
|
213
264
|
|
|
214
265
|
def initialize
|
|
215
266
|
@monitor = Monitor.new
|
|
216
267
|
@state = {contents: {}, agents: {}, journals: {}, executions: {}}
|
|
268
|
+
@workflow_state_data = {}
|
|
217
269
|
@contents = Contents.new(self)
|
|
218
270
|
@agents = Agents.new(self)
|
|
219
271
|
@journals = Journals.new(self)
|
|
220
272
|
@executions = Executions.new(self)
|
|
221
|
-
@
|
|
222
|
-
super(
|
|
223
|
-
|
|
273
|
+
@workflow_states = WorkflowStates.new(self)
|
|
274
|
+
super(
|
|
275
|
+
contents: @contents,
|
|
276
|
+
agents: @agents,
|
|
277
|
+
journals: @journals,
|
|
278
|
+
executions: @executions,
|
|
279
|
+
workflow_states: @workflow_states
|
|
280
|
+
)
|
|
224
281
|
end
|
|
225
282
|
|
|
226
283
|
def capabilities
|
|
227
284
|
{atomic_all: true, atomic_admission: true, optimistic_revision: true}.freeze
|
|
228
285
|
end
|
|
229
286
|
|
|
287
|
+
def assert_agent_watermark!(agent_id:, agent_revision:, journal_position:)
|
|
288
|
+
synchronize do
|
|
289
|
+
stored = @state[:agents][agent_id.to_s]
|
|
290
|
+
unless stored
|
|
291
|
+
raise NotFoundError, "Agent not found: #{agent_id}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if stored.agent_revision != agent_revision
|
|
295
|
+
raise ConflictError,
|
|
296
|
+
"agent revision conflict: expected #{agent_revision}, actual #{stored.agent_revision}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
actual_position = Array(@state[:journals][agent_id.to_s]).length
|
|
300
|
+
if actual_position != journal_position
|
|
301
|
+
raise ConflictError,
|
|
302
|
+
"journal position conflict: expected #{journal_position}, actual #{actual_position}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
true
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
230
309
|
def transaction
|
|
231
310
|
synchronize do
|
|
232
|
-
|
|
311
|
+
state_snapshot = Marshal.load(Marshal.dump(@state))
|
|
312
|
+
workflow_snapshot = deep_dup_workflow_value(@workflow_state_data)
|
|
233
313
|
begin
|
|
234
314
|
yield self
|
|
235
315
|
rescue
|
|
236
|
-
@state
|
|
316
|
+
@state.replace(state_snapshot)
|
|
317
|
+
@workflow_state_data.replace(workflow_snapshot)
|
|
237
318
|
raise
|
|
238
319
|
end
|
|
239
320
|
end
|
|
@@ -242,6 +323,30 @@ module Phronomy
|
|
|
242
323
|
def synchronize(&block)
|
|
243
324
|
@monitor.synchronize(&block)
|
|
244
325
|
end
|
|
326
|
+
|
|
327
|
+
# Workflow fields historically accepted ordinary Ruby values in the
|
|
328
|
+
# in-memory store. Keep that contract without forcing the Agent durable
|
|
329
|
+
# state Marshal snapshot to serialize arbitrary Workflow values.
|
|
330
|
+
def deep_dup_workflow_value(value)
|
|
331
|
+
case value
|
|
332
|
+
when Hash
|
|
333
|
+
value.each_with_object({}) do |(key, child), result|
|
|
334
|
+
result[deep_dup_workflow_value(key)] = deep_dup_workflow_value(child)
|
|
335
|
+
end
|
|
336
|
+
when Array
|
|
337
|
+
value.map { |child| deep_dup_workflow_value(child) }
|
|
338
|
+
when NilClass, Symbol, Integer, Float, TrueClass, FalseClass
|
|
339
|
+
value
|
|
340
|
+
else
|
|
341
|
+
return value if value.frozen?
|
|
342
|
+
|
|
343
|
+
begin
|
|
344
|
+
value.dup
|
|
345
|
+
rescue TypeError
|
|
346
|
+
value
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
245
350
|
end
|
|
246
351
|
end
|
|
247
352
|
end
|
data/lib/phronomy/persistence.rb
CHANGED
|
@@ -6,14 +6,14 @@ module Phronomy
|
|
|
6
6
|
class NotFoundError < Phronomy::Error; end
|
|
7
7
|
class UnsupportedBackendError < Phronomy::Error; end
|
|
8
8
|
|
|
9
|
-
attr_reader :contents, :agents, :journals, :executions, :
|
|
9
|
+
attr_reader :contents, :agents, :journals, :executions, :workflow_states
|
|
10
10
|
|
|
11
|
-
def initialize(contents:, agents:, journals:, executions:,
|
|
11
|
+
def initialize(contents:, agents:, journals:, executions:, workflow_states:)
|
|
12
12
|
@contents = contents
|
|
13
13
|
@agents = agents
|
|
14
14
|
@journals = journals
|
|
15
15
|
@executions = executions
|
|
16
|
-
@
|
|
16
|
+
@workflow_states = workflow_states
|
|
17
17
|
validate_capabilities!
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -25,6 +25,15 @@ module Phronomy
|
|
|
25
25
|
raise UnsupportedBackendError, "#{self.class} does not provide atomic_all"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Verifies that a live Agent still owns the durable base it hydrated.
|
|
29
|
+
# Backends should implement this as a revision/position precondition check,
|
|
30
|
+
# not as a state reload returned to the caller.
|
|
31
|
+
# @api private
|
|
32
|
+
def assert_agent_watermark!(agent_id:, agent_revision:, journal_position:)
|
|
33
|
+
raise UnsupportedBackendError,
|
|
34
|
+
"#{self.class} does not provide Agent durable-watermark checks"
|
|
35
|
+
end
|
|
36
|
+
|
|
28
37
|
private
|
|
29
38
|
|
|
30
39
|
def validate_capabilities!
|
data/lib/phronomy/version.rb
CHANGED
data/lib/phronomy/workflow.rb
CHANGED
|
@@ -8,8 +8,8 @@ module Phronomy
|
|
|
8
8
|
class Workflow
|
|
9
9
|
include Phronomy::Runnable
|
|
10
10
|
|
|
11
|
-
def self.define(context_class,
|
|
12
|
-
builder = Builder.new(context_class,
|
|
11
|
+
def self.define(context_class, persistence: nil, &block)
|
|
12
|
+
builder = Builder.new(context_class, persistence: persistence)
|
|
13
13
|
builder.instance_eval(&block)
|
|
14
14
|
builder.build
|
|
15
15
|
end
|
|
@@ -41,12 +41,13 @@ module Phronomy
|
|
|
41
41
|
@runner.send_event(state: state, event: event, input: input)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Sends an event to an active Workflow
|
|
44
|
+
# Sends an event to an active Workflow execution without blocking.
|
|
45
45
|
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
46
|
+
# +thread_id+ is the logical/durable Workflow identity. EventLoop resolves it
|
|
47
|
+
# to the currently owning Runtime-only fsm_session_id. InvocationContext's
|
|
48
|
+
# application session_id is unrelated to this routing.
|
|
48
49
|
#
|
|
49
|
-
# @return [Boolean] true when admitted; false when the
|
|
50
|
+
# @return [Boolean] true when admitted; false when the Workflow is not live
|
|
50
51
|
# or Runtime shutdown has begun
|
|
51
52
|
# @api public
|
|
52
53
|
def signal(thread_id:, event:, payload: nil)
|
|
@@ -76,9 +77,9 @@ module Phronomy
|
|
|
76
77
|
class Builder
|
|
77
78
|
FINISH = Phronomy::WorkflowRunner::FINISH
|
|
78
79
|
|
|
79
|
-
def initialize(context_class,
|
|
80
|
+
def initialize(context_class, persistence: nil)
|
|
80
81
|
@context_class = context_class
|
|
81
|
-
@
|
|
82
|
+
@persistence = persistence
|
|
82
83
|
@initial = nil
|
|
83
84
|
@declared_states = []
|
|
84
85
|
@entry_actions = {}
|
|
@@ -167,7 +168,7 @@ module Phronomy
|
|
|
167
168
|
external_events: external_events,
|
|
168
169
|
entry_point: @initial || @declared_states.first,
|
|
169
170
|
wait_state_names: @wait_state_names.dup,
|
|
170
|
-
|
|
171
|
+
persistence: @persistence
|
|
171
172
|
)
|
|
172
173
|
Workflow.new(runner)
|
|
173
174
|
end
|