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