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.
Files changed (39) hide show
  1. data/lib/dynflow.rb +2 -1
  2. data/lib/dynflow/action.rb +271 -101
  3. data/lib/dynflow/action/format.rb +4 -4
  4. data/lib/dynflow/action/progress.rb +8 -8
  5. data/lib/dynflow/execution_plan.rb +14 -15
  6. data/lib/dynflow/execution_plan/output_reference.rb +56 -23
  7. data/lib/dynflow/execution_plan/steps/abstract.rb +1 -3
  8. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +0 -17
  9. data/lib/dynflow/execution_plan/steps/error.rb +35 -11
  10. data/lib/dynflow/execution_plan/steps/finalize_step.rb +1 -1
  11. data/lib/dynflow/execution_plan/steps/plan_step.rb +8 -3
  12. data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
  13. data/lib/dynflow/listeners/socket.rb +0 -1
  14. data/lib/dynflow/middleware.rb +0 -1
  15. data/lib/dynflow/middleware/world.rb +1 -1
  16. data/lib/dynflow/persistence.rb +2 -5
  17. data/lib/dynflow/serializable.rb +24 -11
  18. data/lib/dynflow/testing/assertions.rb +5 -5
  19. data/lib/dynflow/testing/dummy_execution_plan.rb +1 -1
  20. data/lib/dynflow/testing/dummy_planned_action.rb +2 -1
  21. data/lib/dynflow/testing/dummy_world.rb +4 -0
  22. data/lib/dynflow/testing/factories.rb +18 -10
  23. data/lib/dynflow/version.rb +1 -1
  24. data/lib/dynflow/world.rb +35 -21
  25. data/test/action_test.rb +50 -76
  26. data/test/clock_test.rb +4 -4
  27. data/test/execution_plan_test.rb +1 -1
  28. data/test/executor_test.rb +3 -2
  29. data/test/remote_via_socket_test.rb +11 -9
  30. data/test/support/code_workflow_example.rb +2 -3
  31. data/test/test_helper.rb +1 -1
  32. data/test/testing_test.rb +3 -3
  33. metadata +2 -8
  34. data/lib/dynflow/action/finalize_phase.rb +0 -20
  35. data/lib/dynflow/action/flow_phase.rb +0 -44
  36. data/lib/dynflow/action/plan_phase.rb +0 -87
  37. data/lib/dynflow/action/presenter.rb +0 -51
  38. data/lib/dynflow/action/run_phase.rb +0 -45
  39. data/lib/dynflow/middleware/action.rb +0 -9
@@ -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'
@@ -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
- # Override this to extend the phase classes
31
- def self.phase_modules
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.generate_phase(*modules)
50
- Class.new(self) { modules.each { |m| include m } }
27
+ def self.inherited(child)
28
+ children << child
29
+ super child
51
30
  end
52
31
 
53
- def self.phase?
54
- [PlanPhase, RunPhase, FinalizePhase, Presenter].any? { |phase| self < phase }
32
+ def self.children
33
+ @children ||= []
55
34
  end
56
35
 
57
- def self.all_children
58
- #noinspection RubyArgCount
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
- def self.attr_indifferent_access_hash(*names)
72
- attr_reader(*names)
73
- names.each do |name|
74
- define_method("#{name}=") { |v| indifferent_access_hash_variable_set name, v }
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
- def indifferent_access_hash_variable_set(name, value)
79
- Type! value, Hash
80
- instance_variable_set :"@#{name}", value.with_indifferent_access
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.from_hash(hash, phase, *args)
84
- check_class_key_present hash
85
- raise ArgumentError, "unknown phase '#{phase}'" unless [:plan_phase, :run_phase, :finalize_phase].include? phase
86
- Action.constantize(hash[:class]).send(phase).new_from_hash(hash, *args)
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, :execution_plan_id, :id, :plan_step_id, :run_step_id, :finalize_step_id
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! attributes[:step], ExecutionPlan::Steps::Abstract
98
- @execution_plan_id = attributes[:execution_plan_id] || raise(ArgumentError, 'missing execution_plan_id')
99
- @id = attributes[:id] || raise(ArgumentError, 'missing id')
100
- @plan_step_id = attributes[:plan_step_id]
101
- @run_step_id = attributes[:run_step_id]
102
- @finalize_step_id = attributes[:finalize_step_id]
103
- end
104
-
105
- def self.action_class
106
- # superclass because we run this from the phases of action class
107
- if phase?
108
- superclass
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
- self
127
+ @output
111
128
  end
112
129
  end
113
130
 
114
- def self.constantize(action_name)
115
- action_name.constantize
116
- rescue NameError
117
- Action::Missing.generate(action_name)
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 action_class
125
- self.class.action_class
146
+ def plan_step
147
+ phase! Present
148
+ execution_plan.steps.fetch(plan_step_id)
126
149
  end
127
150
 
128
- def to_hash
129
- recursive_to_hash class: action_class.name,
130
- execution_plan_id: execution_plan_id,
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
- # @api private
138
- # @return [Array<Fixnum>] - ids of steps referenced from action
139
- def required_step_ids(value = self.input)
140
- ret = case value
141
- when Hash
142
- value.values.map { |val| required_step_ids(val) }
143
- when Array
144
- value.map { |val| required_step_ids(val) }
145
- when ExecutionPlan::OutputReference
146
- value.step_id
147
- else
148
- # no reference hidden in this arg
149
- end
150
- return Array(ret).flatten.compact
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
- ERROR = Object.new
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 = if error.is_a?(String)
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 self.inherited(child)
233
- children << child
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 self.children
237
- @children ||= []
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