smart_proxy_dynflow 0.2.4 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -25
  3. data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
  4. data/lib/smart_proxy_dynflow.rb +16 -1
  5. data/lib/smart_proxy_dynflow/action.rb +12 -0
  6. data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
  7. data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
  8. data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
  9. data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
  10. data/lib/smart_proxy_dynflow/action/runner.rb +76 -0
  11. data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
  12. data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
  13. data/lib/smart_proxy_dynflow/api.rb +63 -40
  14. data/lib/smart_proxy_dynflow/callback.rb +69 -25
  15. data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
  16. data/lib/smart_proxy_dynflow/core.rb +121 -0
  17. data/lib/smart_proxy_dynflow/helpers.rb +52 -6
  18. data/lib/smart_proxy_dynflow/http_config.ru +6 -16
  19. data/lib/smart_proxy_dynflow/log.rb +52 -0
  20. data/lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb +59 -0
  21. data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
  22. data/lib/smart_proxy_dynflow/plugin.rb +9 -14
  23. data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
  24. data/lib/smart_proxy_dynflow/runner.rb +10 -0
  25. data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
  26. data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
  27. data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
  28. data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
  29. data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
  30. data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
  31. data/lib/smart_proxy_dynflow/settings.rb +9 -0
  32. data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
  33. data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
  34. data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
  35. data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
  36. data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
  37. data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
  38. data/lib/smart_proxy_dynflow/task_launcher_registry.rb +31 -0
  39. data/lib/smart_proxy_dynflow/testing.rb +24 -0
  40. data/lib/smart_proxy_dynflow/ticker.rb +47 -0
  41. data/lib/smart_proxy_dynflow/version.rb +2 -2
  42. data/settings.d/dynflow.yml.example +6 -5
  43. metadata +83 -11
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
- class Dynflow
2
+ module Dynflow
3
3
  class ProxyAdapter < ::Dynflow::LoggerAdapters::Simple
4
4
  def initialize(logger, level = Logger::DEBUG, formatters = [Formatters::Exception])
5
5
  super(nil, level, formatters)
@@ -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'
@@ -0,0 +1,98 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ # Runner is an object that is able to initiate some action and
4
+ # provide update data on refresh call.
5
+ class Base
6
+ attr_reader :id
7
+ attr_writer :logger
8
+
9
+ def initialize(*_args, suspended_action: nil)
10
+ @suspended_action = suspended_action
11
+ @id = SecureRandom.uuid
12
+ initialize_continuous_outputs
13
+ end
14
+
15
+ def logger
16
+ @logger ||= Logger.new(STDERR)
17
+ end
18
+
19
+ def run_refresh
20
+ logger.debug('refreshing runner')
21
+ refresh
22
+ generate_updates
23
+ end
24
+
25
+ # by default, external event just causes the refresh to be triggered: this allows the descendants
26
+ # of the Base to add custom logic to process the external events.
27
+ # Similarly as `run_refresh`, it's expected to return updates to be dispatched.
28
+ def external_event(_event)
29
+ run_refresh
30
+ end
31
+
32
+ def start
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def refresh
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def kill
41
+ # Override when you can kill the runner in the middle
42
+ end
43
+
44
+ def close
45
+ # if cleanup is needed
46
+ end
47
+
48
+ def timeout
49
+ # Override when timeouts and regular kills should be handled differently
50
+ publish_data('Timeout for execution passed, trying to stop the job', 'debug')
51
+ kill
52
+ end
53
+
54
+ def timeout_interval
55
+ # A number of seconds after which the runner should receive a #timeout
56
+ # or nil for no timeout
57
+ end
58
+
59
+ def publish_data(data, type)
60
+ @continuous_output.add_output(data, type)
61
+ end
62
+
63
+ def publish_exception(context, exception, fatal = true)
64
+ logger.error("#{context} - #{exception.class} #{exception.message}:\n" + \
65
+ exception.backtrace.join("\n"))
66
+ dispatch_exception context, exception
67
+ publish_exit_status('EXCEPTION') if fatal
68
+ end
69
+
70
+ def publish_exit_status(status)
71
+ @exit_status = status
72
+ end
73
+
74
+ def dispatch_exception(context, exception)
75
+ @continuous_output.add_exception(context, exception)
76
+ end
77
+
78
+ def generate_updates
79
+ return no_update if @continuous_output.empty? && @exit_status.nil?
80
+ new_data = @continuous_output
81
+ @continuous_output = Proxy::Dynflow::ContinuousOutput.new
82
+ new_update(new_data, @exit_status)
83
+ end
84
+
85
+ def no_update
86
+ {}
87
+ end
88
+
89
+ def new_update(data, exit_status)
90
+ { @suspended_action => Runner::Update.new(data, exit_status) }
91
+ end
92
+
93
+ def initialize_continuous_outputs
94
+ @continuous_output = ::Proxy::Dynflow::ContinuousOutput.new
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,40 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ module Command
4
+ def initialize_command(*command)
5
+ @command_out, @command_in, @command_pid = PTY.spawn(*command)
6
+ rescue Errno::ENOENT => e
7
+ publish_exception("Error running command '#{command.join(' ')}'", e)
8
+ end
9
+
10
+ def refresh
11
+ return if @command_out.nil?
12
+ ready_outputs, * = IO.select([@command_out], nil, nil, 0.1)
13
+ if ready_outputs
14
+ if @command_out.nread.positive?
15
+ lines = @command_out.read_nonblock(@command_out.nread)
16
+ else
17
+ close_io
18
+ Process.wait(@command_pid)
19
+ publish_exit_status($CHILD_STATUS.exitstatus)
20
+ end
21
+ publish_data(lines, 'stdout') if lines && !lines.empty?
22
+ end
23
+ end
24
+
25
+ def close
26
+ close_io
27
+ end
28
+
29
+ private
30
+
31
+ def close_io
32
+ @command_out.close if @command_out && !@command_out.closed?
33
+ @command_out = nil
34
+
35
+ @command_in.close if @command_in && !@command_in.closed?
36
+ @command_in = nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -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