dynflow 0.8.9 → 0.8.10
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/.travis.yml +1 -0
- data/Gemfile +1 -1
- data/dynflow.gemspec +2 -2
- data/examples/example_helper.rb +4 -1
- data/examples/future_execution.rb +4 -5
- data/examples/orchestrate.rb +1 -0
- data/examples/sub_plan_concurrency_control.rb +73 -0
- data/lib/dynflow.rb +2 -0
- data/lib/dynflow/action/with_sub_plans.rb +42 -4
- data/lib/dynflow/config.rb +8 -0
- data/lib/dynflow/delayed_plan.rb +4 -3
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +11 -7
- data/lib/dynflow/execution_plan.rb +5 -5
- data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
- data/lib/dynflow/semaphores.rb +8 -0
- data/lib/dynflow/semaphores/abstract.rb +46 -0
- data/lib/dynflow/semaphores/aggregating.rb +63 -0
- data/lib/dynflow/semaphores/dummy.rb +32 -0
- data/lib/dynflow/semaphores/stateful.rb +70 -0
- data/lib/dynflow/throttle_limiter.rb +135 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +16 -8
- data/test/action_test.rb +2 -2
- data/test/concurrency_control_test.rb +241 -0
- data/test/future_execution_test.rb +5 -5
- data/test/middleware_test.rb +2 -2
- data/test/prepare_travis_env.sh +1 -1
- data/test/semaphores_test.rb +98 -0
- data/test/test_helper.rb +1 -1
- metadata +18 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc004bca5a068a20db3c14f727b365b3ac8784b8
|
4
|
+
data.tar.gz: c1c95bbb21be8430e2da6f822d77262a0e7f769f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47a908f6fc5b80265d708be96f452450b45de7b8f9f2b63f59e623b162a2f205eebe7d72807d9e09ee10030b3f717293050ab87920ab5e302e990175f0bb9b72
|
7
|
+
data.tar.gz: 0d166b88b31788c97bb9f8dabb1a94e4a351c9df0123a1777179f0fb4fd2bc681e9f467e2a2e01d36ed736d1d9e45233c1ae069291925ee1708a8e2d0ac95bea
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/dynflow.gemspec
CHANGED
@@ -21,8 +21,8 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_dependency "multi_json"
|
22
22
|
s.add_dependency "apipie-params"
|
23
23
|
s.add_dependency "algebrick", '~> 0.7.0'
|
24
|
-
s.add_dependency "concurrent-ruby", '~> 1.0
|
25
|
-
s.add_dependency "concurrent-ruby-edge", '~> 0.2
|
24
|
+
s.add_dependency "concurrent-ruby", '~> 1.0'
|
25
|
+
s.add_dependency "concurrent-ruby-edge", '~> 0.2'
|
26
26
|
s.add_dependency "sequel"
|
27
27
|
|
28
28
|
s.add_development_dependency "rack-test"
|
data/examples/example_helper.rb
CHANGED
@@ -8,6 +8,10 @@ class ExampleHelper
|
|
8
8
|
@world ||= create_world
|
9
9
|
end
|
10
10
|
|
11
|
+
def set_world(world)
|
12
|
+
@world = world
|
13
|
+
end
|
14
|
+
|
11
15
|
def create_world
|
12
16
|
config = Dynflow::Config.new
|
13
17
|
config.persistence_adapter = persistence_adapter
|
@@ -31,7 +35,6 @@ class ExampleHelper
|
|
31
35
|
Dynflow::LoggerAdapters::Simple.new $stderr, 4
|
32
36
|
end
|
33
37
|
|
34
|
-
|
35
38
|
def run_web_console(world = ExampleHelper.world)
|
36
39
|
require 'dynflow/web'
|
37
40
|
dynflow_console = Dynflow::Web.setup do
|
@@ -12,15 +12,14 @@ class CustomPassedObject
|
|
12
12
|
end
|
13
13
|
|
14
14
|
class CustomPassedObjectSerializer < ::Dynflow::Serializers::Abstract
|
15
|
-
def serialize
|
16
|
-
object = args.first
|
15
|
+
def serialize(arg)
|
17
16
|
# Serialized output can be anything that is representable as JSON: Array, Hash...
|
18
|
-
{ :id =>
|
17
|
+
{ :id => arg.id, :name => arg.name }
|
19
18
|
end
|
20
19
|
|
21
|
-
def deserialize
|
20
|
+
def deserialize(arg)
|
22
21
|
# Deserialized output must be an Array
|
23
|
-
|
22
|
+
CustomPassedObject.new(arg[:id], arg[:name])
|
24
23
|
end
|
25
24
|
end
|
26
25
|
|
data/examples/orchestrate.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
example_description = <<DESC
|
4
|
+
Sub-plan Concurrency Control Example
|
5
|
+
====================================
|
6
|
+
|
7
|
+
This example shows, how an action with sub-plans can be used
|
8
|
+
to control concurrency level and tasks distribution over time.
|
9
|
+
|
10
|
+
This is useful, when doing resource-intensive bulk actions,
|
11
|
+
where running all actions at once would drain the system's resources.
|
12
|
+
|
13
|
+
DESC
|
14
|
+
|
15
|
+
require_relative 'example_helper'
|
16
|
+
|
17
|
+
class CostyAction < Dynflow::Action
|
18
|
+
|
19
|
+
SleepTime = 10
|
20
|
+
|
21
|
+
def plan(number)
|
22
|
+
action_logger.info("#{number} PLAN")
|
23
|
+
plan_self(:number => number)
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(event = nil)
|
27
|
+
unless output.key? :slept
|
28
|
+
output[:slept] = true
|
29
|
+
suspend do |suspended_action|
|
30
|
+
action_logger.info("#{input[:number]} SLEEP")
|
31
|
+
world.clock.ping(suspended_action, SleepTime)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalize
|
37
|
+
action_logger.info("#{input[:number]} DONE")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
class ConcurrencyControlExample < Dynflow::Action
|
43
|
+
include Dynflow::Action::WithSubPlans
|
44
|
+
|
45
|
+
ConcurrencyLevel = 2
|
46
|
+
RunWithin = 2 * 60
|
47
|
+
|
48
|
+
def plan(count)
|
49
|
+
limit_concurrency_level(ConcurrencyLevel)
|
50
|
+
distribute_over_time(RunWithin)
|
51
|
+
super(:count => count)
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_sub_plans
|
55
|
+
sleep 1
|
56
|
+
input[:count].times.map { |i| trigger(CostyAction, i) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if $0 == __FILE__
|
61
|
+
ExampleHelper.world.action_logger.level = Logger::INFO
|
62
|
+
triggered = ExampleHelper.world.trigger(ConcurrencyControlExample, 10)
|
63
|
+
puts example_description
|
64
|
+
puts <<-MSG.gsub(/^.*\|/, '')
|
65
|
+
| Execution plan #{triggered.id} with 10 sub plans triggered
|
66
|
+
| You can see the details at http://localhost:4567/#{triggered.id}/actions/1/sub_plans
|
67
|
+
| Or simply watch in the console that there are never more than #{ConcurrencyControlExample::ConcurrencyLevel} running at the same time.
|
68
|
+
|
|
69
|
+
| The tasks are distributed over #{ConcurrencyControlExample::RunWithin} seconds.
|
70
|
+
MSG
|
71
|
+
|
72
|
+
ExampleHelper.run_web_console
|
73
|
+
end
|
data/lib/dynflow.rb
CHANGED
@@ -17,8 +17,8 @@ module Dynflow
|
|
17
17
|
end
|
18
18
|
end),
|
19
19
|
(on SubPlanFinished do
|
20
|
-
|
21
|
-
|
20
|
+
mark_as_done(event.execution_plan_id, event.success)
|
21
|
+
try_to_finish or suspend
|
22
22
|
end),
|
23
23
|
(on Action::Cancellable::Cancel do
|
24
24
|
cancel!
|
@@ -28,7 +28,15 @@ module Dynflow
|
|
28
28
|
def initiate
|
29
29
|
sub_plans = create_sub_plans
|
30
30
|
sub_plans = Array[sub_plans] unless sub_plans.is_a? Array
|
31
|
-
|
31
|
+
if uses_concurrency_control
|
32
|
+
planned, failed = sub_plans.partition { |plan| plan.state == :planned }
|
33
|
+
calculate_time_distribution sub_plans.count
|
34
|
+
sub_plans = world.throttle_limiter.handle_plans!(execution_plan_id,
|
35
|
+
planned.map(&:id),
|
36
|
+
failed.map(&:id),
|
37
|
+
input[:concurrency_control])
|
38
|
+
end
|
39
|
+
wait_for_sub_plans sub_plans
|
32
40
|
end
|
33
41
|
|
34
42
|
# @abstract when the logic for the initiation of the subtasks
|
@@ -55,13 +63,40 @@ module Dynflow
|
|
55
63
|
end
|
56
64
|
|
57
65
|
def cancel!
|
66
|
+
@world.throttle_limiter.cancel!(execution_plan_id)
|
58
67
|
sub_plans('state' => 'running').each(&:cancel)
|
59
68
|
suspend
|
60
69
|
end
|
61
70
|
|
62
71
|
# Helper for creating sub plans
|
63
72
|
def trigger(*args)
|
64
|
-
|
73
|
+
if uses_concurrency_control
|
74
|
+
world.plan_with_caller(self, *args)
|
75
|
+
else
|
76
|
+
world.trigger { world.plan_with_caller(self, *args) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def limit_concurrency_level(level)
|
81
|
+
input[:concurrency_control] ||= {}
|
82
|
+
input[:concurrency_control][:level] = ::Dynflow::Semaphores::Stateful.new(level).to_hash
|
83
|
+
end
|
84
|
+
|
85
|
+
def calculate_time_distribution(count)
|
86
|
+
time = input[:concurrency_control][:time]
|
87
|
+
unless time.nil? || time.is_a?(Hash)
|
88
|
+
# Assume concurrency level 1 unless stated otherwise
|
89
|
+
level = input[:concurrency_control].fetch(:level, {}).fetch(:free, 1)
|
90
|
+
semaphore = ::Dynflow::Semaphores::Stateful.new(nil, level,
|
91
|
+
:interval => time.to_f / (count * level),
|
92
|
+
:time_span => time)
|
93
|
+
input[:concurrency_control][:time] = semaphore.to_hash
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def distribute_over_time(time_span)
|
98
|
+
input[:concurrency_control] ||= {}
|
99
|
+
input[:concurrency_control][:time] = time_span
|
65
100
|
end
|
66
101
|
|
67
102
|
def wait_for_sub_plans(sub_plans)
|
@@ -171,5 +206,8 @@ module Dynflow
|
|
171
206
|
fail "A sub task failed" if output[:failed_count] > 0
|
172
207
|
end
|
173
208
|
|
209
|
+
def uses_concurrency_control
|
210
|
+
@uses_concurrency_control = input.key? :concurrency_control
|
211
|
+
end
|
174
212
|
end
|
175
213
|
end
|
data/lib/dynflow/config.rb
CHANGED
@@ -65,6 +65,10 @@ module Dynflow
|
|
65
65
|
Executors::Parallel.new(world, config.pool_size)
|
66
66
|
end
|
67
67
|
|
68
|
+
config_attr :executor_semaphore, Semaphores::Abstract, FalseClass do |world, config|
|
69
|
+
Semaphores::Dummy.new
|
70
|
+
end
|
71
|
+
|
68
72
|
config_attr :connector, Connectors::Abstract do |world|
|
69
73
|
Connectors::Direct.new(world)
|
70
74
|
end
|
@@ -99,6 +103,10 @@ module Dynflow
|
|
99
103
|
DelayedExecutors::Polling.new(world, options)
|
100
104
|
end
|
101
105
|
|
106
|
+
config_attr :throttle_limiter, ::Dynflow::ThrottleLimiter do |world|
|
107
|
+
::Dynflow::ThrottleLimiter.new(world)
|
108
|
+
end
|
109
|
+
|
102
110
|
config_attr :action_classes do
|
103
111
|
Action.all_children
|
104
112
|
end
|
data/lib/dynflow/delayed_plan.rb
CHANGED
@@ -8,7 +8,7 @@ module Dynflow
|
|
8
8
|
def initialize(world, execution_plan_uuid, start_at, start_before, args_serializer)
|
9
9
|
@world = Type! world, World
|
10
10
|
@execution_plan_uuid = Type! execution_plan_uuid, String
|
11
|
-
@start_at = Type! start_at, Time
|
11
|
+
@start_at = Type! start_at, Time, NilClass
|
12
12
|
@start_before = Type! start_before, Time, NilClass
|
13
13
|
@args_serializer = Type! args_serializer, Serializers::Abstract
|
14
14
|
end
|
@@ -42,8 +42,9 @@ module Dynflow
|
|
42
42
|
return true
|
43
43
|
end
|
44
44
|
|
45
|
-
def execute
|
46
|
-
@world.execute(execution_plan.id)
|
45
|
+
def execute(future = Concurrent.future)
|
46
|
+
@world.execute(execution_plan.id, future)
|
47
|
+
::Dynflow::World::Triggered[execution_plan.id, future]
|
47
48
|
end
|
48
49
|
|
49
50
|
def to_hash
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Dynflow
|
2
2
|
module Dispatcher
|
3
3
|
class ExecutorDispatcher < Abstract
|
4
|
-
def initialize(world)
|
4
|
+
def initialize(world, semaphore)
|
5
5
|
@world = Type! world, World
|
6
6
|
@current_futures = Set.new
|
7
7
|
end
|
@@ -19,12 +19,7 @@ module Dynflow
|
|
19
19
|
execution_lock = Coordinator::ExecutionLock.new(@world, execution.execution_plan_id, envelope.sender_id, envelope.request_id)
|
20
20
|
future = on_finish do |f|
|
21
21
|
f.then do |plan|
|
22
|
-
|
23
|
-
@world.invalidate_execution_lock(execution_lock)
|
24
|
-
else
|
25
|
-
@world.coordinator.release(execution_lock)
|
26
|
-
respond(envelope, Done)
|
27
|
-
end
|
22
|
+
when_done(plan, envelope, execution, execution_lock)
|
28
23
|
end.rescue do |reason|
|
29
24
|
@world.coordinator.release(execution_lock)
|
30
25
|
respond(envelope, Failed[reason.to_s])
|
@@ -37,6 +32,15 @@ module Dynflow
|
|
37
32
|
respond(envelope, Failed[e.message])
|
38
33
|
end
|
39
34
|
|
35
|
+
def when_done(plan, envelope, execution, execution_lock)
|
36
|
+
if plan.state == :running
|
37
|
+
@world.invalidate_execution_lock(execution_lock)
|
38
|
+
else
|
39
|
+
@world.coordinator.release(execution_lock)
|
40
|
+
respond(envelope, Done)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
40
44
|
def perform_event(envelope, event_request)
|
41
45
|
future = on_finish do |f|
|
42
46
|
f.then do
|
@@ -23,7 +23,7 @@ module Dynflow
|
|
23
23
|
@state_transitions ||= { pending: [:stopped, :scheduled, :planning],
|
24
24
|
scheduled: [:planning, :stopped],
|
25
25
|
planning: [:planned, :stopped],
|
26
|
-
planned: [:running],
|
26
|
+
planned: [:running, :stopped],
|
27
27
|
running: [:paused, :stopped],
|
28
28
|
paused: [:running],
|
29
29
|
stopped: [] }
|
@@ -152,9 +152,9 @@ module Dynflow
|
|
152
152
|
@last_step_id += 1
|
153
153
|
end
|
154
154
|
|
155
|
-
def delay(action_class, delay_options, *args)
|
155
|
+
def delay(caller_action, action_class, delay_options, *args)
|
156
156
|
save
|
157
|
-
@root_plan_step = add_scheduling_step(action_class)
|
157
|
+
@root_plan_step = add_scheduling_step(action_class, caller_action)
|
158
158
|
execution_history.add("delay", @world.id)
|
159
159
|
serializer = root_plan_step.delay(delay_options, args)
|
160
160
|
delayed_plan = DelayedPlan.new(@world,
|
@@ -280,9 +280,9 @@ module Dynflow
|
|
280
280
|
current_run_flow.add_and_resolve(@dependency_graph, new_flow) if current_run_flow
|
281
281
|
end
|
282
282
|
|
283
|
-
def add_scheduling_step(action_class)
|
283
|
+
def add_scheduling_step(action_class, caller_action = nil)
|
284
284
|
add_step(Steps::PlanStep, action_class, generate_action_id, :scheduling).tap do |step|
|
285
|
-
step.initialize_action
|
285
|
+
step.initialize_action(caller_action)
|
286
286
|
end
|
287
287
|
end
|
288
288
|
|
@@ -4,7 +4,7 @@ module Dynflow
|
|
4
4
|
|
5
5
|
def self.state_transitions
|
6
6
|
@state_transitions ||= {
|
7
|
-
pending: [:running, :skipped], # :skipped when it cannot be run because it depends on skipping step
|
7
|
+
pending: [:running, :skipped, :error], # :skipped when it cannot be run because it depends on skipping step
|
8
8
|
running: [:success, :error, :suspended],
|
9
9
|
success: [:suspended], # after not-done process_update
|
10
10
|
suspended: [:running, :error], # process_update, e.g. error in setup_progress_updates
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dynflow
|
2
|
+
module Semaphores
|
3
|
+
class Abstract
|
4
|
+
|
5
|
+
# Tries to get ticket from the semaphore
|
6
|
+
# Returns true if thing got a ticket
|
7
|
+
# Rturns false otherwise and puts the thing into the semaphore's queue
|
8
|
+
def wait(thing)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Gets first object from the queue
|
13
|
+
def get_waiting
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Checks if there are objects in the queue
|
18
|
+
def has_waiting?
|
19
|
+
raise NotImpelementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns n tickets to the semaphore
|
23
|
+
def release(n = 1)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Saves the semaphore's state to some persistent storage
|
28
|
+
def save
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tries to get n tickets
|
33
|
+
# Returns n if the semaphore has free >= n
|
34
|
+
# Returns free if n > free
|
35
|
+
def get(n = 1)
|
36
|
+
raise NotImplementedErrorn
|
37
|
+
end
|
38
|
+
|
39
|
+
# Requests all tickets
|
40
|
+
# Returns all free tickets from the semaphore
|
41
|
+
def drain
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Dynflow
|
2
|
+
module Semaphores
|
3
|
+
class Aggregating < Abstract
|
4
|
+
|
5
|
+
attr_reader :children, :waiting
|
6
|
+
|
7
|
+
def initialize(children)
|
8
|
+
@children = children
|
9
|
+
@waiting = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def wait(thing)
|
13
|
+
if get > 0
|
14
|
+
true
|
15
|
+
else
|
16
|
+
@waiting << thing
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_waiting
|
22
|
+
@waiting.shift
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_waiting?
|
26
|
+
!@waiting.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def save
|
30
|
+
@children.values.each(&:save)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(n = 1)
|
34
|
+
available = free
|
35
|
+
if n > available
|
36
|
+
drain
|
37
|
+
else
|
38
|
+
@children.values.each { |child| child.get n }
|
39
|
+
n
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def drain
|
44
|
+
available = free
|
45
|
+
@children.values.each { |child| child.get available }
|
46
|
+
available
|
47
|
+
end
|
48
|
+
|
49
|
+
def free
|
50
|
+
@children.values.map(&:free).reduce { |acc, cur| cur < acc ? cur : acc }
|
51
|
+
end
|
52
|
+
|
53
|
+
def release(n = 1, key = nil)
|
54
|
+
if key.nil?
|
55
|
+
@children.values.each { |child| child.release n }
|
56
|
+
else
|
57
|
+
@children[key].release n
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|