dynflow 0.8.16 → 0.8.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +0 -25
- data/doc/pages/plugins/div_tag.rb +1 -1
- data/doc/pages/plugins/tags.rb +0 -1
- data/examples/orchestrate.rb +0 -1
- data/examples/remote_executor.rb +3 -3
- data/examples/sub_plan_concurrency_control.rb +0 -1
- data/examples/sub_plans.rb +0 -1
- data/lib/dynflow.rb +1 -0
- data/lib/dynflow/action.rb +6 -6
- data/lib/dynflow/config.rb +2 -2
- data/lib/dynflow/connectors/database.rb +1 -1
- data/lib/dynflow/connectors/direct.rb +1 -1
- data/lib/dynflow/coordinator.rb +4 -4
- data/lib/dynflow/director.rb +190 -0
- data/lib/dynflow/director/execution_plan_manager.rb +107 -0
- data/lib/dynflow/director/flow_manager.rb +43 -0
- data/lib/dynflow/director/running_steps_manager.rb +79 -0
- data/lib/dynflow/director/sequence_cursor.rb +91 -0
- data/lib/dynflow/{executors/parallel → director}/sequential_manager.rb +2 -2
- data/lib/dynflow/director/work_queue.rb +48 -0
- data/lib/dynflow/dispatcher/client_dispatcher.rb +24 -24
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +1 -1
- data/lib/dynflow/execution_plan.rb +32 -15
- data/lib/dynflow/execution_plan/steps/abstract.rb +14 -14
- data/lib/dynflow/execution_plan/steps/error.rb +1 -1
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +0 -1
- data/lib/dynflow/execution_plan/steps/plan_step.rb +11 -12
- data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
- data/lib/dynflow/executors/abstract.rb +5 -8
- data/lib/dynflow/executors/parallel.rb +4 -34
- data/lib/dynflow/executors/parallel/core.rb +18 -118
- data/lib/dynflow/executors/parallel/pool.rb +2 -2
- data/lib/dynflow/executors/parallel/worker.rb +3 -11
- data/lib/dynflow/persistence_adapters/sequel.rb +1 -2
- data/lib/dynflow/testing.rb +2 -0
- data/lib/dynflow/testing/in_thread_executor.rb +52 -0
- data/lib/dynflow/testing/in_thread_world.rb +64 -0
- data/lib/dynflow/testing/managed_clock.rb +1 -1
- data/lib/dynflow/throttle_limiter.rb +1 -1
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +13 -7
- data/test/abnormal_states_recovery_test.rb +10 -0
- data/test/action_test.rb +9 -9
- data/test/clock_test.rb +0 -2
- data/test/concurrency_control_test.rb +1 -1
- data/test/execution_plan_test.rb +0 -2
- data/test/executor_test.rb +6 -13
- data/test/support/code_workflow_example.rb +1 -1
- data/test/support/rescue_example.rb +0 -1
- data/test/test_helper.rb +9 -12
- data/test/testing_test.rb +74 -2
- data/web/views/plan_step.erb +2 -0
- metadata +11 -8
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +0 -111
- data/lib/dynflow/executors/parallel/flow_manager.rb +0 -45
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +0 -81
- data/lib/dynflow/executors/parallel/sequence_cursor.rb +0 -97
- data/lib/dynflow/executors/parallel/work_queue.rb +0 -50
@@ -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(
|
12
|
-
|
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,
|
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
|
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
|
|
data/lib/dynflow/testing.rb
CHANGED
@@ -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
|
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.
|
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)
|
data/lib/dynflow/version.rb
CHANGED
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
|
-
|
9
|
-
|
10
|
-
|
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
|
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
|
-
|
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)
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
@@ -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])
|
56
|
+
suspend { |suspended| world.clock.ping(suspended, 100, [:run]) } if input[:should_sleep]
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
data/test/execution_plan_test.rb
CHANGED
data/test/executor_test.rb
CHANGED
@@ -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.
|
117
|
-
s.
|
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.
|
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.
|
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) {
|
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)
|