dynflow 0.3.0 → 0.4.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.
- data/lib/dynflow.rb +2 -1
- data/lib/dynflow/action.rb +271 -101
- data/lib/dynflow/action/format.rb +4 -4
- data/lib/dynflow/action/progress.rb +8 -8
- data/lib/dynflow/execution_plan.rb +14 -15
- data/lib/dynflow/execution_plan/output_reference.rb +56 -23
- data/lib/dynflow/execution_plan/steps/abstract.rb +1 -3
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +0 -17
- data/lib/dynflow/execution_plan/steps/error.rb +35 -11
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +1 -1
- data/lib/dynflow/execution_plan/steps/plan_step.rb +8 -3
- data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
- data/lib/dynflow/listeners/socket.rb +0 -1
- data/lib/dynflow/middleware.rb +0 -1
- data/lib/dynflow/middleware/world.rb +1 -1
- data/lib/dynflow/persistence.rb +2 -5
- data/lib/dynflow/serializable.rb +24 -11
- data/lib/dynflow/testing/assertions.rb +5 -5
- data/lib/dynflow/testing/dummy_execution_plan.rb +1 -1
- data/lib/dynflow/testing/dummy_planned_action.rb +2 -1
- data/lib/dynflow/testing/dummy_world.rb +4 -0
- data/lib/dynflow/testing/factories.rb +18 -10
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +35 -21
- data/test/action_test.rb +50 -76
- data/test/clock_test.rb +4 -4
- data/test/execution_plan_test.rb +1 -1
- data/test/executor_test.rb +3 -2
- data/test/remote_via_socket_test.rb +11 -9
- data/test/support/code_workflow_example.rb +2 -3
- data/test/test_helper.rb +1 -1
- data/test/testing_test.rb +3 -3
- metadata +2 -8
- data/lib/dynflow/action/finalize_phase.rb +0 -20
- data/lib/dynflow/action/flow_phase.rb +0 -44
- data/lib/dynflow/action/plan_phase.rb +0 -87
- data/lib/dynflow/action/presenter.rb +0 -51
- data/lib/dynflow/action/run_phase.rb +0 -45
- data/lib/dynflow/middleware/action.rb +0 -9
data/lib/dynflow.rb
CHANGED
@@ -7,6 +7,7 @@ require 'active_support/core_ext/hash/indifferent_access'
|
|
7
7
|
# TODO validate in/output, also validate unknown keys
|
8
8
|
# TODO performance testing, how many actions will it handle?
|
9
9
|
# TODO profiling, find bottlenecks
|
10
|
+
# FIND change ids to uuid, uuid-<action_id>, uuid-<action_id-(plan, run, finalize)
|
10
11
|
module Dynflow
|
11
12
|
|
12
13
|
class Error < StandardError
|
@@ -20,9 +21,9 @@ module Dynflow
|
|
20
21
|
require 'dynflow/transaction_adapters'
|
21
22
|
require 'dynflow/persistence'
|
22
23
|
require 'dynflow/middleware'
|
23
|
-
require 'dynflow/action'
|
24
24
|
require 'dynflow/flows'
|
25
25
|
require 'dynflow/execution_plan'
|
26
|
+
require 'dynflow/action'
|
26
27
|
require 'dynflow/listeners'
|
27
28
|
require 'dynflow/executors'
|
28
29
|
require 'dynflow/logger_adapters'
|
data/lib/dynflow/action.rb
CHANGED
@@ -1,64 +1,40 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
2
|
|
3
3
|
module Dynflow
|
4
|
-
|
5
|
-
# TODO unify phases into one class, check what can be called in what phase at runtime
|
6
4
|
class Action < Serializable
|
5
|
+
|
6
|
+
OutputReference = ExecutionPlan::OutputReference
|
7
|
+
|
7
8
|
include Algebrick::TypeCheck
|
8
9
|
include Algebrick::Matching
|
9
10
|
|
10
11
|
require 'dynflow/action/format'
|
11
12
|
extend Format
|
12
13
|
|
13
|
-
extend Middleware::Action
|
14
|
-
|
15
14
|
require 'dynflow/action/progress'
|
16
15
|
include Progress
|
17
16
|
|
18
17
|
require 'dynflow/action/suspended'
|
19
18
|
require 'dynflow/action/missing'
|
20
19
|
|
21
|
-
require 'dynflow/action/plan_phase'
|
22
|
-
require 'dynflow/action/flow_phase'
|
23
|
-
require 'dynflow/action/run_phase'
|
24
|
-
require 'dynflow/action/finalize_phase'
|
25
|
-
|
26
|
-
require 'dynflow/action/presenter'
|
27
20
|
require 'dynflow/action/polling'
|
28
21
|
require 'dynflow/action/cancellable_polling'
|
29
22
|
|
30
|
-
|
31
|
-
|
32
|
-
{ plan_phase: [PlanPhase],
|
33
|
-
run_phase: [RunPhase],
|
34
|
-
finalize_phase: [FinalizePhase],
|
35
|
-
presenter: [Presenter] }.freeze
|
36
|
-
end
|
37
|
-
|
38
|
-
phase_modules.each do |phase_name, _|
|
39
|
-
define_singleton_method phase_name do
|
40
|
-
instance_variable_get :"@#{phase_name}" or
|
41
|
-
instance_variable_set :"@#{phase_name}", __send__("create_#{phase_name}")
|
42
|
-
end
|
43
|
-
|
44
|
-
define_singleton_method "create_#{phase_name}" do
|
45
|
-
generate_phase(*phase_modules[phase_name])
|
46
|
-
end
|
23
|
+
def self.all_children
|
24
|
+
children.inject(children) { |children, child| children + child.all_children }
|
47
25
|
end
|
48
26
|
|
49
|
-
def self.
|
50
|
-
|
27
|
+
def self.inherited(child)
|
28
|
+
children << child
|
29
|
+
super child
|
51
30
|
end
|
52
31
|
|
53
|
-
def self.
|
54
|
-
|
32
|
+
def self.children
|
33
|
+
@children ||= []
|
55
34
|
end
|
56
35
|
|
57
|
-
def self.
|
58
|
-
|
59
|
-
children.
|
60
|
-
inject(children) { |children, child| children + child.all_children }.
|
61
|
-
select { |ch| !ch.phase? }
|
36
|
+
def self.middleware
|
37
|
+
@middleware ||= Middleware::Register.new
|
62
38
|
end
|
63
39
|
|
64
40
|
# FIND define subscriptions in world independent on action's classes,
|
@@ -68,108 +44,190 @@ module Dynflow
|
|
68
44
|
nil
|
69
45
|
end
|
70
46
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
47
|
+
ERROR = Object.new
|
48
|
+
SUSPEND = Object.new
|
49
|
+
Phase = Algebrick.type do
|
50
|
+
Executable = type do
|
51
|
+
variants Plan = atom,
|
52
|
+
Run = atom,
|
53
|
+
Finalize = atom
|
75
54
|
end
|
55
|
+
variants Executable, Present = atom
|
76
56
|
end
|
77
57
|
|
78
|
-
|
79
|
-
|
80
|
-
|
58
|
+
module Executable
|
59
|
+
def execute_method_name
|
60
|
+
match self,
|
61
|
+
(on Plan, :execute_plan),
|
62
|
+
(on Run, :execute_run),
|
63
|
+
(on Finalize, :execute_finalize)
|
64
|
+
end
|
81
65
|
end
|
82
66
|
|
83
|
-
def self.
|
84
|
-
|
85
|
-
|
86
|
-
Action.
|
67
|
+
def self.constantize(action_name)
|
68
|
+
super action_name
|
69
|
+
rescue NameError
|
70
|
+
Action::Missing.generate(action_name)
|
87
71
|
end
|
88
72
|
|
89
|
-
attr_reader :world, :
|
73
|
+
attr_reader :world, :phase, :execution_plan_id, :id, :input,
|
74
|
+
:plan_step_id, :run_step_id, :finalize_step_id
|
90
75
|
|
91
76
|
def initialize(attributes, world)
|
92
|
-
raise "It's not expected to initialize this class directly, use phases." unless self.class.phase?
|
93
|
-
|
94
77
|
Type! attributes, Hash
|
95
78
|
|
79
|
+
@phase = Type! attributes.fetch(:phase), Phase
|
96
80
|
@world = Type! world, World
|
97
|
-
@step = Type!
|
98
|
-
|
99
|
-
@
|
100
|
-
@
|
101
|
-
@
|
102
|
-
@
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
if phase?
|
108
|
-
|
81
|
+
@step = Type!(attributes.fetch(:step),
|
82
|
+
ExecutionPlan::Steps::Abstract) if phase? Executable
|
83
|
+
@execution_plan_id = Type! attributes.fetch(:execution_plan_id), String
|
84
|
+
@id = Type! attributes.fetch(:id), Integer
|
85
|
+
@plan_step_id = Type! attributes.fetch(:plan_step_id), Integer
|
86
|
+
@run_step_id = Type! attributes.fetch(:run_step_id), Integer, NilClass
|
87
|
+
@finalize_step_id = Type! attributes.fetch(:finalize_step_id), Integer, NilClass
|
88
|
+
|
89
|
+
@execution_plan = Type!(attributes.fetch(:execution_plan),
|
90
|
+
ExecutionPlan) if phase? Plan, Present
|
91
|
+
@trigger = Type! attributes.fetch(:trigger), Action, NilClass if phase? Plan
|
92
|
+
|
93
|
+
getter =-> key, required do
|
94
|
+
required ? attributes.fetch(key) : attributes.fetch(key, {})
|
95
|
+
end
|
96
|
+
|
97
|
+
@input = OutputReference.deserialize getter.(:input, phase?(Run, Finalize, Present))
|
98
|
+
@output = OutputReference.deserialize getter.(:output, false) if phase? Run, Finalize, Present
|
99
|
+
end
|
100
|
+
|
101
|
+
def phase?(*phases)
|
102
|
+
Match? phase, *phases
|
103
|
+
end
|
104
|
+
|
105
|
+
def phase!(*phases)
|
106
|
+
phase?(*phases) or
|
107
|
+
raise TypeError, "Wrong phase #{phase}, required #{phases}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def input=(hash)
|
111
|
+
Type! hash, Hash
|
112
|
+
phase! Plan
|
113
|
+
@input = hash.with_indifferent_access
|
114
|
+
end
|
115
|
+
|
116
|
+
def output=(hash)
|
117
|
+
Type! hash, Hash
|
118
|
+
phase! Run
|
119
|
+
@output = hash.with_indifferent_access
|
120
|
+
end
|
121
|
+
|
122
|
+
def output
|
123
|
+
if phase? Plan
|
124
|
+
@output_reference or
|
125
|
+
raise 'plan_self has to be invoked before being able to reference the output'
|
109
126
|
else
|
110
|
-
|
127
|
+
@output
|
111
128
|
end
|
112
129
|
end
|
113
130
|
|
114
|
-
def
|
115
|
-
|
116
|
-
|
117
|
-
|
131
|
+
def trigger
|
132
|
+
phase! Plan
|
133
|
+
@trigger
|
134
|
+
end
|
135
|
+
|
136
|
+
def execution_plan
|
137
|
+
phase! Plan, Present
|
138
|
+
@execution_plan
|
118
139
|
end
|
119
140
|
|
120
141
|
def action_logger
|
142
|
+
phase! Executable
|
121
143
|
world.action_logger
|
122
144
|
end
|
123
145
|
|
124
|
-
def
|
125
|
-
|
146
|
+
def plan_step
|
147
|
+
phase! Present
|
148
|
+
execution_plan.steps.fetch(plan_step_id)
|
126
149
|
end
|
127
150
|
|
128
|
-
def
|
129
|
-
|
130
|
-
|
131
|
-
id: id,
|
132
|
-
plan_step_id: plan_step_id,
|
133
|
-
run_step_id: run_step_id,
|
134
|
-
finalize_step_id: finalize_step_id
|
151
|
+
def run_step
|
152
|
+
phase! Present
|
153
|
+
execution_plan.steps.fetch(run_step_id) if run_step_id
|
135
154
|
end
|
136
155
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
156
|
+
def finalize_step
|
157
|
+
phase! Present
|
158
|
+
execution_plan.steps.fetch(finalize_step_id) if finalize_step_id
|
159
|
+
end
|
160
|
+
|
161
|
+
def steps
|
162
|
+
[plan_step, run_step, finalize_step_id]
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_hash
|
166
|
+
recursive_to_hash(
|
167
|
+
{ class: self.class.name,
|
168
|
+
execution_plan_id: execution_plan_id,
|
169
|
+
id: id,
|
170
|
+
plan_step_id: plan_step_id,
|
171
|
+
run_step_id: run_step_id,
|
172
|
+
finalize_step_id: finalize_step_id,
|
173
|
+
input: input },
|
174
|
+
if phase? Run, Finalize, Present
|
175
|
+
{ output: output }
|
176
|
+
end)
|
151
177
|
end
|
152
178
|
|
153
179
|
def state
|
180
|
+
phase! Executable
|
154
181
|
@step.state
|
155
182
|
end
|
156
183
|
|
157
184
|
def error
|
185
|
+
phase! Executable
|
158
186
|
@step.error
|
159
187
|
end
|
160
188
|
|
189
|
+
def execute(*args)
|
190
|
+
phase! Executable
|
191
|
+
self.send phase.execute_method_name, *args
|
192
|
+
end
|
193
|
+
|
194
|
+
# @api private
|
195
|
+
# @return [Array<Fixnum>] - ids of steps referenced from action
|
196
|
+
def required_step_ids(input = self.input)
|
197
|
+
results = []
|
198
|
+
recursion =-> value do
|
199
|
+
case value
|
200
|
+
when Hash
|
201
|
+
value.values.each { |v| recursion.(v) }
|
202
|
+
when Array
|
203
|
+
value.each { |v| recursion.(v) }
|
204
|
+
when ExecutionPlan::OutputReference
|
205
|
+
results << value.step_id
|
206
|
+
else
|
207
|
+
# no reference hidden in this arg
|
208
|
+
end
|
209
|
+
results
|
210
|
+
end
|
211
|
+
recursion.(input)
|
212
|
+
end
|
213
|
+
|
161
214
|
protected
|
162
215
|
|
163
216
|
def state=(state)
|
217
|
+
phase! Executable
|
164
218
|
@world.logger.debug "step #{execution_plan_id}:#{@step.id} #{self.state} >> #{state}"
|
165
219
|
@step.state = state
|
166
220
|
end
|
167
221
|
|
168
222
|
def save_state
|
223
|
+
phase! Executable
|
169
224
|
@step.save
|
170
225
|
end
|
171
226
|
|
172
|
-
# @override
|
227
|
+
# @override to implement the action's *Plan phase* behaviour.
|
228
|
+
# By default it plans itself and expects input-hash.
|
229
|
+
# Use #plan_self and #plan_action methods to plan actions.
|
230
|
+
# It can use DB in this phase.
|
173
231
|
def plan(*args)
|
174
232
|
if trigger
|
175
233
|
# if the action is triggered by subscription, by default use the
|
@@ -184,16 +242,71 @@ module Dynflow
|
|
184
242
|
self
|
185
243
|
end
|
186
244
|
|
245
|
+
# Add this method to implement the action's *Run phase* behaviour.
|
246
|
+
# It should not use DB in this phase.
|
247
|
+
def run(event = nil)
|
248
|
+
# just a rdoc placeholder
|
249
|
+
end
|
250
|
+
remove_method :run
|
251
|
+
|
252
|
+
# Add this method to implement the action's *Finalize phase* behaviour.
|
253
|
+
# It can use DB in this phase.
|
254
|
+
def finalize
|
255
|
+
# just a rdoc placeholder
|
256
|
+
end
|
257
|
+
remove_method :finalize
|
258
|
+
|
187
259
|
def self.new_from_hash(hash, world)
|
188
260
|
new(hash, world)
|
189
261
|
end
|
190
262
|
|
191
263
|
private
|
192
264
|
|
193
|
-
|
265
|
+
# DSL for plan phase
|
266
|
+
|
267
|
+
def concurrence(&block)
|
268
|
+
phase! Plan
|
269
|
+
@execution_plan.switch_flow(Flows::Concurrence.new([]), &block)
|
270
|
+
end
|
271
|
+
|
272
|
+
def sequence(&block)
|
273
|
+
phase! Plan
|
274
|
+
@execution_plan.switch_flow(Flows::Sequence.new([]), &block)
|
275
|
+
end
|
276
|
+
|
277
|
+
def plan_self(input)
|
278
|
+
phase! Plan
|
279
|
+
self.input = input
|
280
|
+
if self.respond_to?(:run)
|
281
|
+
run_step = @execution_plan.add_run_step(self)
|
282
|
+
@run_step_id = run_step.id
|
283
|
+
@output_reference = OutputReference.new(@execution_plan.id, run_step.id, id)
|
284
|
+
end
|
285
|
+
|
286
|
+
if self.respond_to?(:finalize)
|
287
|
+
finalize_step = @execution_plan.add_finalize_step(self)
|
288
|
+
@finalize_step_id = finalize_step.id
|
289
|
+
end
|
290
|
+
|
291
|
+
return self # to stay consistent with plan_action
|
292
|
+
end
|
293
|
+
|
294
|
+
def plan_action(action_class, *args)
|
295
|
+
phase! Plan
|
296
|
+
@execution_plan.add_plan_step(action_class, self).execute(@execution_plan, nil, *args)
|
297
|
+
end
|
298
|
+
|
299
|
+
# DSL for run phase
|
300
|
+
|
301
|
+
def suspend(&block)
|
302
|
+
phase! Run
|
303
|
+
block.call Action::Suspended.new self if block
|
304
|
+
throw SUSPEND, SUSPEND
|
305
|
+
end
|
194
306
|
|
195
307
|
# DSL to terminate action execution and set it to error
|
196
308
|
def error!(error)
|
309
|
+
phase! Executable
|
197
310
|
set_error(error)
|
198
311
|
throw ERROR
|
199
312
|
end
|
@@ -219,22 +332,79 @@ module Dynflow
|
|
219
332
|
end
|
220
333
|
|
221
334
|
def set_error(error)
|
335
|
+
phase! Executable
|
222
336
|
Type! error, Exception, String
|
223
337
|
action_logger.error error
|
224
338
|
self.state = :error
|
225
|
-
@step.error =
|
226
|
-
ExecutionPlan::Steps::Error.new(nil, error, nil)
|
227
|
-
else
|
228
|
-
ExecutionPlan::Steps::Error.new(error.class.name, error.message, error.backtrace)
|
229
|
-
end
|
339
|
+
@step.error = ExecutionPlan::Steps::Error.new(error)
|
230
340
|
end
|
231
341
|
|
232
|
-
def
|
233
|
-
|
342
|
+
def execute_plan(*args)
|
343
|
+
phase! Plan
|
344
|
+
self.state = :running
|
345
|
+
save_state
|
346
|
+
with_error_handling do
|
347
|
+
concurrence do
|
348
|
+
world.middleware.execute(:plan, self, *args) do |*new_args|
|
349
|
+
plan(*new_args)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
subscribed_actions = world.subscribed_actions(self.class)
|
354
|
+
if subscribed_actions.any?
|
355
|
+
# we encapsulate the flow for this action into a concurrence and
|
356
|
+
# add the subscribed flows to it as well.
|
357
|
+
trigger_flow = @execution_plan.current_run_flow.sub_flows.pop
|
358
|
+
@execution_plan.switch_flow(Flows::Concurrence.new([trigger_flow].compact)) do
|
359
|
+
subscribed_actions.each do |action_class|
|
360
|
+
new_plan_step = @execution_plan.add_plan_step(action_class, self)
|
361
|
+
new_plan_step.execute(@execution_plan, self, *args)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
234
366
|
end
|
235
367
|
|
236
|
-
def
|
237
|
-
|
368
|
+
def execute_run(event)
|
369
|
+
phase! Run
|
370
|
+
@world.logger.debug "step #{execution_plan_id}:#{@step.id} got event #{event}" if event
|
371
|
+
@input = OutputReference.dereference @input, world.persistence
|
372
|
+
|
373
|
+
case
|
374
|
+
when state == :running
|
375
|
+
raise NotImplementedError, 'recovery after restart is not implemented'
|
376
|
+
|
377
|
+
when [:pending, :error, :suspended].include?(state)
|
378
|
+
if [:pending, :error].include?(state) && event
|
379
|
+
raise 'event can be processed only when in suspended state'
|
380
|
+
end
|
381
|
+
|
382
|
+
self.state = :running
|
383
|
+
save_state
|
384
|
+
with_error_handling do
|
385
|
+
result = catch(SUSPEND) do
|
386
|
+
world.middleware.execute(:run, self, *[event].compact) { |*args| run(*args) }
|
387
|
+
end
|
388
|
+
if result == SUSPEND
|
389
|
+
self.state = :suspended
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
else
|
394
|
+
raise "wrong state #{state} when event:#{event}"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def execute_finalize
|
399
|
+
phase! Finalize
|
400
|
+
@input = OutputReference.dereference @input, world.persistence
|
401
|
+
self.state = :running
|
402
|
+
save_state
|
403
|
+
with_error_handling do
|
404
|
+
world.middleware.execute(:finalize, self) do
|
405
|
+
finalize
|
406
|
+
end
|
407
|
+
end
|
238
408
|
end
|
239
409
|
end
|
240
410
|
end
|