smart_proxy_dynflow 0.2.3 → 0.5.1

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 (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 -12
@@ -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