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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd41952d6800002a81136459150422a4e11d0c83
4
- data.tar.gz: 23d4b25ffe9bbb6c33fe82f75710d43c13d59721
3
+ metadata.gz: bc004bca5a068a20db3c14f727b365b3ac8784b8
4
+ data.tar.gz: c1c95bbb21be8430e2da6f822d77262a0e7f769f
5
5
  SHA512:
6
- metadata.gz: 6c149b85eed0f445780d0f6984cce4570cc52123a4cec9db787cf1e59826a762e2bcc05124bf71a260b6f5b51f15a21d492128631841debd125968cf79ac1ccc
7
- data.tar.gz: b9029f1873ddfc2c78c0a5f7873dd3588ce7d74b27f02dde3d2fec848ec2f89422bc25ed5fe3980e7b14e6d43dcaa986d76a9f6b26aef94ca8a1d1afc1fe128e
6
+ metadata.gz: 47a908f6fc5b80265d708be96f452450b45de7b8f9f2b63f59e623b162a2f205eebe7d72807d9e09ee10030b3f717293050ab87920ab5e302e990175f0bb9b72
7
+ data.tar.gz: 0d166b88b31788c97bb9f8dabb1a94e4a351c9df0123a1777179f0fb4fd2bc681e9f467e2a2e01d36ed736d1d9e45233c1ae069291925ee1708a8e2d0ac95bea
@@ -1,3 +1,4 @@
1
+ sudo: false
1
2
  language:
2
3
  - ruby
3
4
 
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :concurrent_ruby_ext do
6
- gem 'concurrent-ruby-ext', '~> 1.0.0'
6
+ gem 'concurrent-ruby-ext', '~> 1.0'
7
7
  end
8
8
 
9
9
  group :pry do
@@ -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.0'
25
- s.add_dependency "concurrent-ruby-edge", '~> 0.2.0'
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"
@@ -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 => object.id, :name => object.name }
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
- [CustomPassedObject.new(serialized_args[:id], serialized_args[:name])]
22
+ CustomPassedObject.new(arg[:id], arg[:name])
24
23
  end
25
24
  end
26
25
 
@@ -145,6 +145,7 @@ module Orchestrate
145
145
  end
146
146
 
147
147
  if $0 == __FILE__
148
+ ExampleHelper.world.action_logger.level = Logger::INFO
148
149
  ExampleHelper.something_should_fail!
149
150
  ExampleHelper.world.trigger(Orchestrate::CreateInfrastructure)
150
151
  Thread.new do
@@ -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
@@ -45,5 +45,7 @@ module Dynflow
45
45
  require 'dynflow/dispatcher'
46
46
  require 'dynflow/serializers'
47
47
  require 'dynflow/delayed_executors'
48
+ require 'dynflow/semaphores'
49
+ require 'dynflow/throttle_limiter'
48
50
  require 'dynflow/config'
49
51
  end
@@ -17,8 +17,8 @@ module Dynflow
17
17
  end
18
18
  end),
19
19
  (on SubPlanFinished do
20
- mark_as_done(event.execution_plan_id, event.success)
21
- try_to_finish or suspend
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
- wait_for_sub_plans(sub_plans)
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
- world.trigger { world.plan_with_caller(self, *args) }
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
@@ -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
@@ -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
- if plan.state == :running
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,8 @@
1
+ module Dynflow
2
+ module Semaphores
3
+ require 'dynflow/semaphores/abstract'
4
+ require 'dynflow/semaphores/stateful'
5
+ require 'dynflow/semaphores/aggregating'
6
+ require 'dynflow/semaphores/dummy'
7
+ end
8
+ end
@@ -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