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
@@ -0,0 +1,107 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Director
|
3
|
+
class ExecutionPlanManager
|
4
|
+
include Algebrick::TypeCheck
|
5
|
+
include Algebrick::Matching
|
6
|
+
|
7
|
+
attr_reader :execution_plan, :future
|
8
|
+
|
9
|
+
def initialize(world, execution_plan, future)
|
10
|
+
@world = Type! world, World
|
11
|
+
@execution_plan = Type! execution_plan, ExecutionPlan
|
12
|
+
@future = Type! future, Concurrent::Edge::Future
|
13
|
+
@running_steps_manager = RunningStepsManager.new(world)
|
14
|
+
|
15
|
+
unless [:planned, :paused].include? execution_plan.state
|
16
|
+
raise "execution_plan is not in pending or paused state, it's #{execution_plan.state}"
|
17
|
+
end
|
18
|
+
execution_plan.execution_history.add('start execution', @world.id)
|
19
|
+
execution_plan.update_state(:running)
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
raise "The future was already set" if @future.completed?
|
24
|
+
start_run or start_finalize or finish
|
25
|
+
end
|
26
|
+
|
27
|
+
def prepare_next_step(step)
|
28
|
+
StepWorkItem.new(execution_plan.id, step).tap do |work|
|
29
|
+
@running_steps_manager.add(step, work)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Array<WorkItem>] of Work items to continue with
|
34
|
+
def what_is_next(work)
|
35
|
+
Type! work, WorkItem
|
36
|
+
|
37
|
+
case work
|
38
|
+
when StepWorkItem
|
39
|
+
step = work.step
|
40
|
+
execution_plan.steps[step.id] = step
|
41
|
+
suspended, work = @running_steps_manager.done(step)
|
42
|
+
work = compute_next_from_step(step) unless suspended
|
43
|
+
work
|
44
|
+
when FinalizeWorkItem
|
45
|
+
raise "Finalize work item without @finalize_manager ready" unless @finalize_manager
|
46
|
+
finish
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def event(event)
|
51
|
+
Type! event, Event
|
52
|
+
unless event.execution_plan_id == @execution_plan.id
|
53
|
+
raise "event #{event.inspect} doesn't belong to plan #{@execution_plan.id}"
|
54
|
+
end
|
55
|
+
@running_steps_manager.event(event)
|
56
|
+
end
|
57
|
+
|
58
|
+
def done?
|
59
|
+
(!@run_manager || @run_manager.done?) && (!@finalize_manager || @finalize_manager.done?)
|
60
|
+
end
|
61
|
+
|
62
|
+
def terminate
|
63
|
+
@running_steps_manager.terminate
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def compute_next_from_step(step)
|
69
|
+
raise "run manager not set" unless @run_manager
|
70
|
+
raise "run manager already done" if @run_manager.done?
|
71
|
+
|
72
|
+
next_steps = @run_manager.what_is_next(step)
|
73
|
+
if @run_manager.done?
|
74
|
+
start_finalize or finish
|
75
|
+
else
|
76
|
+
next_steps.map { |s| prepare_next_step(s) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def no_work
|
81
|
+
raise "No work but not done" unless done?
|
82
|
+
[]
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_run
|
86
|
+
return if execution_plan.run_flow.empty?
|
87
|
+
raise 'run phase already started' if @run_manager
|
88
|
+
@run_manager = FlowManager.new(execution_plan, execution_plan.run_flow)
|
89
|
+
@run_manager.start.map { |s| prepare_next_step(s) }.tap { |a| raise if a.empty? }
|
90
|
+
end
|
91
|
+
|
92
|
+
def start_finalize
|
93
|
+
return if execution_plan.finalize_flow.empty?
|
94
|
+
raise 'finalize phase already started' if @finalize_manager
|
95
|
+
@finalize_manager = SequentialManager.new(@world, execution_plan)
|
96
|
+
[FinalizeWorkItem.new(execution_plan.id, @finalize_manager)]
|
97
|
+
end
|
98
|
+
|
99
|
+
def finish
|
100
|
+
execution_plan.execution_history.add('finish execution', @world.id)
|
101
|
+
@execution_plan.update_state(execution_plan.error? ? :paused : :stopped)
|
102
|
+
return no_work
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Director
|
3
|
+
class FlowManager
|
4
|
+
include Algebrick::TypeCheck
|
5
|
+
|
6
|
+
attr_reader :execution_plan, :cursor_index
|
7
|
+
|
8
|
+
def initialize(execution_plan, flow)
|
9
|
+
@execution_plan = Type! execution_plan, ExecutionPlan
|
10
|
+
@flow = flow
|
11
|
+
@cursor_index = {}
|
12
|
+
@cursor = build_root_cursor
|
13
|
+
end
|
14
|
+
|
15
|
+
def done?
|
16
|
+
@cursor.done?
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Set] of steps to continue with
|
20
|
+
def what_is_next(flow_step)
|
21
|
+
return [] if flow_step.state == :suspended
|
22
|
+
|
23
|
+
success = flow_step.state != :error
|
24
|
+
return cursor_index[flow_step.id].what_is_next(flow_step, success)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Set] of steps to continue with
|
28
|
+
def start
|
29
|
+
return @cursor.what_is_next.tap do |steps|
|
30
|
+
raise 'invalid state' if steps.empty? && !done?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def build_root_cursor
|
37
|
+
# the root cursor has to always run against sequence
|
38
|
+
sequence = @flow.is_a?(Flows::Sequence) ? @flow : Flows::Sequence.new([@flow])
|
39
|
+
return SequenceCursor.new(self, sequence, nil)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Director
|
3
|
+
# Handles the events generated while running actions, makes sure
|
4
|
+
# the events are sent to the action only when in suspended state
|
5
|
+
class RunningStepsManager
|
6
|
+
include Algebrick::TypeCheck
|
7
|
+
|
8
|
+
def initialize(world)
|
9
|
+
@world = Type! world, World
|
10
|
+
@running_steps = {}
|
11
|
+
@events = WorkQueue.new(Integer, WorkItem)
|
12
|
+
end
|
13
|
+
|
14
|
+
def terminate
|
15
|
+
pending_work = @events.clear.values.flatten(1)
|
16
|
+
pending_work.each do |w|
|
17
|
+
if EventWorkItem === w
|
18
|
+
w.event.result.fail UnprocessableEvent.new("dropping due to termination")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(step, work)
|
24
|
+
Type! step, ExecutionPlan::Steps::RunStep
|
25
|
+
@running_steps[step.id] = step
|
26
|
+
# we make sure not to run any event when the step is still being executed
|
27
|
+
@events.push(step.id, work)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# @returns [TrueClass|FalseClass, Array<WorkItem>]
|
32
|
+
def done(step)
|
33
|
+
Type! step, ExecutionPlan::Steps::RunStep
|
34
|
+
@events.shift(step.id).tap do |work|
|
35
|
+
work.event.result.success true if EventWorkItem === work
|
36
|
+
end
|
37
|
+
|
38
|
+
if step.state == :suspended
|
39
|
+
return true, [@events.first(step.id)].compact
|
40
|
+
else
|
41
|
+
while (event = @events.shift(step.id))
|
42
|
+
message = "step #{step.execution_plan_id}:#{step.id} dropping event #{event.event}"
|
43
|
+
@world.logger.warn message
|
44
|
+
event.event.result.fail UnprocessableEvent.new(message).
|
45
|
+
tap { |e| e.set_backtrace(caller) }
|
46
|
+
end
|
47
|
+
raise 'assert' unless @events.empty?(step.id)
|
48
|
+
@running_steps.delete(step.id)
|
49
|
+
return false, []
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def try_to_terminate
|
54
|
+
@running_steps.delete_if do |_, step|
|
55
|
+
step.state != :running
|
56
|
+
end
|
57
|
+
return @running_steps.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# @returns [Array<WorkItem>]
|
61
|
+
def event(event)
|
62
|
+
Type! event, Event
|
63
|
+
next_work_items = []
|
64
|
+
|
65
|
+
step = @running_steps[event.step_id]
|
66
|
+
unless step
|
67
|
+
event.result.fail UnprocessableEvent.new('step is not suspended, it cannot process events')
|
68
|
+
return next_work_items
|
69
|
+
end
|
70
|
+
|
71
|
+
can_run_event = @events.empty?(step.id)
|
72
|
+
work = EventWorkItem.new(event.execution_plan_id, step, event)
|
73
|
+
@events.push(step.id, work)
|
74
|
+
next_work_items << work if can_run_event
|
75
|
+
next_work_items
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Director
|
3
|
+
class SequenceCursor
|
4
|
+
|
5
|
+
def initialize(flow_manager, sequence, parent_cursor = nil)
|
6
|
+
@flow_manager = flow_manager
|
7
|
+
@sequence = sequence
|
8
|
+
@parent_cursor = parent_cursor
|
9
|
+
@todo = []
|
10
|
+
@index = -1 # starts before first element
|
11
|
+
@no_error_so_far = true
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [ExecutionPlan::Steps::Abstract, SequenceCursor] work
|
15
|
+
# step or sequence cursor that was done
|
16
|
+
# @param [true, false] success was the work finished successfully
|
17
|
+
# @return [Array<Integer>] new step_ids that can be done next
|
18
|
+
def what_is_next(work = nil, success = true)
|
19
|
+
unless work.nil? || @todo.delete(work)
|
20
|
+
raise "marking as done work that was not expected: #{work.inspect}"
|
21
|
+
end
|
22
|
+
|
23
|
+
@no_error_so_far &&= success
|
24
|
+
|
25
|
+
if done_here?
|
26
|
+
return next_steps
|
27
|
+
else
|
28
|
+
return []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# return true if we can't move the cursor further, either when
|
33
|
+
# everyting is done in the sequence or there was some failure
|
34
|
+
# that prevents us from moving
|
35
|
+
def done?
|
36
|
+
(!@no_error_so_far && done_here?) || @index == @sequence.size
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# steps we can do right now without waiting for anything
|
42
|
+
def steps_todo
|
43
|
+
@todo.map do |item|
|
44
|
+
case item
|
45
|
+
when SequenceCursor
|
46
|
+
item.steps_todo
|
47
|
+
else
|
48
|
+
item
|
49
|
+
end
|
50
|
+
end.flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
def move
|
54
|
+
@index += 1
|
55
|
+
next_flow = @sequence.sub_flows[@index]
|
56
|
+
add_todo(next_flow)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def done_here?
|
62
|
+
@todo.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def next_steps
|
66
|
+
move if @no_error_so_far
|
67
|
+
return steps_todo unless done?
|
68
|
+
if @parent_cursor
|
69
|
+
return @parent_cursor.what_is_next(self, @no_error_so_far)
|
70
|
+
else
|
71
|
+
return []
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_todo(flow)
|
76
|
+
case flow
|
77
|
+
when Flows::Sequence
|
78
|
+
@todo << SequenceCursor.new(@flow_manager, flow, self).tap do |cursor|
|
79
|
+
cursor.move
|
80
|
+
end
|
81
|
+
when Flows::Concurrence
|
82
|
+
flow.sub_flows.each { |sub_flow| add_todo(sub_flow) }
|
83
|
+
when Flows::Atom
|
84
|
+
@flow_manager.cursor_index[flow.step_id] = self
|
85
|
+
@todo << @flow_manager.execution_plan.steps[flow.step_id]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Director
|
3
|
+
class WorkQueue
|
4
|
+
include Algebrick::TypeCheck
|
5
|
+
|
6
|
+
def initialize(key_type = Object, work_type = Object)
|
7
|
+
@key_type = key_type
|
8
|
+
@work_type = work_type
|
9
|
+
@stash = Hash.new { |hash, key| hash[key] = [] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def push(key, work)
|
13
|
+
Type! key, @key_type
|
14
|
+
Type! work, @work_type
|
15
|
+
@stash[key].push work
|
16
|
+
end
|
17
|
+
|
18
|
+
def shift(key)
|
19
|
+
return nil unless present? key
|
20
|
+
@stash[key].shift.tap { |work| @stash.delete(key) if @stash[key].empty? }
|
21
|
+
end
|
22
|
+
|
23
|
+
def present?(key)
|
24
|
+
@stash.key?(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty?(key)
|
28
|
+
!present?(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
ret = @stash.dup
|
33
|
+
@stash.clear
|
34
|
+
ret
|
35
|
+
end
|
36
|
+
|
37
|
+
def size(key)
|
38
|
+
return 0 if empty?(key)
|
39
|
+
@stash[key].size
|
40
|
+
end
|
41
|
+
|
42
|
+
def first(key)
|
43
|
+
return nil if empty?(key)
|
44
|
+
@stash[key].first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -52,15 +52,15 @@ module Dynflow
|
|
52
52
|
|
53
53
|
def dispatch_request(request, client_world_id, request_id)
|
54
54
|
executor_id = match request,
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
55
|
+
(on ~Execution do |execution|
|
56
|
+
AnyExecutor
|
57
|
+
end),
|
58
|
+
(on ~Event do |event|
|
59
|
+
find_executor(event.execution_plan_id)
|
60
|
+
end),
|
61
|
+
(on Ping.(~any) do |receiver_id|
|
62
|
+
receiver_id
|
63
|
+
end)
|
64
64
|
envelope = Envelope[request_id, client_world_id, executor_id, request]
|
65
65
|
if Dispatcher::UnknownWorld === envelope.receiver_id
|
66
66
|
raise Dynflow::Error, "Could not find an executor for #{envelope}"
|
@@ -74,15 +74,15 @@ module Dynflow
|
|
74
74
|
def dispatch_response(envelope)
|
75
75
|
return unless @tracked_requests.key?(envelope.request_id)
|
76
76
|
match envelope.message,
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
77
|
+
(on ~Accepted do
|
78
|
+
@tracked_requests[envelope.request_id].accept!
|
79
|
+
end),
|
80
|
+
(on ~Failed do |msg|
|
81
|
+
resolve_tracked_request(envelope.request_id, Dynflow::Error.new(msg.error))
|
82
|
+
end),
|
83
|
+
(on Done | Pong do
|
84
|
+
resolve_tracked_request(envelope.request_id)
|
85
|
+
end)
|
86
86
|
end
|
87
87
|
|
88
88
|
private
|
@@ -128,12 +128,12 @@ module Dynflow
|
|
128
128
|
else
|
129
129
|
tracked_request = @tracked_requests[id]
|
130
130
|
resolve_to = match tracked_request.request,
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
131
|
+
(on Execution.(execution_plan_id: ~any) do |uuid|
|
132
|
+
@world.persistence.load_execution_plan(uuid)
|
133
|
+
end),
|
134
|
+
(on Event | Ping do
|
135
|
+
true
|
136
|
+
end)
|
137
137
|
@tracked_requests.delete(id).success! resolve_to
|
138
138
|
end
|
139
139
|
end
|