dynflow 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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