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.
- checksums.yaml +4 -4
- data/Gemfile +6 -25
- data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
- data/lib/smart_proxy_dynflow.rb +16 -1
- data/lib/smart_proxy_dynflow/action.rb +12 -0
- data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
- data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
- data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
- data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
- data/lib/smart_proxy_dynflow/action/runner.rb +76 -0
- data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
- data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
- data/lib/smart_proxy_dynflow/api.rb +63 -40
- data/lib/smart_proxy_dynflow/callback.rb +69 -25
- data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
- data/lib/smart_proxy_dynflow/core.rb +121 -0
- data/lib/smart_proxy_dynflow/helpers.rb +52 -6
- data/lib/smart_proxy_dynflow/http_config.ru +6 -16
- data/lib/smart_proxy_dynflow/log.rb +52 -0
- data/lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb +59 -0
- data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
- data/lib/smart_proxy_dynflow/plugin.rb +9 -14
- data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
- data/lib/smart_proxy_dynflow/runner.rb +10 -0
- data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
- data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
- data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
- data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
- data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
- data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
- data/lib/smart_proxy_dynflow/settings.rb +9 -0
- data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
- data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
- data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
- data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
- data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
- data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
- data/lib/smart_proxy_dynflow/task_launcher_registry.rb +31 -0
- data/lib/smart_proxy_dynflow/testing.rb +24 -0
- data/lib/smart_proxy_dynflow/ticker.rb +47 -0
- data/lib/smart_proxy_dynflow/version.rb +2 -2
- data/settings.d/dynflow.yml.example +6 -5
- metadata +83 -11
@@ -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,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
|