dynflow 0.8.16 → 0.8.17

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 (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)