smart_proxy_dynflow 0.4.0 → 0.6.0

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -7
  3. data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
  4. data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
  5. data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
  6. data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
  7. data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
  8. data/lib/smart_proxy_dynflow/action/runner.rb +81 -0
  9. data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
  10. data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
  11. data/lib/smart_proxy_dynflow/action.rb +12 -0
  12. data/lib/smart_proxy_dynflow/api.rb +1 -1
  13. data/lib/smart_proxy_dynflow/callback.rb +8 -44
  14. data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
  15. data/lib/smart_proxy_dynflow/core.rb +2 -2
  16. data/lib/smart_proxy_dynflow/helpers.rb +11 -6
  17. data/lib/smart_proxy_dynflow/log.rb +1 -1
  18. data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
  19. data/lib/smart_proxy_dynflow/plugin.rb +9 -1
  20. data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
  21. data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
  22. data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
  23. data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
  24. data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
  25. data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
  26. data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
  27. data/lib/smart_proxy_dynflow/runner.rb +10 -0
  28. data/lib/smart_proxy_dynflow/settings.rb +1 -1
  29. data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
  30. data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
  31. data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
  32. data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
  33. data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
  34. data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
  35. data/lib/smart_proxy_dynflow/task_launcher_registry.rb +1 -1
  36. data/lib/smart_proxy_dynflow/testing.rb +1 -1
  37. data/lib/smart_proxy_dynflow/ticker.rb +47 -0
  38. data/lib/smart_proxy_dynflow/version.rb +2 -2
  39. data/lib/smart_proxy_dynflow.rb +2 -7
  40. metadata +57 -5
@@ -0,0 +1,11 @@
1
+ require 'io/wait'
2
+ require 'pty'
3
+ require 'smart_proxy_dynflow/runner/command'
4
+
5
+ module Proxy::Dynflow
6
+ module Runner
7
+ class CommandRunner < Base
8
+ include Command
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,191 @@
1
+ require 'smart_proxy_dynflow/ticker'
2
+
3
+ module Proxy::Dynflow
4
+ module Runner
5
+ class Dispatcher
6
+ def self.instance
7
+ return @instance if @instance
8
+ @instance = new(Proxy::Dynflow::Core.world.clock,
9
+ Proxy::Dynflow::Core.world.logger)
10
+ end
11
+
12
+ class RunnerActor < ::Dynflow::Actor
13
+ def initialize(dispatcher, suspended_action, runner, clock, logger, _options = {})
14
+ @dispatcher = dispatcher
15
+ @clock = clock
16
+ @ticker = dispatcher.ticker
17
+ @logger = logger
18
+ @suspended_action = suspended_action
19
+ @runner = runner
20
+ @finishing = false
21
+ end
22
+
23
+ def on_envelope(*args)
24
+ super
25
+ rescue => e
26
+ handle_exception(e)
27
+ end
28
+
29
+ def start_runner
30
+ @logger.debug("start runner #{@runner.id}")
31
+ set_timeout if @runner.timeout_interval
32
+ @runner.start
33
+ refresh_runner
34
+ ensure
35
+ plan_next_refresh
36
+ end
37
+
38
+ def refresh_runner
39
+ @logger.debug("refresh runner #{@runner.id}")
40
+ dispatch_updates(@runner.run_refresh)
41
+ ensure
42
+ @refresh_planned = false
43
+ plan_next_refresh
44
+ end
45
+
46
+ def dispatch_updates(updates)
47
+ updates.each { |receiver, update| (receiver || @suspended_action) << update }
48
+
49
+ # This is a workaround when the runner does not accept the suspended action
50
+ main_key = updates.keys.any?(&:nil?) ? nil : @suspended_action
51
+ main_process = updates[main_key]
52
+ finish if main_process&.exit_status
53
+ end
54
+
55
+ def timeout_runner
56
+ @logger.debug("timeout runner #{@runner.id}")
57
+ @runner.timeout
58
+ rescue => e
59
+ handle_exception(e, false)
60
+ end
61
+
62
+ def kill
63
+ @logger.debug("kill runner #{@runner.id}")
64
+ @runner.kill
65
+ rescue => e
66
+ handle_exception(e, false)
67
+ end
68
+
69
+ def finish
70
+ @logger.debug("finish runner #{@runner.id}")
71
+ @finishing = true
72
+ @dispatcher.finish(@runner.id)
73
+ end
74
+
75
+ def start_termination(*args)
76
+ @logger.debug("terminate #{@runner.id}")
77
+ super
78
+ @runner.close
79
+ finish_termination
80
+ end
81
+
82
+ def external_event(event)
83
+ dispatch_updates(@runner.external_event(event))
84
+ end
85
+
86
+ private
87
+
88
+ def set_timeout
89
+ timeout_time = Time.now.getlocal + @runner.timeout_interval
90
+ @logger.debug("setting timeout for #{@runner.id} to #{timeout_time}")
91
+ @clock.ping(reference, timeout_time, :timeout_runner)
92
+ end
93
+
94
+ def plan_next_refresh
95
+ if !@finishing && !@refresh_planned
96
+ @logger.debug("planning to refresh #{@runner.id}")
97
+ @ticker.tell([:add_event, reference, :refresh_runner])
98
+ @refresh_planned = true
99
+ end
100
+ end
101
+
102
+ def handle_exception(exception, fatal = true)
103
+ @dispatcher.handle_command_exception(@runner.id, exception, fatal)
104
+ end
105
+ end
106
+
107
+ attr_reader :ticker
108
+ def initialize(clock, logger)
109
+ @mutex = Mutex.new
110
+ @clock = clock
111
+ @logger = logger
112
+ @ticker = ::Proxy::Dynflow::Ticker.spawn('dispatcher-ticker', @clock, @logger, refresh_interval)
113
+ @runner_actors = {}
114
+ @runner_suspended_actions = {}
115
+ end
116
+
117
+ def synchronize(&block)
118
+ @mutex.synchronize(&block)
119
+ end
120
+
121
+ def start(suspended_action, runner)
122
+ synchronize do
123
+ raise "Actor with runner id #{runner.id} already exists" if @runner_actors[runner.id]
124
+ runner.logger = @logger
125
+ runner_actor = RunnerActor.spawn("runner-actor-#{runner.id}", self, suspended_action, runner, @clock, @logger)
126
+ @runner_actors[runner.id] = runner_actor
127
+ @runner_suspended_actions[runner.id] = suspended_action
128
+ runner_actor.tell(:start_runner)
129
+ return runner.id
130
+ rescue => exception
131
+ _handle_command_exception(runner.id, exception)
132
+ return nil
133
+ end
134
+ end
135
+
136
+ def kill(runner_id)
137
+ synchronize do
138
+ runner_actor = @runner_actors[runner_id]
139
+ runner_actor&.tell(:kill)
140
+ rescue => exception
141
+ _handle_command_exception(runner_id, exception, false)
142
+ end
143
+ end
144
+
145
+ def finish(runner_id)
146
+ synchronize do
147
+ _finish(runner_id)
148
+ rescue => exception
149
+ _handle_command_exception(runner_id, exception, false)
150
+ end
151
+ end
152
+
153
+ def external_event(runner_id, external_event)
154
+ synchronize do
155
+ runner_actor = @runner_actors[runner_id]
156
+ runner_actor&.tell([:external_event, external_event])
157
+ end
158
+ end
159
+
160
+ def handle_command_exception(*args)
161
+ synchronize { _handle_command_exception(*args) }
162
+ end
163
+
164
+ def refresh_interval
165
+ 1
166
+ end
167
+
168
+ private
169
+
170
+ def _finish(runner_id)
171
+ runner_actor = @runner_actors.delete(runner_id)
172
+ return unless runner_actor
173
+ @logger.debug("closing session for command [#{runner_id}]," \
174
+ "#{@runner_actors.size} actors left ")
175
+ runner_actor.tell([:start_termination, Concurrent::Promises.resolvable_future])
176
+ ensure
177
+ @runner_suspended_actions.delete(runner_id)
178
+ end
179
+
180
+ def _handle_command_exception(runner_id, exception, fatal = true)
181
+ @logger.error("error while dispatching request to runner #{runner_id}:"\
182
+ "#{exception.class} #{exception.message}:\n #{exception.backtrace.join("\n")}")
183
+ suspended_action = @runner_suspended_actions[runner_id]
184
+ if suspended_action
185
+ suspended_action << Runner::Update.encode_exception('Runner error', exception, fatal)
186
+ end
187
+ _finish(runner_id) if fatal
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,57 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ class Parent < Base
4
+ # targets = { identifier => { :execution_plan_id => "...", :run_step_id => id,
5
+ # :input => { ... } }
6
+ def initialize(targets = {}, suspended_action: nil)
7
+ @targets = targets
8
+ @exit_statuses = {}
9
+ super suspended_action: suspended_action
10
+ end
11
+
12
+ def generate_updates
13
+ base = {}
14
+ base[@suspended_action] = Runner::Update.new(Proxy::Dynflow::ContinuousOutput.new, @exit_status) if @exit_status
15
+ # Operate on all hosts if the main process ended or only on hosts for which we have updates
16
+ @outputs.reject { |_, output| @exit_status.nil? && output.empty? }
17
+ .reduce(base) do |acc, (identifier, output)|
18
+ @outputs[identifier] = Proxy::Dynflow::ContinuousOutput.new # Create a new ContinuousOutput for next round of updates
19
+ exit_status = @exit_statuses[identifier] || @exit_status if @exit_status
20
+ acc.merge(host_action(identifier) => Runner::Update.new(output, exit_status))
21
+ end
22
+ end
23
+
24
+ def initialize_continuous_outputs
25
+ @outputs = @targets.keys.reduce({}) do |acc, target|
26
+ acc.merge(target => Proxy::Dynflow::ContinuousOutput.new)
27
+ end
28
+ end
29
+
30
+ def host_action(identifier)
31
+ options = @targets[identifier].slice('execution_plan_id', 'run_step_id')
32
+ .merge(:world => Proxy::Dynflow::Core.world)
33
+ Dynflow::Action::Suspended.new OpenStruct.new(options)
34
+ end
35
+
36
+ def broadcast_data(data, type)
37
+ @outputs.each_value { |output| output.add_output(data, type) }
38
+ end
39
+
40
+ def publish_data(_data, _type)
41
+ true
42
+ end
43
+
44
+ def publish_data_for(identifier, data, type)
45
+ @outputs[identifier].add_output(data, type)
46
+ end
47
+
48
+ def dispatch_exception(context, exception)
49
+ @outputs.each_value { |output| output.add_exception(context, exception) }
50
+ end
51
+
52
+ def publish_exit_status_for(identifier, exit_status)
53
+ @exit_statuses[identifier] = exit_status
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ require 'smart_proxy_dynflow/continuous_output'
2
+
3
+ module Proxy::Dynflow
4
+ module Runner
5
+ # Runner::Update represents chunk of data produced by runner that
6
+ # can be consumed by other components, such as RunnerAction
7
+ class Update
8
+ attr_reader :continuous_output, :exit_status
9
+ def initialize(continuous_output, exit_status)
10
+ @continuous_output = continuous_output
11
+ @exit_status = exit_status
12
+ end
13
+
14
+ def self.encode_exception(context, exception, fatal = true)
15
+ continuous_output = ::Proxy::Dynflow::ContinuousOutput.new
16
+ continuous_output.add_exception(context, exception)
17
+ new(continuous_output, fatal ? 'EXCEPTION' : nil)
18
+ end
19
+ end
20
+
21
+ class ExternalEvent
22
+ attr_reader :data
23
+ def initialize(data = {})
24
+ @data = data
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ end
4
+ end
5
+
6
+ require 'smart_proxy_dynflow/runner/update'
7
+ require 'smart_proxy_dynflow/runner/base'
8
+ require 'smart_proxy_dynflow/runner/dispatcher'
9
+ require 'smart_proxy_dynflow/action/runner'
10
+ require 'smart_proxy_dynflow/runner/parent'
@@ -1,6 +1,6 @@
1
1
  # require 'ostruct'
2
2
 
3
- class Proxy::Dynflow
3
+ module Proxy::Dynflow
4
4
  class Settings
5
5
  def self.instance
6
6
  Proxy::Dynflow::Plugin.settings
@@ -0,0 +1,53 @@
1
+ module Proxy::Dynflow
2
+ module SettingsLoader
3
+ def self.settings_registry
4
+ @settings_registry ||= {}
5
+ end
6
+
7
+ def self.name_to_settings
8
+ @name_to_settings ||= {}
9
+ end
10
+
11
+ def self.settings_keys
12
+ @settings_keys ||= []
13
+ end
14
+
15
+ def self.settings_registered?(name)
16
+ name_to_settings.key?(name)
17
+ end
18
+
19
+ def self.register_settings(names, object)
20
+ names = [names] unless names.is_a? Array
21
+ names.each do |name|
22
+ raise 'settings name has to be a symbol' unless name.is_a? Symbol
23
+ raise "settings #{name} already registered" if SettingsLoader.settings_registered?(name)
24
+ name_to_settings[name] = object
25
+ end
26
+ settings_registry[names] = object
27
+ end
28
+
29
+ def self.setup_settings(name, settings)
30
+ raise "Settings for #{name} were not registered" unless settings_registered?(name)
31
+ name_to_settings[name].initialize_settings(settings)
32
+ end
33
+
34
+ def register_settings(names, defaults = {})
35
+ SettingsLoader.register_settings(names, self)
36
+ @defaults = defaults
37
+ end
38
+
39
+ def initialize_settings(settings = {})
40
+ @settings = @defaults.merge(settings)
41
+ validate_settings!
42
+ end
43
+
44
+ def settings
45
+ raise "Settings for #{self} not initalized" unless @settings
46
+ @settings
47
+ end
48
+
49
+ def validate_settings!
50
+ raise 'Only symbols expected in keys' unless @settings.keys.all? { |key| key.is_a? Symbol }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Abstract
4
+ attr_reader :callback, :options, :results, :world
5
+ def initialize(world, callback, options = {})
6
+ @world = world
7
+ @callback = callback
8
+ @options = options
9
+ @results = {}
10
+ end
11
+
12
+ def launch!(_input)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def self.input_format; end
17
+
18
+ private
19
+
20
+ def format_result(result)
21
+ if result.triggered?
22
+ { :result => 'success', :task_id => result.execution_plan_id }
23
+ else
24
+ plan = world.persistence.load_execution_plan(result.id)
25
+ { :result => 'error', :errors => plan.errors }
26
+ end
27
+ end
28
+
29
+ def action_class(input)
30
+ options[:action_class_override] || ::Dynflow::Utils.constantize(input['action_class'])
31
+ end
32
+
33
+ def with_callback(input)
34
+ input.merge(:callback_host => callback)
35
+ end
36
+
37
+ def trigger(parent, klass, *input)
38
+ world.trigger do
39
+ world.plan_with_options(caller_action: parent, action_class: klass, args: input)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Batch < Abstract
4
+ def launch!(input)
5
+ trigger(nil, Proxy::Dynflow::Action::Batch, self, input)
6
+ end
7
+
8
+ def launch_children(parent, input_hash)
9
+ input_hash.each do |task_id, input|
10
+ launcher = child_launcher(parent)
11
+ launcher.launch!(transform_input(input))
12
+ results[task_id] = launcher.results
13
+ end
14
+ end
15
+
16
+ def prepare_batch(input_hash)
17
+ success_tasks = input_hash.select do |task_id, _input|
18
+ results[task_id][:result] == 'success'
19
+ end
20
+ success_tasks.reduce({}) do |acc, (key, value)|
21
+ acc.merge(results[key][:task_id] => value['action_input']['callback'])
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def child_launcher(parent)
28
+ Single.new(world, callback, :parent => parent)
29
+ end
30
+
31
+ # Identity by default
32
+ def transform_input(input)
33
+ input
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ require 'smart_proxy_dynflow/runner'
2
+
3
+ module Proxy::Dynflow
4
+ module TaskLauncher
5
+ class AbstractGroup < Batch
6
+ def self.runner_class
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def launch!(input)
11
+ trigger(nil, Action::SingleRunnerBatch, self, input)
12
+ end
13
+
14
+ def launch_children(parent, input_hash)
15
+ super(parent, input_hash)
16
+ trigger(parent, Action::BatchRunner, self, input_hash)
17
+ end
18
+
19
+ def operation
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def runner_input(input)
24
+ input.reduce({}) do |acc, (id, input)|
25
+ input = { :execution_plan_id => results[id][:task_id],
26
+ :run_step_id => 2,
27
+ :input => input }
28
+ acc.merge(id => input)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def child_launcher(parent)
35
+ Single.new(world, callback, :parent => parent, :action_class_override => Action::OutputCollector)
36
+ end
37
+
38
+ def transform_input(input)
39
+ wipe_callback(input)
40
+ end
41
+
42
+ def wipe_callback(input)
43
+ callback = input['action_input']['callback']
44
+ input.merge('action_input' => input['action_input'].merge('callback' => nil, :task_id => callback['task_id']))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Single < Abstract
4
+ def self.input_format
5
+ { :action_class => "MyActionClass", :action_input => {} }
6
+ end
7
+
8
+ def launch!(input)
9
+ triggered = trigger(options[:parent],
10
+ action_class(input),
11
+ with_callback(input.fetch('action_input', {})))
12
+ @results = format_result(triggered)
13
+ triggered
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ end
4
+ end
5
+
6
+ require 'smart_proxy_dynflow/task_launcher/abstract'
7
+ require 'smart_proxy_dynflow/task_launcher/single'
8
+ require 'smart_proxy_dynflow/task_launcher/batch'
9
+ require 'smart_proxy_dynflow/task_launcher/group'
@@ -1,4 +1,4 @@
1
- class Proxy::Dynflow
1
+ module Proxy::Dynflow
2
2
  class TaskLauncherRegistry
3
3
  class << self
4
4
  def register(name, launcher)
@@ -4,7 +4,7 @@ unless defined? DYNFLOW_TESTING_LOG_LEVEL
4
4
  DYNFLOW_TESTING_LOG_LEVEL = 4
5
5
  end
6
6
 
7
- class Proxy::Dynflow
7
+ module Proxy::Dynflow
8
8
  # Helper for usage in other dependent plugins that need Dynflow
9
9
  # related things, such as testing instance of world etc.
10
10
  module Testing
@@ -0,0 +1,47 @@
1
+ require 'dynflow'
2
+
3
+ module Proxy::Dynflow
4
+ class Ticker < ::Dynflow::Actor
5
+ attr_reader :clock
6
+
7
+ def initialize(clock, logger, refresh_interval)
8
+ @clock = clock
9
+ @logger = logger
10
+ @events = []
11
+ @refresh_interval = refresh_interval
12
+ plan_next_tick
13
+ end
14
+
15
+ def tick
16
+ @logger.debug("Ticker ticking for #{@events.size} events")
17
+ @events.each do |(target, args)|
18
+ pass_event(target, args)
19
+ end
20
+ @events = []
21
+ ensure
22
+ @planned = false
23
+ plan_next_tick
24
+ end
25
+
26
+ def add_event(target, args)
27
+ @events << [target, args]
28
+ plan_next_tick
29
+ end
30
+
31
+ private
32
+
33
+ def pass_event(target, args)
34
+ target.tell(args)
35
+ rescue => e
36
+ @logger.error("Failed passing event to #{target} with #{args}")
37
+ @logger.error(e)
38
+ end
39
+
40
+ def plan_next_tick
41
+ if !@planned && !@events.empty?
42
+ @clock.ping(reference, Time.now.getlocal + @refresh_interval, :tick)
43
+ @planned = true
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
- class Dynflow
3
- VERSION = '0.4.0'.freeze
2
+ module Dynflow
3
+ VERSION = '0.6.0'.freeze
4
4
  end
5
5
  end
@@ -3,10 +3,9 @@ require 'dynflow'
3
3
  require 'smart_proxy_dynflow/task_launcher_registry'
4
4
  require 'smart_proxy_dynflow/middleware/keep_current_request_id'
5
5
 
6
- require 'foreman_tasks_core'
7
-
8
6
  require 'smart_proxy_dynflow/log'
9
7
  require 'smart_proxy_dynflow/settings'
8
+ require 'smart_proxy_dynflow/ticker'
10
9
  require 'smart_proxy_dynflow/core'
11
10
  require 'smart_proxy_dynflow/callback'
12
11
 
@@ -16,10 +15,6 @@ require 'smart_proxy_dynflow/helpers'
16
15
  require 'smart_proxy_dynflow/api'
17
16
 
18
17
  module Proxy
19
- class Dynflow
20
- Core.after_initialize do |dynflow_core|
21
- ForemanTasksCore.dynflow_setup(dynflow_core.world)
22
- end
23
- Core.register_silencer_matchers ForemanTasksCore.silent_dead_letter_matchers
18
+ module Dynflow
24
19
  end
25
20
  end