dynflow 0.8.16 → 0.8.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/.rubocop_todo.yml +0 -25
  4. data/doc/pages/plugins/div_tag.rb +1 -1
  5. data/doc/pages/plugins/tags.rb +0 -1
  6. data/examples/orchestrate.rb +0 -1
  7. data/examples/remote_executor.rb +3 -3
  8. data/examples/sub_plan_concurrency_control.rb +0 -1
  9. data/examples/sub_plans.rb +0 -1
  10. data/lib/dynflow.rb +1 -0
  11. data/lib/dynflow/action.rb +6 -6
  12. data/lib/dynflow/config.rb +2 -2
  13. data/lib/dynflow/connectors/database.rb +1 -1
  14. data/lib/dynflow/connectors/direct.rb +1 -1
  15. data/lib/dynflow/coordinator.rb +4 -4
  16. data/lib/dynflow/director.rb +190 -0
  17. data/lib/dynflow/director/execution_plan_manager.rb +107 -0
  18. data/lib/dynflow/director/flow_manager.rb +43 -0
  19. data/lib/dynflow/director/running_steps_manager.rb +79 -0
  20. data/lib/dynflow/director/sequence_cursor.rb +91 -0
  21. data/lib/dynflow/{executors/parallel → director}/sequential_manager.rb +2 -2
  22. data/lib/dynflow/director/work_queue.rb +48 -0
  23. data/lib/dynflow/dispatcher/client_dispatcher.rb +24 -24
  24. data/lib/dynflow/dispatcher/executor_dispatcher.rb +1 -1
  25. data/lib/dynflow/execution_plan.rb +32 -15
  26. data/lib/dynflow/execution_plan/steps/abstract.rb +14 -14
  27. data/lib/dynflow/execution_plan/steps/error.rb +1 -1
  28. data/lib/dynflow/execution_plan/steps/finalize_step.rb +0 -1
  29. data/lib/dynflow/execution_plan/steps/plan_step.rb +11 -12
  30. data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
  31. data/lib/dynflow/executors/abstract.rb +5 -8
  32. data/lib/dynflow/executors/parallel.rb +4 -34
  33. data/lib/dynflow/executors/parallel/core.rb +18 -118
  34. data/lib/dynflow/executors/parallel/pool.rb +2 -2
  35. data/lib/dynflow/executors/parallel/worker.rb +3 -11
  36. data/lib/dynflow/persistence_adapters/sequel.rb +1 -2
  37. data/lib/dynflow/testing.rb +2 -0
  38. data/lib/dynflow/testing/in_thread_executor.rb +52 -0
  39. data/lib/dynflow/testing/in_thread_world.rb +64 -0
  40. data/lib/dynflow/testing/managed_clock.rb +1 -1
  41. data/lib/dynflow/throttle_limiter.rb +1 -1
  42. data/lib/dynflow/version.rb +1 -1
  43. data/lib/dynflow/world.rb +13 -7
  44. data/test/abnormal_states_recovery_test.rb +10 -0
  45. data/test/action_test.rb +9 -9
  46. data/test/clock_test.rb +0 -2
  47. data/test/concurrency_control_test.rb +1 -1
  48. data/test/execution_plan_test.rb +0 -2
  49. data/test/executor_test.rb +6 -13
  50. data/test/support/code_workflow_example.rb +1 -1
  51. data/test/support/rescue_example.rb +0 -1
  52. data/test/test_helper.rb +9 -12
  53. data/test/testing_test.rb +74 -2
  54. data/web/views/plan_step.erb +2 -0
  55. metadata +11 -8
  56. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +0 -111
  57. data/lib/dynflow/executors/parallel/flow_manager.rb +0 -45
  58. data/lib/dynflow/executors/parallel/running_steps_manager.rb +0 -81
  59. data/lib/dynflow/executors/parallel/sequence_cursor.rb +0 -97
  60. data/lib/dynflow/executors/parallel/work_queue.rb +0 -50
@@ -47,8 +47,8 @@ module Dynflow
47
47
  distribute_jobs
48
48
  end
49
49
 
50
- def worker_done(worker, step)
51
- @executor_core.tell([:finish_step, step])
50
+ def worker_done(worker, work)
51
+ @executor_core.tell([:work_finished, work])
52
52
  @free_workers << worker
53
53
  distribute_jobs
54
54
  end
@@ -2,25 +2,17 @@ module Dynflow
2
2
  module Executors
3
3
  class Parallel < Abstract
4
4
  class Worker < Actor
5
-
6
5
  def initialize(pool, transaction_adapter)
7
6
  @pool = Type! pool, Concurrent::Actor::Reference
8
7
  @transaction_adapter = Type! transaction_adapter, TransactionAdapters::Abstract
9
8
  end
10
9
 
11
- def on_message(message)
12
- match message,
13
- (on Work::Step.(step: ~any) |
14
- Work::Event.(step: ~any, event: Parallel::Event.(event: ~any)) do |step, event|
15
- step.execute event
16
- end),
17
- (on Work::Finalize.(~any, any) do |sequential_manager|
18
- sequential_manager.finalize
19
- end)
10
+ def on_message(work_item)
11
+ work_item.execute
20
12
  rescue Errors::PersistenceError => e
21
13
  @pool.tell([:handle_persistence_error, e])
22
14
  ensure
23
- @pool.tell([:worker_done, reference, message])
15
+ @pool.tell([:worker_done, reference, work_item])
24
16
  @transaction_adapter.cleanup
25
17
  end
26
18
  end
@@ -266,7 +266,6 @@ module Dynflow
266
266
  records.map { |record| load_data(record) }
267
267
  end
268
268
 
269
-
270
269
  def load_data(record)
271
270
  Utils.indifferent_hash(MultiJson.load(record[:data]))
272
271
  end
@@ -277,7 +276,7 @@ module Dynflow
277
276
 
278
277
  def extract_metadata(what, value)
279
278
  meta_keys = META_DATA.fetch(what)
280
- value = Utils.indifferent_hash(value)
279
+ value = Utils.indifferent_hash(value)
281
280
  meta_keys.inject({}) { |h, k| h.update k.to_sym => value[k] }
282
281
  end
283
282
 
@@ -23,6 +23,8 @@ module Dynflow
23
23
  require 'dynflow/testing/dummy_execution_plan'
24
24
  require 'dynflow/testing/dummy_step'
25
25
  require 'dynflow/testing/dummy_planned_action'
26
+ require 'dynflow/testing/in_thread_executor'
27
+ require 'dynflow/testing/in_thread_world'
26
28
  require 'dynflow/testing/assertions'
27
29
  require 'dynflow/testing/factories'
28
30
 
@@ -0,0 +1,52 @@
1
+ module Dynflow
2
+ module Testing
3
+ class InThreadExecutor < Dynflow::Executors::Abstract
4
+ def initialize(world)
5
+ @world = world
6
+ @director = Director.new(@world)
7
+ @work_items = Queue.new
8
+ end
9
+
10
+ def execute(execution_plan_id, finished = Concurrent.future, _wait_for_acceptance = true)
11
+ feed_queue(@director.start_execution(execution_plan_id, finished))
12
+ process_work_items
13
+ finished
14
+ end
15
+
16
+ def process_work_items
17
+ until @work_items.empty?
18
+ feed_queue(handle_work(@work_items.pop))
19
+ clock_tick
20
+ end
21
+ end
22
+
23
+ def handle_work(work_item)
24
+ work_item.execute
25
+ @director.work_finished(work_item)
26
+ end
27
+
28
+ def event(execution_plan_id, step_id, event, future = Concurrent.future)
29
+ event = (Director::Event[execution_plan_id, step_id, event, future])
30
+ @director.handle_event(event).each do |work_item|
31
+ @work_items << work_item
32
+ end
33
+ future
34
+ end
35
+
36
+ def clock_tick
37
+ @world.clock.progress
38
+ end
39
+
40
+ def feed_queue(work_items)
41
+ work_items.each { |work_item| @work_items.push(work_item) }
42
+ end
43
+
44
+ def terminate(future = Concurrent.future)
45
+ @director.terminate
46
+ future.success true
47
+ rescue => e
48
+ future.fail e
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ module Dynflow
2
+ module Testing
3
+ class InThreadWorld < Dynflow::World
4
+ def self.test_world_config
5
+ config = Dynflow::Config.new
6
+ config.persistence_adapter = persistence_adapter
7
+ config.logger_adapter = logger_adapter
8
+ config.coordinator_adapter = coordinator_adapter
9
+ config.delayed_executor = nil
10
+ config.auto_rescue = false
11
+ config.auto_validity_check = false
12
+ config.exit_on_terminate = false
13
+ config.auto_execute = false
14
+ config.auto_terminate = false
15
+ yield config if block_given?
16
+ return config
17
+ end
18
+
19
+ def self.persistence_adapter
20
+ @persistence_adapter ||= begin
21
+ db_config = ENV['DB_CONN_STRING'] || 'sqlite:/'
22
+ puts "Using database configuration: #{db_config}"
23
+ Dynflow::PersistenceAdapters::Sequel.new(db_config)
24
+ end
25
+ end
26
+
27
+ def self.logger_adapter
28
+ @adapter ||= Dynflow::LoggerAdapters::Simple.new $stderr, 4
29
+ end
30
+
31
+ def self.coordinator_adapter
32
+ ->(world, _) { CoordiationAdapterWithLog.new(world) }
33
+ end
34
+
35
+ # The worlds created by this method are getting terminated after each test run
36
+ def self.instance(&block)
37
+ @instance ||= self.new(test_world_config(&block))
38
+ end
39
+
40
+ def initialize(*args)
41
+ super
42
+ @clock = ManagedClock.new
43
+ @executor = InThreadExecutor.new(self)
44
+ end
45
+
46
+ def execute(execution_plan_id, done = Concurrent.future)
47
+ @executor.execute(execution_plan_id, done)
48
+ end
49
+
50
+ def terminate(future = Concurrent.future)
51
+ run_before_termination_hooks
52
+ @executor.terminate
53
+ coordinator.delete_world(registered_world)
54
+ future.success true
55
+ rescue => e
56
+ future.fail e
57
+ end
58
+
59
+ def event(execution_plan_id, step_id, event, done = Concurrent.future)
60
+ @executor.event(execution_plan_id, step_id, event, done)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -11,7 +11,7 @@ module Dynflow
11
11
  end
12
12
 
13
13
  def ping(who, time, with_what = nil, where = :<<)
14
- time = current_time + time if time.is_a? Numeric
14
+ time = current_time + time if time.is_a? Numeric
15
15
  with = with_what.nil? ? None : Some[Object][with_what]
16
16
  @pending_pings << Clock::Timer[who, time, with, where]
17
17
  @pending_pings.sort!
@@ -100,7 +100,7 @@ module Dynflow
100
100
 
101
101
  def cancel_plan_id(plan_id, reason)
102
102
  plan = @world.persistence.load_execution_plan(plan_id)
103
- steps = plan.steps.values.select { |step| step.is_a?(::Dynflow::ExecutionPlan::Steps::RunStep) }
103
+ steps = plan.run_steps
104
104
  steps.each do |step|
105
105
  step.state = :error
106
106
  step.error = ::Dynflow::ExecutionPlan::Steps::Error.new(reason)
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.16'
2
+ VERSION = '0.8.17'
3
3
  end
data/lib/dynflow/world.rb CHANGED
@@ -5,9 +5,9 @@ module Dynflow
5
5
  include Algebrick::Matching
6
6
 
7
7
  attr_reader :id, :client_dispatcher, :executor_dispatcher, :executor, :connector,
8
- :transaction_adapter, :logger_adapter, :coordinator,
9
- :persistence, :action_classes, :subscription_index,
10
- :middleware, :auto_rescue, :clock, :meta, :delayed_executor, :auto_validity_check, :validity_check_timeout, :throttle_limiter
8
+ :transaction_adapter, :logger_adapter, :coordinator,
9
+ :persistence, :action_classes, :subscription_index,
10
+ :middleware, :auto_rescue, :clock, :meta, :delayed_executor, :auto_validity_check, :validity_check_timeout, :throttle_limiter
11
11
 
12
12
  def initialize(config)
13
13
  @id = SecureRandom.uuid
@@ -141,7 +141,7 @@ module Dynflow
141
141
  else
142
142
  execution_plan = plan(action_class, *args)
143
143
  end
144
- planned = execution_plan.state == :planned
144
+ planned = execution_plan.state == :planned
145
145
 
146
146
  if planned
147
147
  done = execute(execution_plan.id, Concurrent.future)
@@ -280,7 +280,13 @@ module Dynflow
280
280
  end
281
281
 
282
282
  def invalidate_execution_lock(execution_lock)
283
- plan = persistence.load_execution_plan(execution_lock.execution_plan_id)
283
+ begin
284
+ plan = persistence.load_execution_plan(execution_lock.execution_plan_id)
285
+ rescue KeyError => e
286
+ logger.error "invalidated execution plan #{execution_lock.execution_plan_id} missing, skipping"
287
+ coordinator.release(execution_lock)
288
+ return
289
+ end
284
290
  plan.execution_history.add('terminate execution', execution_lock.world_id)
285
291
 
286
292
  plan.steps.values.each do |step|
@@ -291,7 +297,7 @@ module Dynflow
291
297
  end
292
298
  end
293
299
 
294
- plan.update_state(:paused) unless [:paused, :stopped].include?(plan.state)
300
+ plan.update_state(:paused) if plan.state == :running
295
301
  plan.save
296
302
  coordinator.release(execution_lock)
297
303
  unless plan.error?
@@ -322,6 +328,7 @@ module Dynflow
322
328
  invalidate(world)
323
329
  result = :invalidated
324
330
  rescue => e
331
+ logger.error e
325
332
  result = e.message
326
333
  end
327
334
  else
@@ -374,7 +381,6 @@ module Dynflow
374
381
  end
375
382
 
376
383
  private
377
-
378
384
  def calculate_subscription_index
379
385
  @subscription_index =
380
386
  action_classes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |klass, index|
@@ -85,6 +85,16 @@ module Dynflow
85
85
  "unlock world-invalidation:#{executor_world.id}"]
86
86
  client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
87
87
  end
88
+
89
+ it "handles missing execution plans" do
90
+ lock = Coordinator::ExecutionLock.new(executor_world, "missing", nil, nil)
91
+ executor_world.coordinator.acquire(lock)
92
+ client_world.invalidate(executor_world.registered_world)
93
+ expected_locks = ["lock world-invalidation:#{executor_world.id}",
94
+ "unlock execution-plan:missing",
95
+ "unlock world-invalidation:#{executor_world.id}"]
96
+ client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
97
+ end
88
98
  end
89
99
  end
90
100
 
data/test/action_test.rb CHANGED
@@ -81,7 +81,7 @@ module Dynflow
81
81
  end
82
82
 
83
83
  it 'fails when output is not serializable' do
84
- klass = Class.new(Dynflow::Action) do
84
+ klass = Class.new(Dynflow::Action) do
85
85
  def run
86
86
  output.update key: Object.new
87
87
  end
@@ -148,7 +148,7 @@ module Dynflow
148
148
 
149
149
  class Config
150
150
  attr_accessor :external_service, :poll_max_retries,
151
- :poll_intervals, :attempts_before_next_interval
151
+ :poll_intervals, :attempts_before_next_interval
152
152
 
153
153
  def initialize
154
154
  @external_service = ExternalService.new
@@ -235,13 +235,13 @@ module Dynflow
235
235
  end
236
236
 
237
237
  it 'initiates the external task' do
238
- action = run_action plan
238
+ action = run_action plan
239
239
 
240
240
  action.output[:task][:task_id].must_equal 123
241
241
  end
242
242
 
243
243
  it 'polls till the task is done' do
244
- action = run_action plan
244
+ action = run_action plan
245
245
 
246
246
  9.times { progress_action_time action }
247
247
  action.done?.must_equal false
@@ -255,14 +255,14 @@ module Dynflow
255
255
  end
256
256
 
257
257
  it 'tries to poll for the old task when resuming' do
258
- action = run_action plan
258
+ action = run_action plan
259
259
  action.output[:task][:progress].must_equal 0
260
260
  run_action action
261
261
  action.output[:task][:progress].must_equal 10
262
262
  end
263
263
 
264
264
  it 'invokes the external task again when polling on the old one fails' do
265
- action = run_action plan
265
+ action = run_action plan
266
266
  action.world.silence_logger!
267
267
  action.external_service.will_fail
268
268
  action.output[:task][:progress].must_equal 0
@@ -271,7 +271,7 @@ module Dynflow
271
271
  end
272
272
 
273
273
  it 'tolerates some failure while polling' do
274
- action = run_action plan
274
+ action = run_action plan
275
275
  action.external_service.will_fail
276
276
  action.world.silence_logger!
277
277
 
@@ -293,7 +293,7 @@ module Dynflow
293
293
  TestPollingAction.config.poll_intervals = [1, 2]
294
294
  TestPollingAction.config.attempts_before_next_interval = 2
295
295
 
296
- action = run_action plan
296
+ action = run_action plan
297
297
  pings = []
298
298
  pings << next_ping(action)
299
299
  progress_action_time action
@@ -317,7 +317,7 @@ module Dynflow
317
317
  end
318
318
 
319
319
  it 'timesout' do
320
- action = run_action plan
320
+ action = run_action plan
321
321
  iterations = 0
322
322
  while progress_action_time action
323
323
  # we count the number of iterations till the timeout occurs
data/test/clock_test.rb CHANGED
@@ -12,7 +12,6 @@ describe clock_class do
12
12
  clock.ping [], 0.1, :pong
13
13
  end
14
14
 
15
-
16
15
  it 'pongs' do
17
16
  q = Queue.new
18
17
  start = Time.now
@@ -51,5 +50,4 @@ describe clock_class do
51
50
  threads.each &:join
52
51
  end
53
52
 
54
-
55
53
  end
@@ -53,7 +53,7 @@ module Dynflow
53
53
  unless output[:slept]
54
54
  output[:slept] = true
55
55
  puts "SLEEPING" if input[:should_sleep]
56
- suspend { |suspended| world.clock.ping(suspended, 100, [:run]) } if input[:should_sleep]
56
+ suspend { |suspended| world.clock.ping(suspended, 100, [:run]) } if input[:should_sleep]
57
57
  end
58
58
  end
59
59
  end
@@ -61,7 +61,6 @@ module Dynflow
61
61
 
62
62
  end
63
63
 
64
-
65
64
  describe 'for error in running phase' do
66
65
 
67
66
  before do
@@ -224,7 +223,6 @@ module Dynflow
224
223
  end
225
224
  end
226
225
 
227
-
228
226
  describe 'finalize flow' do
229
227
 
230
228
  let :execution_plan do
@@ -113,9 +113,8 @@ module Dynflow
113
113
  TestPause.when_paused do
114
114
  plan = world.persistence.load_execution_plan(execution_plan.id)
115
115
  plan.state.must_equal :running
116
- triage = plan.steps.values.find do |s|
117
- s.is_a?(Dynflow::ExecutionPlan::Steps::RunStep) &&
118
- s.action_class == Support::CodeWorkflowExample::Triage
116
+ triage = plan.run_steps.find do |s|
117
+ s.action_class == Support::CodeWorkflowExample::Triage
119
118
  end
120
119
  triage.state.must_equal :running
121
120
  world.persistence.
@@ -204,10 +203,7 @@ module Dynflow
204
203
  it 'fails' do
205
204
  assert_equal :error, result.result
206
205
  assert_equal :paused, result.state
207
- assert_equal :error,
208
- result.steps.values.
209
- find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.
210
- state
206
+ assert_equal :error, result.run_steps.first.state
211
207
  end
212
208
  end
213
209
 
@@ -292,14 +288,12 @@ module Dynflow
292
288
  specify do
293
289
  assert_equal :paused, result.state
294
290
  assert_equal :error, result.result
295
- assert_equal :error, result.steps.values.
296
- find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.state
291
+ assert_equal :error, result.run_steps.first.state
297
292
 
298
293
  ep = world.execute(result.id).value
299
294
  assert_equal :stopped, ep.state
300
295
  assert_equal :success, ep.result
301
- assert_equal :success, ep.steps.values.
302
- find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.state
296
+ assert_equal :success, ep.run_steps.first.state
303
297
  end
304
298
  end
305
299
 
@@ -536,7 +530,7 @@ module Dynflow
536
530
  world.plan(Support::CodeWorkflowExample::IncomingIssues, issues_data)
537
531
  end
538
532
 
539
- let(:manager) { Executors::Parallel::FlowManager.new execution_plan, execution_plan.run_flow }
533
+ let(:manager) { Director::FlowManager.new execution_plan, execution_plan.run_flow }
540
534
 
541
535
  def assert_next_steps(expected_next_step_ids, finished_step_id = nil, success = true)
542
536
  if finished_step_id
@@ -569,7 +563,6 @@ module Dynflow
569
563
  assert_next_steps([], 4, false)
570
564
  end
571
565
 
572
-
573
566
  it "is not done while other steps can be finished" do
574
567
  assert_next_steps([4, 13])
575
568
  assert_next_steps([], 4, false)