dynflow 0.8.9 → 0.8.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|