smart_proxy_dynflow 0.6.3 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/smart_proxy_dynflow/action/batch.rb +7 -6
- data/lib/smart_proxy_dynflow/action/batch_callback.rb +6 -1
- data/lib/smart_proxy_dynflow/action/batch_runner.rb +3 -3
- data/lib/smart_proxy_dynflow/action/external_polling.rb +16 -0
- data/lib/smart_proxy_dynflow/action/output_collector.rb +1 -0
- data/lib/smart_proxy_dynflow/action/runner.rb +9 -0
- data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +2 -14
- data/lib/smart_proxy_dynflow/core.rb +3 -0
- data/lib/smart_proxy_dynflow/helpers.rb +14 -3
- data/lib/smart_proxy_dynflow/io_buffer.rb +115 -0
- data/lib/smart_proxy_dynflow/plugin.rb +6 -0
- data/lib/smart_proxy_dynflow/process_manager.rb +168 -0
- data/lib/smart_proxy_dynflow/runner/base.rb +8 -2
- data/lib/smart_proxy_dynflow/runner/command.rb +13 -0
- data/lib/smart_proxy_dynflow/runner/command_runner.rb +1 -0
- data/lib/smart_proxy_dynflow/runner/dispatcher.rb +11 -0
- data/lib/smart_proxy_dynflow/runner/parent.rb +2 -2
- data/lib/smart_proxy_dynflow/runner/process_manager_command.rb +44 -0
- data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +10 -2
- data/lib/smart_proxy_dynflow/task_launcher/batch.rb +12 -11
- data/lib/smart_proxy_dynflow/task_launcher/group.rb +10 -4
- data/lib/smart_proxy_dynflow/task_launcher/single.rb +3 -2
- data/lib/smart_proxy_dynflow/version.rb +1 -1
- data/settings.d/dynflow.yml.example +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c9cd93e1cf950c5f7977d700a2d8b307faa98182af93063c55ced580617d054
|
4
|
+
data.tar.gz: 2de7767e855c5914d6db7678ef2c2302c414a20b04cb6dd4df7294263ee00f95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b03995ac1c1bf44c900926371d3b42c56f05dc8b994fb07f3c87613e788a990e9ec597d67bb24a97279ae4bc0557fca01dabfbaace26299553467f8173ff8f73
|
7
|
+
data.tar.gz: 0d4737d0298d940988c51ba62e04c45740530660ae76c492e706459965446e984f2ddeb319651a416864636dca407d4f281029bb75da9ad747da7f6b6d71ee42
|
@@ -3,15 +3,16 @@ module Proxy::Dynflow::Action
|
|
3
3
|
include Dynflow::Action::WithSubPlans
|
4
4
|
include Dynflow::Action::WithPollingSubPlans
|
5
5
|
|
6
|
-
# {
|
6
|
+
# { execution_plan_uuid => { :action_class => Klass, :input => input } }
|
7
7
|
def plan(launcher, input_hash)
|
8
|
-
|
9
|
-
|
8
|
+
plan_self :input_hash => input_hash,
|
9
|
+
:launcher => launcher.to_hash
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
12
|
+
def create_sub_plans
|
13
|
+
Proxy::Dynflow::TaskLauncher::Abstract
|
14
|
+
.new_from_hash(world, input[:launcher])
|
15
|
+
.launch_children(self, input[:input_hash])
|
15
16
|
end
|
16
17
|
|
17
18
|
def rescue_strategy
|
@@ -1,7 +1,12 @@
|
|
1
1
|
module Proxy::Dynflow::Action
|
2
2
|
class BatchCallback < ::Dynflow::Action
|
3
3
|
def plan(input_hash, results)
|
4
|
-
|
4
|
+
# In input_hash there are complete inputs for all the actions for which this is reporting
|
5
|
+
# Trim it down to only the bare minimum we actually need
|
6
|
+
callbacks = input_hash.reduce({}) do |acc, (key, value)|
|
7
|
+
acc.merge(key => value['action_input']['callback'])
|
8
|
+
end
|
9
|
+
plan_self :targets => callbacks, :results => results
|
5
10
|
end
|
6
11
|
|
7
12
|
def run
|
@@ -2,13 +2,13 @@ require 'smart_proxy_dynflow/action/runner'
|
|
2
2
|
|
3
3
|
module Proxy::Dynflow::Action
|
4
4
|
class BatchRunner < ::Proxy::Dynflow::Action::Runner
|
5
|
-
def plan(launcher, input)
|
6
|
-
plan_self :targets => launcher.runner_input(input), :operation => launcher.operation
|
5
|
+
def plan(launcher, input, runner_id)
|
6
|
+
plan_self :targets => launcher.runner_input(input), :operation => launcher.operation, :runner_id => runner_id
|
7
7
|
end
|
8
8
|
|
9
9
|
def initiate_runner
|
10
10
|
launcher = Proxy::Dynflow::TaskLauncherRegistry.fetch(input[:operation])
|
11
|
-
launcher.runner_class.new(input[:targets], suspended_action: suspended_action)
|
11
|
+
launcher.runner_class.new(input[:targets], suspended_action: suspended_action, id: input[:runner_id])
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'smart_proxy_dynflow/action/shareable'
|
2
|
+
require 'smart_proxy_dynflow/action/external_polling'
|
2
3
|
module Proxy::Dynflow
|
3
4
|
module Action
|
4
5
|
class Runner < Shareable
|
5
6
|
include ::Dynflow::Action::Cancellable
|
7
|
+
include ::Proxy::Dynflow::Action::WithExternalPolling
|
6
8
|
|
7
9
|
def run(event = nil)
|
8
10
|
case event
|
@@ -14,6 +16,9 @@ module Proxy::Dynflow
|
|
14
16
|
process_external_event(event)
|
15
17
|
when ::Dynflow::Action::Cancellable::Cancel
|
16
18
|
kill_run
|
19
|
+
when ::Proxy::Dynflow::Action::WithExternalPolling::Poll
|
20
|
+
poll
|
21
|
+
suspend
|
17
22
|
else
|
18
23
|
raise "Unexpected event #{event.inspect}"
|
19
24
|
end
|
@@ -69,6 +74,10 @@ module Proxy::Dynflow
|
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
77
|
+
def poll
|
78
|
+
runner_dispatcher.refresh_output(output[:runner_id])
|
79
|
+
end
|
80
|
+
|
72
81
|
def failed_run?
|
73
82
|
output[:exit_status] != 0
|
74
83
|
end
|
@@ -1,20 +1,8 @@
|
|
1
1
|
module Proxy::Dynflow::Action
|
2
2
|
class SingleRunnerBatch < Batch
|
3
3
|
def plan(launcher, input_hash)
|
4
|
-
|
5
|
-
|
6
|
-
results = plan_self
|
7
|
-
plan_action BatchCallback, launcher.prepare_batch(input_hash), results.output[:results]
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def run(event = nil)
|
12
|
-
super unless event == Dynflow::Action::Skip
|
13
|
-
end
|
14
|
-
|
15
|
-
def initiate
|
16
|
-
ping suspended_action
|
17
|
-
wait_for_sub_plans sub_plans
|
4
|
+
results = super
|
5
|
+
plan_action BatchCallback, input_hash, results.output[:results]
|
18
6
|
end
|
19
7
|
|
20
8
|
def check_for_errors!(optional = true)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
1
3
|
module Proxy::Dynflow
|
2
4
|
class Core
|
3
5
|
attr_accessor :world, :accepted_cert_serial
|
@@ -29,6 +31,7 @@ module Proxy::Dynflow
|
|
29
31
|
Log.instance.warn "Could not open DB for dynflow at '#{db_file}', " \
|
30
32
|
"will keep data in memory. Restart will drop all dynflow data."
|
31
33
|
else
|
34
|
+
FileUtils.mkdir_p(File.dirname(db_file))
|
32
35
|
db_conn_string += "/#{db_file}"
|
33
36
|
end
|
34
37
|
|
@@ -36,9 +36,8 @@ module Proxy
|
|
36
36
|
def task_status(task_id)
|
37
37
|
ep = world.persistence.load_execution_plan(task_id)
|
38
38
|
actions = ep.actions.map do |action|
|
39
|
-
|
40
|
-
|
41
|
-
hash
|
39
|
+
refresh_output(ep, action)
|
40
|
+
expand_output(action)
|
42
41
|
end
|
43
42
|
ep.to_hash.merge(:actions => actions)
|
44
43
|
rescue KeyError => _e
|
@@ -58,6 +57,18 @@ module Proxy
|
|
58
57
|
params['step_id'].to_i,
|
59
58
|
::Proxy::Dynflow::Runner::ExternalEvent.new(params))
|
60
59
|
end
|
60
|
+
|
61
|
+
def refresh_output(execution_plan, action)
|
62
|
+
if action.is_a?(Proxy::Dynflow::Action::WithExternalPolling) && %i[running suspended].include?(action.run_step&.state)
|
63
|
+
world.event(execution_plan.id, action.run_step_id, Proxy::Dynflow::Action::WithExternalPolling::Poll)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def expand_output(action)
|
68
|
+
hash = action.to_hash
|
69
|
+
hash[:output][:result] = action.output_result if action.is_a?(Proxy::Dynflow::Action::Runner)
|
70
|
+
hash
|
71
|
+
end
|
61
72
|
end
|
62
73
|
end
|
63
74
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Proxy
|
2
|
+
module Dynflow
|
3
|
+
# A buffer around an IO object providing buffering and convenience methods
|
4
|
+
# for non-blocking reads and writes.
|
5
|
+
#
|
6
|
+
# @note Using a single IOBuffer with a single IO for both reads and writes might not be a good idea. If you need to use a single IO for both reads and writes, wrap it in two separate IOBuffers.
|
7
|
+
#
|
8
|
+
# @attr_accessor [IO] io The IO which the buffer wraps
|
9
|
+
# @attr_reader [String] buffer The buffer where the data read from the underlying IO is buffered
|
10
|
+
class IOBuffer
|
11
|
+
attr_accessor :io
|
12
|
+
attr_reader :buffer
|
13
|
+
|
14
|
+
# @param [IO] io The IO object to be buffered
|
15
|
+
def initialize(io)
|
16
|
+
@buffer = ''
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sets a callback to be executed each time data is read from the
|
21
|
+
# underlying IO.
|
22
|
+
#
|
23
|
+
# @note Note that if the callback is provided, the buffer will store the return value of the callback instead of the raw data.
|
24
|
+
#
|
25
|
+
# @yieldparam [String] data read from the underlying IO
|
26
|
+
# @yieldreturn [String] data to be buffered
|
27
|
+
# @return [void]
|
28
|
+
def on_data(&block)
|
29
|
+
@callback = block
|
30
|
+
end
|
31
|
+
|
32
|
+
# Exposes the underlying IO so that the buffer itself can be used in IO.select calls.
|
33
|
+
#
|
34
|
+
# @return [IO] the underlying IO
|
35
|
+
def to_io
|
36
|
+
@io
|
37
|
+
end
|
38
|
+
|
39
|
+
# Exposes the contents of the buffer as a String
|
40
|
+
#
|
41
|
+
# @return [String] the buffered data
|
42
|
+
def to_s
|
43
|
+
@buffer
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks whether the buffer is empty
|
47
|
+
#
|
48
|
+
# @return [true, false] whether the buffer is empty
|
49
|
+
def empty?
|
50
|
+
@buffer.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks whether the underlying IO is empty
|
54
|
+
#
|
55
|
+
# @return [true, false] whether the underlying IO is empty
|
56
|
+
def closed?
|
57
|
+
@io.closed?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Closes the underlying IO. Does nothing if the IO is already closed.
|
61
|
+
#
|
62
|
+
# @return [void]
|
63
|
+
def close
|
64
|
+
@io.close unless @io.closed?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reads all the data that is currently waiting in the IO and stores it. If
|
68
|
+
# EOFError is encountered during the read, the underlying IO is closed.
|
69
|
+
#
|
70
|
+
# @return [void]
|
71
|
+
def read_available!
|
72
|
+
data = ''
|
73
|
+
loop { data += @io.read_nonblock(4096) }
|
74
|
+
rescue IO::WaitReadable # rubocop:disable Lint/HandleExceptions
|
75
|
+
rescue EOFError
|
76
|
+
close
|
77
|
+
ensure
|
78
|
+
@buffer += with_callback(data) unless data.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Writes all the data into the IO that can be written without blocking. It
|
82
|
+
# is a no-op if there are no data to be written. If an EOFError is
|
83
|
+
# encountered during the write, the underlying IO is closed.
|
84
|
+
#
|
85
|
+
# @return [void]
|
86
|
+
def write_available!
|
87
|
+
until @buffer.empty?
|
88
|
+
n = @io.write_nonblock(@buffer)
|
89
|
+
@buffer = @buffer[n..-1]
|
90
|
+
end
|
91
|
+
rescue IO::WaitWritable # rubocop:disable Lint/HandleExceptions
|
92
|
+
rescue EOFError
|
93
|
+
close
|
94
|
+
end
|
95
|
+
|
96
|
+
# Adds data to the buffer. If the buffer is used for writing, then this
|
97
|
+
# should be the preferred method of queueing the data to be written.
|
98
|
+
#
|
99
|
+
# @return [void]
|
100
|
+
def add_data(data)
|
101
|
+
@buffer += data
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def with_callback(data)
|
107
|
+
if @callback
|
108
|
+
@callback.call(data)
|
109
|
+
else
|
110
|
+
data
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -14,6 +14,8 @@ module Proxy::Dynflow
|
|
14
14
|
:execution_plan_cleaner_age => 60 * 30
|
15
15
|
plugin :dynflow, Proxy::Dynflow::VERSION
|
16
16
|
|
17
|
+
capability(proc { self.available_operations })
|
18
|
+
|
17
19
|
after_activation do
|
18
20
|
require 'smart_proxy_dynflow/settings_loader'
|
19
21
|
require 'smart_proxy_dynflow/otp_manager'
|
@@ -25,5 +27,9 @@ module Proxy::Dynflow
|
|
25
27
|
|
26
28
|
Proxy::Dynflow::Core.ensure_initialized
|
27
29
|
end
|
30
|
+
|
31
|
+
def self.available_operations
|
32
|
+
TaskLauncherRegistry.operations
|
33
|
+
end
|
28
34
|
end
|
29
35
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'smart_proxy_dynflow/io_buffer'
|
2
|
+
|
3
|
+
module Proxy
|
4
|
+
module Dynflow
|
5
|
+
# An abstraction for managing local processes.
|
6
|
+
#
|
7
|
+
# It can be used to:
|
8
|
+
# - spawn a local process
|
9
|
+
# - track its lifecycle
|
10
|
+
# - communicate with it through its standard input, output and error
|
11
|
+
# - step through the execution one event at a time or start the child process and wait until it finishes
|
12
|
+
#
|
13
|
+
# @example Run date command and collect its output
|
14
|
+
# pm = ProcessManager.new('date')
|
15
|
+
# pm.run!
|
16
|
+
# pm.status #=> 0
|
17
|
+
# pm.stdout.to_s.chomp #=> "Thu Feb 3 04:27:42 PM CET 2022"
|
18
|
+
#
|
19
|
+
# @example Run a shell loop, outputting all the lines it generates
|
20
|
+
# pm = ProcessManager.new(['/bin/sh', '-c', 'for i in 1 2 3; do echo $i; sleep 1; done'])
|
21
|
+
# pm.on_stdout { |data| puts data; '' }
|
22
|
+
# pm.run!
|
23
|
+
# #=> 1
|
24
|
+
# #=> 2
|
25
|
+
# #=> 3
|
26
|
+
#
|
27
|
+
# @example Run bc (calculator) interactively and count down from 10 to 0
|
28
|
+
# pm = ProcessManager.new('bc')
|
29
|
+
# pm.on_stdout do |data|
|
30
|
+
# if data.match?(/^\d+/)
|
31
|
+
# n = data.to_i
|
32
|
+
# if n.zero?
|
33
|
+
# pm.stdin.to_io.close
|
34
|
+
# else
|
35
|
+
# pm.stdin.add_data("#{n} - 1\n")
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
# data
|
39
|
+
# end
|
40
|
+
# pm.stdin.add_data("10\n")
|
41
|
+
# pm.run!
|
42
|
+
# pm.stdout.to_s.lines #=. ["10\n", "9\n", "8\n", "7\n", "6\n", "5\n", "4\n", "3\n", "2\n", "1\n", "0\n"]
|
43
|
+
#
|
44
|
+
# @attr_reader [Proxy::Dynflow::IOBuffer] stdin IOBuffer buffering writes to child process' standard input
|
45
|
+
# @attr_reader [Proxy::Dynflow::IOBuffer] stdout IOBuffer buffering reads from child process' standard output
|
46
|
+
# @attr_reader [Proxy::Dynflow::IOBuffer] stderr IOBuffer buffering reads from child process' standard error
|
47
|
+
# @attr_reader [nil, Integer] pid Process id of the child process, nil if the process was not started yet, -1 if the process could not be started
|
48
|
+
# @attr_reader [nil, Integer] status Exit status of the child process. nil if the child process has not finished yet, 255 if the process could not be started
|
49
|
+
class ProcessManager
|
50
|
+
attr_reader :stdin, :stdout, :stderr, :pid, :status
|
51
|
+
|
52
|
+
# @param [String, [String], [Hash, String]] command A command to run in one of the forms accepted by Kernel.spawn
|
53
|
+
def initialize(command)
|
54
|
+
@command = command
|
55
|
+
@stdin = IOBuffer.new(nil)
|
56
|
+
@stdout = IOBuffer.new(nil)
|
57
|
+
@stderr = IOBuffer.new(nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Starts the process manager and runs it until it finishes
|
61
|
+
#
|
62
|
+
# @return [ProcessManager] the process manager itself to allow method chaining
|
63
|
+
def run!
|
64
|
+
start! unless started?
|
65
|
+
process until done?
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Starts the child process. It creates 3 pipes for communicating with the
|
70
|
+
# child process and the forks it. The process manager is considered done
|
71
|
+
# if the child process cannot be started.
|
72
|
+
#
|
73
|
+
# @return [void]
|
74
|
+
def start!
|
75
|
+
in_read, in_write = IO.pipe
|
76
|
+
out_read, out_write = IO.pipe
|
77
|
+
err_read, err_write = IO.pipe
|
78
|
+
|
79
|
+
@stdin.io = in_write
|
80
|
+
@stdout.io = out_read
|
81
|
+
@stderr.io = err_read
|
82
|
+
|
83
|
+
@pid = spawn(*@command, :in => in_read, :out => out_write, :err => err_write)
|
84
|
+
[in_read, out_write, err_write].each(&:close)
|
85
|
+
rescue Errno::ENOENT => e
|
86
|
+
[in_read, in_write, out_read, out_write, err_read, err_write].each(&:close)
|
87
|
+
@pid = -1
|
88
|
+
@status = 255
|
89
|
+
@stderr.add_data(e.message)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Determines whether the process manager already forked off its child process
|
93
|
+
#
|
94
|
+
# @return [true, false] whether the process manager already forked off its child process
|
95
|
+
def started?
|
96
|
+
!pid.nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Determines whether the child process of the process manager already finished
|
100
|
+
#
|
101
|
+
# @return [true, false] whether the child process of the process manager already finished
|
102
|
+
def done?
|
103
|
+
started? && !status.nil?
|
104
|
+
end
|
105
|
+
|
106
|
+
# Runs a single iteration of the manager's processing loop. It waits until either:
|
107
|
+
# - data is available in pipes connected to the child process' standard output or error
|
108
|
+
# - there is pending data to be written and the pipe connected to the child process' standard input is writable
|
109
|
+
# - a timeout is reached
|
110
|
+
#
|
111
|
+
# After the wait, all pending data is read and written.
|
112
|
+
#
|
113
|
+
# If all the pipes connected to the child process are closed, it marks the
|
114
|
+
# execution as complete and performs cleanup.
|
115
|
+
#
|
116
|
+
# @param timeout [nil, Numeric] controls how long this call should wait for data to become available. Waits indefinitely if nil.
|
117
|
+
# @return [void]
|
118
|
+
def process(timeout: nil)
|
119
|
+
raise 'Cannot process until the manager is started' unless started?
|
120
|
+
writers = [@stdin].reject { |buf| buf.empty? || buf.closed? }
|
121
|
+
readers = [@stdout, @stderr].reject(&:closed?)
|
122
|
+
|
123
|
+
if readers.empty? && writers.empty?
|
124
|
+
finish
|
125
|
+
return
|
126
|
+
end
|
127
|
+
|
128
|
+
ready_readers, ready_writers = IO.select(readers, writers, nil, timeout)
|
129
|
+
(ready_readers || []).each(&:read_available!)
|
130
|
+
(ready_writers || []).each(&:write_available!)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Sets block to be executed each time data is read from child process' standard output
|
134
|
+
#
|
135
|
+
# @return [void]
|
136
|
+
def on_stdout(&block)
|
137
|
+
@stdout.on_data(&block)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Sets block to be executed each time data is read from child process' standard error
|
141
|
+
#
|
142
|
+
# @return [void]
|
143
|
+
def on_stderr(&block)
|
144
|
+
@stderr.on_data(&block)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Makes the process manager close all the pipes it may have opened to communicate with the child process
|
148
|
+
#
|
149
|
+
# @return [void]
|
150
|
+
def close
|
151
|
+
[@stdin, @stdout, @stderr].each(&:close)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Makes the process manager finish its run, closing opened FDs and reaping the child process
|
157
|
+
#
|
158
|
+
# @return [void]
|
159
|
+
def finish
|
160
|
+
close
|
161
|
+
unless @pid == -1
|
162
|
+
_pid, status = Process.wait2(@pid)
|
163
|
+
@status = status.exitstatus
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -6,9 +6,9 @@ module Proxy::Dynflow
|
|
6
6
|
attr_reader :id
|
7
7
|
attr_writer :logger
|
8
8
|
|
9
|
-
def initialize(*_args, suspended_action: nil)
|
9
|
+
def initialize(*_args, suspended_action: nil, id: nil)
|
10
10
|
@suspended_action = suspended_action
|
11
|
-
@id = SecureRandom.uuid
|
11
|
+
@id = id || SecureRandom.uuid
|
12
12
|
initialize_continuous_outputs
|
13
13
|
end
|
14
14
|
|
@@ -93,6 +93,12 @@ module Proxy::Dynflow
|
|
93
93
|
def initialize_continuous_outputs
|
94
94
|
@continuous_output = ::Proxy::Dynflow::ContinuousOutput.new
|
95
95
|
end
|
96
|
+
|
97
|
+
def run_refresh_output
|
98
|
+
logger.debug('refreshing runner on demand')
|
99
|
+
refresh
|
100
|
+
generate_updates
|
101
|
+
end
|
96
102
|
end
|
97
103
|
end
|
98
104
|
end
|
@@ -1,5 +1,18 @@
|
|
1
1
|
module Proxy::Dynflow
|
2
2
|
module Runner
|
3
|
+
# This module expects to be included into a Runner action, where it can be
|
4
|
+
# used to simplify handling of long-running processes. However it tracks the
|
5
|
+
# running process as a group of instance variables, which has served us
|
6
|
+
# reasonably well in the past, but can be rather error prone.
|
7
|
+
#
|
8
|
+
# A better alternative to this is
|
9
|
+
# {::Proxy::Dynflow::Runner::ProcessManagerCommand}. It tracks the whole
|
10
|
+
# execution of a process under a single instance variable and uses a more
|
11
|
+
# robust {::Proxy::Dynflow::ProcessManager} under the hood. It also
|
12
|
+
# maintains the same interface and can be used as a drop-in replacement.
|
13
|
+
#
|
14
|
+
# This module is now soft-deprecated and
|
15
|
+
# {::Proxy::Dynflow::Runner::ProcessManagerCommand} should be used instead.
|
3
16
|
module Command
|
4
17
|
def initialize_command(*command)
|
5
18
|
@command_out, @command_in, @command_pid = PTY.spawn(*command)
|
@@ -43,6 +43,11 @@ module Proxy::Dynflow
|
|
43
43
|
plan_next_refresh
|
44
44
|
end
|
45
45
|
|
46
|
+
def refresh_output
|
47
|
+
@logger.debug("refresh output #{@runner.id}")
|
48
|
+
dispatch_updates(@runner.run_refresh_output)
|
49
|
+
end
|
50
|
+
|
46
51
|
def dispatch_updates(updates)
|
47
52
|
updates.each { |receiver, update| (receiver || @suspended_action) << update }
|
48
53
|
|
@@ -157,6 +162,12 @@ module Proxy::Dynflow
|
|
157
162
|
end
|
158
163
|
end
|
159
164
|
|
165
|
+
def refresh_output(runner_id)
|
166
|
+
synchronize do
|
167
|
+
@runner_actors[runner_id]&.tell([:refresh_output])
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
160
171
|
def handle_command_exception(*args)
|
161
172
|
synchronize { _handle_command_exception(*args) }
|
162
173
|
end
|
@@ -3,10 +3,10 @@ module Proxy::Dynflow
|
|
3
3
|
class Parent < Base
|
4
4
|
# targets = { identifier => { :execution_plan_id => "...", :run_step_id => id,
|
5
5
|
# :input => { ... } }
|
6
|
-
def initialize(targets = {}, suspended_action: nil)
|
6
|
+
def initialize(targets = {}, suspended_action: nil, id: nil)
|
7
7
|
@targets = targets
|
8
8
|
@exit_statuses = {}
|
9
|
-
super suspended_action: suspended_action
|
9
|
+
super suspended_action: suspended_action, id: id
|
10
10
|
end
|
11
11
|
|
12
12
|
def generate_updates
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'smart_proxy_dynflow/process_manager'
|
2
|
+
|
3
|
+
module Proxy::Dynflow
|
4
|
+
module Runner
|
5
|
+
# A convenience module which should be included into a Runner action. It
|
6
|
+
# leverages {::Proxy::Dynflow::ProcessManager} to reliably keep track of
|
7
|
+
# an external process and collect its output.
|
8
|
+
#
|
9
|
+
# The only expectation from the Runner action is to call
|
10
|
+
# {#initialize_command} somewhere and pass the command to be run to it.
|
11
|
+
module ProcessManagerCommand
|
12
|
+
def initialize_command(*command)
|
13
|
+
@process_manager = ProcessManager.new(command)
|
14
|
+
set_process_manager_callbacks(@process_manager)
|
15
|
+
@process_manager.start!
|
16
|
+
if @process_manager.done? && @process_manager.status == 255
|
17
|
+
exception = RuntimeError.new(@process_manager.stderr.to_s)
|
18
|
+
exception.set_backtrace Thread.current.backtrace
|
19
|
+
publish_exception("Error running command '#{command.join(' ')}'", exception)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_process_manager_callbacks(pm)
|
24
|
+
pm.on_stdout do |data|
|
25
|
+
publish_data(data, 'stdout')
|
26
|
+
''
|
27
|
+
end
|
28
|
+
pm.on_stderr do |data|
|
29
|
+
publish_data(data, 'stderr')
|
30
|
+
''
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh
|
35
|
+
@process_manager.process(timeout: 0.1) unless @process_manager.done?
|
36
|
+
publish_exit_status(@process_manager.status) if @process_manager.done?
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
@process_manager&.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -15,6 +15,14 @@ module Proxy::Dynflow
|
|
15
15
|
|
16
16
|
def self.input_format; end
|
17
17
|
|
18
|
+
def to_hash
|
19
|
+
{ :class => self.class.to_s, :callback => callback, :options => options }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.new_from_hash(world, hash)
|
23
|
+
::Dynflow::Utils.constantize(hash[:class]).new(world, hash[:callback], hash[:options])
|
24
|
+
end
|
25
|
+
|
18
26
|
private
|
19
27
|
|
20
28
|
def format_result(result)
|
@@ -34,9 +42,9 @@ module Proxy::Dynflow
|
|
34
42
|
input.merge(:callback_host => callback)
|
35
43
|
end
|
36
44
|
|
37
|
-
def trigger(parent, klass, *input)
|
45
|
+
def trigger(parent, klass, *input, id: nil)
|
38
46
|
world.trigger do
|
39
|
-
world.plan_with_options(caller_action: parent, action_class: klass, args: input)
|
47
|
+
world.plan_with_options(caller_action: parent, action_class: klass, args: input, id: id)
|
40
48
|
end
|
41
49
|
end
|
42
50
|
end
|
@@ -2,36 +2,37 @@ module Proxy::Dynflow
|
|
2
2
|
module TaskLauncher
|
3
3
|
class Batch < Abstract
|
4
4
|
def launch!(input)
|
5
|
-
trigger(nil,
|
5
|
+
plan = trigger(nil, action_class, self, input)
|
6
|
+
results[:parent] = format_result(plan)
|
6
7
|
end
|
7
8
|
|
8
9
|
def launch_children(parent, input_hash)
|
9
|
-
input_hash.
|
10
|
+
input_hash.map do |task_id, input|
|
10
11
|
launcher = child_launcher(parent)
|
11
|
-
launcher.launch!(transform_input(input))
|
12
|
+
triggered = launcher.launch!(transform_input(input), id: task_id)
|
12
13
|
results[task_id] = launcher.results
|
14
|
+
triggered
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
16
18
|
def prepare_batch(input_hash)
|
17
|
-
|
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
|
19
|
+
input_hash
|
23
20
|
end
|
24
21
|
|
25
|
-
private
|
26
|
-
|
27
22
|
def child_launcher(parent)
|
28
23
|
Single.new(world, callback, :parent => parent)
|
29
24
|
end
|
30
25
|
|
26
|
+
private
|
27
|
+
|
31
28
|
# Identity by default
|
32
29
|
def transform_input(input)
|
33
30
|
input
|
34
31
|
end
|
32
|
+
|
33
|
+
def action_class
|
34
|
+
Proxy::Dynflow::Action::Batch
|
35
|
+
end
|
35
36
|
end
|
36
37
|
end
|
37
38
|
end
|
@@ -3,17 +3,22 @@ require 'smart_proxy_dynflow/runner'
|
|
3
3
|
module Proxy::Dynflow
|
4
4
|
module TaskLauncher
|
5
5
|
class AbstractGroup < Batch
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@runner_id = SecureRandom.uuid
|
9
|
+
end
|
10
|
+
|
6
11
|
def self.runner_class
|
7
12
|
raise NotImplementedError
|
8
13
|
end
|
9
14
|
|
10
|
-
def
|
11
|
-
|
15
|
+
def action_class
|
16
|
+
Action::SingleRunnerBatch
|
12
17
|
end
|
13
18
|
|
14
19
|
def launch_children(parent, input_hash)
|
15
20
|
super(parent, input_hash)
|
16
|
-
trigger(parent, Action::BatchRunner, self, input_hash)
|
21
|
+
trigger(parent, Action::BatchRunner, self, input_hash, @runner_id)
|
17
22
|
end
|
18
23
|
|
19
24
|
def operation
|
@@ -36,7 +41,8 @@ module Proxy::Dynflow
|
|
36
41
|
end
|
37
42
|
|
38
43
|
def transform_input(input)
|
39
|
-
wipe_callback(input)
|
44
|
+
tmp = wipe_callback(input)
|
45
|
+
input.merge('action_input' => tmp['action_input'].merge(:runner_id => @runner_id))
|
40
46
|
end
|
41
47
|
|
42
48
|
def wipe_callback(input)
|
@@ -5,10 +5,11 @@ module Proxy::Dynflow
|
|
5
5
|
{ :action_class => "MyActionClass", :action_input => {} }
|
6
6
|
end
|
7
7
|
|
8
|
-
def launch!(input)
|
8
|
+
def launch!(input, id: nil)
|
9
9
|
triggered = trigger(options[:parent],
|
10
10
|
action_class(input),
|
11
|
-
with_callback(input.fetch('action_input', {}))
|
11
|
+
with_callback(input.fetch('action_input', {})),
|
12
|
+
id: id)
|
12
13
|
@results = format_result(triggered)
|
13
14
|
triggered
|
14
15
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_dynflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
@@ -151,6 +151,7 @@ files:
|
|
151
151
|
- lib/smart_proxy_dynflow/action/batch.rb
|
152
152
|
- lib/smart_proxy_dynflow/action/batch_callback.rb
|
153
153
|
- lib/smart_proxy_dynflow/action/batch_runner.rb
|
154
|
+
- lib/smart_proxy_dynflow/action/external_polling.rb
|
154
155
|
- lib/smart_proxy_dynflow/action/output_collector.rb
|
155
156
|
- lib/smart_proxy_dynflow/action/runner.rb
|
156
157
|
- lib/smart_proxy_dynflow/action/shareable.rb
|
@@ -161,10 +162,12 @@ files:
|
|
161
162
|
- lib/smart_proxy_dynflow/core.rb
|
162
163
|
- lib/smart_proxy_dynflow/helpers.rb
|
163
164
|
- lib/smart_proxy_dynflow/http_config.ru
|
165
|
+
- lib/smart_proxy_dynflow/io_buffer.rb
|
164
166
|
- lib/smart_proxy_dynflow/log.rb
|
165
167
|
- lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb
|
166
168
|
- lib/smart_proxy_dynflow/otp_manager.rb
|
167
169
|
- lib/smart_proxy_dynflow/plugin.rb
|
170
|
+
- lib/smart_proxy_dynflow/process_manager.rb
|
168
171
|
- lib/smart_proxy_dynflow/proxy_adapter.rb
|
169
172
|
- lib/smart_proxy_dynflow/runner.rb
|
170
173
|
- lib/smart_proxy_dynflow/runner/base.rb
|
@@ -172,6 +175,7 @@ files:
|
|
172
175
|
- lib/smart_proxy_dynflow/runner/command_runner.rb
|
173
176
|
- lib/smart_proxy_dynflow/runner/dispatcher.rb
|
174
177
|
- lib/smart_proxy_dynflow/runner/parent.rb
|
178
|
+
- lib/smart_proxy_dynflow/runner/process_manager_command.rb
|
175
179
|
- lib/smart_proxy_dynflow/runner/update.rb
|
176
180
|
- lib/smart_proxy_dynflow/settings.rb
|
177
181
|
- lib/smart_proxy_dynflow/settings_loader.rb
|