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