cem_acpt 0.3.3-universal-java-17 → 0.3.5-universal-java-17
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.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +36 -0
- data/Gemfile.lock +12 -2
- data/cem_acpt.gemspec +1 -0
- data/lib/cem_acpt/platform/gcp/cmd.rb +42 -0
- data/lib/cem_acpt/platform/gcp/compute.rb +24 -0
- data/lib/cem_acpt/platform/gcp.rb +12 -0
- data/lib/cem_acpt/platform/utils/linux.rb +76 -0
- data/lib/cem_acpt/test_runner/logging.rb +77 -0
- data/lib/cem_acpt/test_runner/run_handler.rb +21 -20
- data/lib/cem_acpt/test_runner/runner.rb +50 -115
- data/lib/cem_acpt/test_runner/runner_workflow_builder.rb +217 -0
- data/lib/cem_acpt/test_runner/workflow/manager.rb +198 -0
- data/lib/cem_acpt/test_runner/workflow/step.rb +181 -0
- data/lib/cem_acpt/test_runner/workflow.rb +11 -0
- data/lib/cem_acpt/version.rb +1 -1
- metadata +23 -2
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'concurrent-ruby'
|
4
4
|
require 'English'
|
5
|
-
require_relative '../logging'
|
6
5
|
require_relative '../rspec_utils'
|
6
|
+
require_relative 'logging'
|
7
7
|
require_relative 'runner_result'
|
8
|
+
require_relative 'runner_workflow_builder'
|
9
|
+
#require_relative 'workflow'
|
8
10
|
|
9
11
|
module CemAcpt
|
10
12
|
module TestRunner
|
@@ -28,7 +30,7 @@ module CemAcpt
|
|
28
30
|
# reporting the results back to the main thread. Runner objects are created
|
29
31
|
# by the RunHandler and then, when started, execute their logic in a thread.
|
30
32
|
class Runner
|
31
|
-
include CemAcpt::
|
33
|
+
include CemAcpt::TestRunner::Logging
|
32
34
|
|
33
35
|
attr_reader :node, :node_exists, :run_result
|
34
36
|
|
@@ -42,34 +44,34 @@ module CemAcpt
|
|
42
44
|
@debug_mode = @context.config.debug_mode?
|
43
45
|
@node_inventory = @context.node_inventory
|
44
46
|
@module_pkg_path = @context.module_package_path
|
47
|
+
@provision_attempts = 0
|
48
|
+
@provision_start_time = nil
|
45
49
|
@node_exists = false
|
46
50
|
@run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
|
47
|
-
@
|
51
|
+
@logger = use_logger(nil, stage: 'Runner', node: @node.node_name, test: @node.test_data[:test_name])
|
48
52
|
validate!
|
49
53
|
end
|
50
54
|
|
51
|
-
def run_step(step_sym)
|
52
|
-
send(step_sym)
|
53
|
-
@completed_steps << step_sym
|
54
|
-
rescue StandardError => e
|
55
|
-
err = CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
|
56
|
-
step_error_logging(err)
|
57
|
-
@run_result.from_error(err)
|
58
|
-
destroy unless step_sym == :destroy
|
59
|
-
end
|
60
|
-
|
61
55
|
# Executes test suite steps
|
62
56
|
def start
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
@logger.info('Starting test suite workflow...')
|
58
|
+
@workflow = new_workflow
|
59
|
+
@workflow.run
|
60
|
+
if @workflow.success?
|
61
|
+
@run_result = @workflow.last_result
|
62
|
+
@workflow.completed_steps.each do |s|
|
63
|
+
@logger.info("Step '#{s.name}' completed successfully")
|
64
|
+
end
|
65
|
+
true
|
66
|
+
else
|
67
|
+
@run_result = @workflow.last_error
|
68
|
+
step_error_logging(@workflow.last_error)
|
69
|
+
false
|
70
|
+
end
|
69
71
|
rescue StandardError => e
|
70
|
-
step_error_logging(e)
|
72
|
+
step_error_logging(e, :fatal)
|
73
|
+
@run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
|
71
74
|
@run_result.from_error(e)
|
72
|
-
destroy
|
73
75
|
end
|
74
76
|
|
75
77
|
# Checks for failures in the test results.
|
@@ -81,104 +83,37 @@ module CemAcpt
|
|
81
83
|
|
82
84
|
private
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
# Builds a new workflow for the runner
|
87
|
+
# @return [CemAcpt::TestRunner::Workflow::Manager] the new workflow
|
88
|
+
def new_workflow
|
89
|
+
builder = RunnerWorkflowBuilder.new(@node, @context.config, @logger)
|
90
|
+
builder.add_provision
|
91
|
+
builder.add_sleep(time: 30)
|
92
|
+
builder.add_wait_for_node_ssh
|
93
|
+
builder.add_check_dnf_automatic
|
94
|
+
builder.add_check_rpm_db
|
95
|
+
builder.add_save_node_to_inventory(node_inventory: @node_inventory, platform: @platform)
|
96
|
+
builder.add_check_for_module_package_path(module_pkg_path: @module_pkg_path)
|
97
|
+
builder.add_install_module(module_pkg_path: @module_pkg_path)
|
98
|
+
builder.add_check_node_inventory_file(node_inventory: @node_inventory)
|
99
|
+
builder.add_run_tests(rspec_opts: rspec_opts, rspec_cmd: CemAcpt::RSpecUtils::Command, run_result: @run_result)
|
100
|
+
builder.add_clean_up
|
101
|
+
builder.workflow
|
91
102
|
end
|
92
103
|
|
93
|
-
def
|
94
|
-
|
104
|
+
def step_error_logging(err, kind = :error)
|
105
|
+
msg = if err.respond_to?(:step)
|
106
|
+
"runner failed on step '#{err.step}': #{err.message}"
|
107
|
+
else
|
108
|
+
"runner failed: #{err.message}"
|
109
|
+
end
|
110
|
+
@logger.send(kind, msg)
|
111
|
+
@logger.debug("failed runner backtrace:\n#{err.backtrace.join("\n")}")
|
112
|
+
@logger.debug("failed runner test data: #{@node.test_data}")
|
95
113
|
end
|
96
114
|
|
97
|
-
|
98
|
-
|
99
|
-
async_info("Provisioning #{@node.node_name}...", log_prefix('PROVISION'))
|
100
|
-
start_time = Time.now
|
101
|
-
@node.provision
|
102
|
-
@node_exists = true
|
103
|
-
max_retries = 60 # equals 300 seconds because we check every five seconds
|
104
|
-
until @node.ready?
|
105
|
-
if max_retries <= 0
|
106
|
-
async_fatal("Node #{@node.node_name} failed to provision", log_prefix('PROVISION'))
|
107
|
-
raise CemAcpt::TestRunner::RunnerProvisionError, "Provisioning timed out for node #{@node.node_name}"
|
108
|
-
end
|
109
|
-
|
110
|
-
async_info("Waiting for #{@node.node_name} to be ready for remote connections...", log_prefix('PROVISION'))
|
111
|
-
max_retries -= 1
|
112
|
-
sleep(5)
|
113
|
-
end
|
114
|
-
async_info("Node #{@node.node_name} is ready...", log_prefix('PROVISION'))
|
115
|
-
node_desc = {
|
116
|
-
test_data: @node.test_data,
|
117
|
-
platform: @platform,
|
118
|
-
local_port: @node.local_port,
|
119
|
-
}.merge(@node.node)
|
120
|
-
@node_inventory.add(@node.node_name, node_desc)
|
121
|
-
@node_inventory.save
|
122
|
-
async_info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds", log_prefix('PROVISION'))
|
123
|
-
end
|
124
|
-
|
125
|
-
# Bootstraps the node for the acceptance test suite. Currently, this
|
126
|
-
# just uploads and installs the module package.
|
127
|
-
def bootstrap
|
128
|
-
async_info("Bootstrapping #{@node.node_name}...", log_prefix('BOOTSTRAP'))
|
129
|
-
until File.exist?(@module_pkg_path)
|
130
|
-
async_debug("Waiting for module package #{@module_pkg_path} to exist...", log_prefix('BOOTSTRAP'))
|
131
|
-
sleep(1)
|
132
|
-
end
|
133
|
-
async_info("Installing module package #{@module_pkg_path}...", log_prefix('BOOTSTRAP'))
|
134
|
-
@node.install_puppet_module_package(@module_pkg_path)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Runs the acceptance test suite via rspec.
|
138
|
-
def run_tests
|
139
|
-
attempts = 0
|
140
|
-
until File.exist?(@node_inventory.save_file_path)
|
141
|
-
raise 'Node inventory file not found' if (attempts += 1) > 3
|
142
|
-
|
143
|
-
sleep(1)
|
144
|
-
end
|
145
|
-
async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
|
146
|
-
@node.run_tests do |cmd_env|
|
147
|
-
cmd_opts = rspec_opts
|
148
|
-
cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
|
149
|
-
# Documentation format gets logged in real time, JSON file is read after the fact
|
150
|
-
begin
|
151
|
-
@rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
|
152
|
-
@rspec_cmd.execute(pty: false, log_prefix: log_prefix('RSPEC'))
|
153
|
-
@run_result.from_json_file(cmd_opts.format[:json])
|
154
|
-
rescue Errno::EIO => e
|
155
|
-
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
|
156
|
-
@run_result.from_error(e)
|
157
|
-
rescue StandardError => e
|
158
|
-
async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", log_prefix('RSPEC'))
|
159
|
-
async_debug("Backtrace:\n#{e.backtrace}", log_prefix('RSPEC'))
|
160
|
-
@run_result.from_error(e)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
async_info("Tests completed with exit code: #{@run_result.exit_status}", log_prefix('RSPEC'))
|
164
|
-
end
|
165
|
-
|
166
|
-
# Destroys the node for the acceptance test suite.
|
167
|
-
def destroy
|
168
|
-
kill_spec_pty_if_exists
|
169
|
-
if @context.config.get('no_destroy_nodes')
|
170
|
-
async_info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true",
|
171
|
-
log_prefix('DESTROY'))
|
172
|
-
else
|
173
|
-
async_info("Destroying #{@node.node_name}...", log_prefix('DESTROY'))
|
174
|
-
@node.destroy
|
175
|
-
@node_exists = false
|
176
|
-
async_info("Node #{@node.node_name} destroyed successfully", log_prefix('DESTROY'))
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
def kill_spec_pty_if_exists
|
181
|
-
@rspec_cmd&.kill_pty
|
115
|
+
def log_prefix(prefix)
|
116
|
+
"#{prefix}: #{@node.test_data[:test_name]}:"
|
182
117
|
end
|
183
118
|
|
184
119
|
# Validates the runner configuration.
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'logging'
|
4
|
+
require_relative 'workflow'
|
5
|
+
|
6
|
+
module CemAcpt
|
7
|
+
module TestRunner
|
8
|
+
# RunnerWorkflowBuilder builds a workflow for a TestRunner
|
9
|
+
# @!attribute [r] workflow
|
10
|
+
# @return [CemAcpt::TestRunner::Workflow::Manager] Workflow object
|
11
|
+
class RunnerWorkflowBuilder
|
12
|
+
include CemAcpt::TestRunner::Logging
|
13
|
+
|
14
|
+
attr_reader :workflow
|
15
|
+
|
16
|
+
# @param node [CemAcpt::Platform::Base] Initialized node object
|
17
|
+
# @param config [Hash] Context config hash
|
18
|
+
# @param logger [CemAcpt::TestRunner::Logging::Logger] Logger object
|
19
|
+
def initialize(node, config, logger = nil)
|
20
|
+
@node = node
|
21
|
+
@logger = use_logger(logger, stage: 'Workflow')
|
22
|
+
@workflow = CemAcpt::TestRunner::Workflow::Manager.new(workflow_manager_opts(config, logger))
|
23
|
+
@config = config
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_sleep(**kwargs)
|
27
|
+
opts = {
|
28
|
+
node: @node,
|
29
|
+
time: kwargs[:time] || 10,
|
30
|
+
retryable: false,
|
31
|
+
}
|
32
|
+
@workflow.add_step(:sleep, **opts) do |s|
|
33
|
+
s.logger.info("sleeping for #{s.opts[:time]} seconds")
|
34
|
+
sleep(s.opts[:time])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_provision(**kwargs)
|
39
|
+
opts = {
|
40
|
+
node: @node,
|
41
|
+
retryable: kwargs[:retryable] || false,
|
42
|
+
}
|
43
|
+
@workflow.add_step(:provision, **opts) do |s|
|
44
|
+
s.opts[:node].provision
|
45
|
+
s.opts[:node]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_wait_for_node_ssh(**kwargs)
|
50
|
+
opts = {
|
51
|
+
node: @node,
|
52
|
+
retryable: kwargs[:retryable] || true,
|
53
|
+
retry_delay: kwargs[:retry_delay] || 30,
|
54
|
+
retry_max: kwargs[:retry_max] || 10,
|
55
|
+
}
|
56
|
+
@workflow.add_step(:wait_for_node_ssh, **opts) do |s|
|
57
|
+
unless s.opts[:node].ready?
|
58
|
+
raise "wait_for_node_ssh timed out for node #{s.opts[:node].node_name}"
|
59
|
+
end
|
60
|
+
s.opts[:node]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_check_dnf_automatic(**kwargs)
|
65
|
+
opts = {
|
66
|
+
node: @node,
|
67
|
+
retryable: kwargs[:retryable] || true,
|
68
|
+
retry_delay: kwargs[:retry_delay] || 30,
|
69
|
+
retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
|
70
|
+
}
|
71
|
+
@workflow.add_step(:check_dnf_automatic, **opts) do |s|
|
72
|
+
unless s.opts[:node].dnf_automatic_success?
|
73
|
+
raise "dnf_automatic failed on node #{s.opts[:node].node_name}"
|
74
|
+
end
|
75
|
+
s.opts[:node]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_check_rpm_db(**kwargs)
|
80
|
+
opts = {
|
81
|
+
node: @node,
|
82
|
+
retryable: kwargs[:retryable] || true,
|
83
|
+
retry_delay: kwargs[:retry_delay] || 30,
|
84
|
+
retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
|
85
|
+
}
|
86
|
+
@workflow.add_step(:rpm_db_check, **opts) do |s|
|
87
|
+
unless s.opts[:node].rpm_db_check_success?
|
88
|
+
raise "rpm_db_check failed on node #{s.opts[:node].node_name}"
|
89
|
+
end
|
90
|
+
s.opts[:node]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_save_node_to_inventory(node_inventory:, platform:, **kwargs)
|
95
|
+
opts = {
|
96
|
+
node: @node,
|
97
|
+
platform: platform,
|
98
|
+
node_inventory: node_inventory,
|
99
|
+
retryable: kwargs[:retryable] || false,
|
100
|
+
}
|
101
|
+
@workflow.add_step(:save_node_to_inventory, **opts) do |s|
|
102
|
+
node_desc = {
|
103
|
+
test_data: s.opts[:node].test_data,
|
104
|
+
platform: s.opts[:platform],
|
105
|
+
local_port: s.opts[:node].local_port,
|
106
|
+
}.merge(s.opts[:node].node)
|
107
|
+
s.opts[:node_inventory].add(s.opts[:node].node_name, node_desc)
|
108
|
+
s.opts[:node_inventory].save
|
109
|
+
s.logger.info("node #{s.opts[:node_name]} saved to inventory")
|
110
|
+
s.opts[:node_inventory]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_check_for_module_package_path(module_pkg_path:, **kwargs)
|
115
|
+
opts = {
|
116
|
+
module_pkg_path: module_pkg_path,
|
117
|
+
retryable: kwargs[:retryable] || true,
|
118
|
+
retry_delay: kwargs[:retry_delay] || 30,
|
119
|
+
}
|
120
|
+
@workflow.add_step(:check_for_module_package_path, **opts) do |s|
|
121
|
+
unless File.exist?(s.opts[:module_pkg_path])
|
122
|
+
raise "module package #{s.opts[:module_pkg_path]} does not exist"
|
123
|
+
end
|
124
|
+
s.opts[:module_pkg_path]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_install_module(module_pkg_path:, **kwargs)
|
129
|
+
opts = {
|
130
|
+
node: @node,
|
131
|
+
module_pkg_path: module_pkg_path,
|
132
|
+
retryable: kwargs[:retryable] || true,
|
133
|
+
retry_delay: kwargs[:retry_delay] || 60,
|
134
|
+
}
|
135
|
+
@workflow.add_step(:bootstrap, **opts) do |s|
|
136
|
+
s.opts[:node].install_puppet_module_package(s.opts[:module_pkg_path])
|
137
|
+
s.opts[:node]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def add_check_node_inventory_file(node_inventory:, **kwargs)
|
142
|
+
opts = {
|
143
|
+
ni_file_path: node_inventory.save_file_path,
|
144
|
+
retryable: kwargs[:retryable] || true,
|
145
|
+
retry_max: kwargs[:retry_max] || 60,
|
146
|
+
retry_delay: kwargs[:retry_delay] || 5,
|
147
|
+
}
|
148
|
+
@workflow.add_step(:check_node_inventory_file, **opts) do |s|
|
149
|
+
unless File.exist?(s.opts[:ni_file_path])
|
150
|
+
raise "node inventory file #{s.opts[:ni_file_path]} not found"
|
151
|
+
end
|
152
|
+
s.opts[:ni_file_path]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def add_run_tests(rspec_opts:, rspec_cmd:, run_result:, **kwargs)
|
157
|
+
opts = {
|
158
|
+
node: @node,
|
159
|
+
rspec_opts: rspec_opts,
|
160
|
+
rspec_cmd: rspec_cmd,
|
161
|
+
run_result: run_result,
|
162
|
+
retryable: kwargs[:retryable] || false,
|
163
|
+
}
|
164
|
+
@workflow.add_step(:run_tests, **opts) do |s|
|
165
|
+
s.opts[:node].run_tests do |cmd_env|
|
166
|
+
cmd_opts = s.opts[:rspec_opts].dup
|
167
|
+
cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
|
168
|
+
rspec_cmd = s.opts[:rspec_cmd].new(cmd_opts)
|
169
|
+
run_result = s.opts[:run_result].dup
|
170
|
+
begin
|
171
|
+
rspec_cmd.execute(pty: false, log_prefix: "RSPEC: #{@node.test_data[:test_name]}")
|
172
|
+
run_result.from_json_file(cmd_opts.format[:json])
|
173
|
+
rescue Errno::EIO => e
|
174
|
+
s.logger.error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}")
|
175
|
+
run_result.from_error(e)
|
176
|
+
rescue StandardError => e
|
177
|
+
s.logger.error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}")
|
178
|
+
s.logger.debug(e.backtrace.join("\n"))
|
179
|
+
run_result.from_error(e)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
run_result
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def add_clean_up(force: false, **kwargs)
|
187
|
+
opts = {
|
188
|
+
node: @node,
|
189
|
+
config: @config,
|
190
|
+
force: force,
|
191
|
+
retryable: kwargs[:retryable] || false,
|
192
|
+
}
|
193
|
+
@workflow.add_step(:clean_up, **opts) do |s|
|
194
|
+
if !force && s.opts[:config].get('no_destroy_nodes')
|
195
|
+
s.logger.info("not destroying node #{s.opts[:node].node_name} because 'no_destroy_nodes' is set to true")
|
196
|
+
else
|
197
|
+
s.logger.info("destroying node #{s.opts[:node].node_name}")
|
198
|
+
s.opts[:node].destroy
|
199
|
+
s.logger.info("node #{s.opts[:node].node_name} destroyed")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def workflow_manager_opts(config, logger = nil)
|
207
|
+
{
|
208
|
+
retry_max: config.get('workflow.retry_max') || 3,
|
209
|
+
retry_delay: config.get('workflow.retry_delay') || 0,
|
210
|
+
ignore_failures: config.get('workflow.ignore_failures') || false,
|
211
|
+
raise_on_fail: config.get('workflow.raise_on_fail') || true,
|
212
|
+
logger: logger,
|
213
|
+
}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../logging'
|
4
|
+
require_relative 'step'
|
5
|
+
|
6
|
+
module CemAcpt
|
7
|
+
module TestRunner
|
8
|
+
module Workflow
|
9
|
+
# Manager is a class that manages how steps in a workflow are executed.
|
10
|
+
# @!attribute [r] completed_steps
|
11
|
+
# @return [Array<Step>] The steps that have been completed
|
12
|
+
# @!attribute [r] last_error
|
13
|
+
# @return [Exception, nil] The last error that occurred, if any
|
14
|
+
# @!attribute [r] last_result
|
15
|
+
# @return [Any] The last result that occurred, if any
|
16
|
+
# @!attribute [r] retry_max
|
17
|
+
# @return [Integer] The maximum number of workflow runs to attempt before failing. Default: 3
|
18
|
+
# @!attribute [r] retry_delay
|
19
|
+
# @return [Integer] The number of seconds to wait between workflow runs. Default: 0
|
20
|
+
# @!attribute [r] steps
|
21
|
+
# @return [Array<Step>] The steps that are part of this workflow
|
22
|
+
class Manager
|
23
|
+
include CemAcpt::TestRunner::Logging
|
24
|
+
|
25
|
+
attr_reader :completed_steps, :last_error, :last_result, :retry_max, :retry_delay, :steps
|
26
|
+
|
27
|
+
# @param [Hash] opts The options to create a new workflow manager with
|
28
|
+
# @option opts [Integer] :retry_max The maximum number of workflow runs to attempt before failing. Default: 3
|
29
|
+
# @option opts [Integer] :retry_delay The number of seconds to wait between workflow runs. Default: 0
|
30
|
+
# @option opts [Boolean] :ignore_failures Whether to ignore Step failures and continue to the next step. Default: false
|
31
|
+
# @option opts [Boolean] :raise_on_fail Whether to raise an exception when a Step fails. Default: true
|
32
|
+
def initialize(**opts)
|
33
|
+
@retry_max = opts[:retry_max] || 3
|
34
|
+
@retry_delay = opts[:retry_delay] || 0
|
35
|
+
@ignore_failures = opts[:ignore_failures] || false
|
36
|
+
@raise_on_fail = opts[:raise_on_fail] || true
|
37
|
+
@logger = use_logger(opts[:logger], stage: 'Workflow')
|
38
|
+
@steps = []
|
39
|
+
@workflow_runs = 0
|
40
|
+
@last_error = nil
|
41
|
+
@last_result = nil
|
42
|
+
@completed_steps = []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a step to the workflow. Steps can be named anything, but if the step name is :clean_up,
|
46
|
+
# it will be run at the end of the workflow, regardless of the order it was added, and it's
|
47
|
+
# output will not be saved as the last_result. Additionally, the :clean_up step will be run
|
48
|
+
# even if the workflow fails.
|
49
|
+
# @param [Symbol] name The name of the step
|
50
|
+
# @param [Hash] kwargs The keyword arguments to pass to the step
|
51
|
+
# @param [Proc] block The block to pass to the step
|
52
|
+
# @yieldparam [Step] step The step that was added. This is passed to the block.
|
53
|
+
def add_step(name, **kwargs, &block)
|
54
|
+
raise ArgumentError, 'name must be a Symbol' unless name.is_a?(Symbol)
|
55
|
+
|
56
|
+
step_name = if @steps.any? { |step| step.name == name }
|
57
|
+
"#{name}_#{@steps.length}".to_sym
|
58
|
+
else
|
59
|
+
name
|
60
|
+
end
|
61
|
+
step = Step.new(step_name, **kwargs.merge(logger: @logger), &block)
|
62
|
+
@steps << StepState.new(step, @steps.length, **kwargs.merge(logger: @logger))
|
63
|
+
end
|
64
|
+
|
65
|
+
# Run the workflow
|
66
|
+
def run
|
67
|
+
@workflow_runs += 1
|
68
|
+
@completed_steps = []
|
69
|
+
@steps.each do |step|
|
70
|
+
next if step.name == :clean_up
|
71
|
+
|
72
|
+
result = step.run
|
73
|
+
handle_result(step, result)
|
74
|
+
end
|
75
|
+
ensure
|
76
|
+
clean_up
|
77
|
+
end
|
78
|
+
|
79
|
+
# Whether the workflow has completed successfully
|
80
|
+
def success?
|
81
|
+
(@completed_steps.length == @steps.length) && @completed_steps.none? { |step| step.failed? }
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Boolean] Whether to ignore failures and continue to the next step
|
85
|
+
def ignore_failures?
|
86
|
+
@ignore_failures
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Boolean] Whether to raise an exception when a step fails
|
90
|
+
def raise_on_fail?
|
91
|
+
@raise_on_fail
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def new_logger(logger)
|
97
|
+
if logger.nil?
|
98
|
+
logger
|
99
|
+
else
|
100
|
+
@logger = logger.dup
|
101
|
+
@logger.add_prefix_parts(stage: 'Workflow')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def log(level, msg)
|
106
|
+
if @logger
|
107
|
+
@logger.send(level, msg)
|
108
|
+
else
|
109
|
+
send("async_#{level}".to_sym, msg)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_debug(msg)
|
114
|
+
log(:debug, msg)
|
115
|
+
end
|
116
|
+
|
117
|
+
def log_info(msg)
|
118
|
+
log(:info, msg)
|
119
|
+
end
|
120
|
+
|
121
|
+
def log_warn(msg)
|
122
|
+
log(:warn, msg)
|
123
|
+
end
|
124
|
+
|
125
|
+
def log_error(msg)
|
126
|
+
log(:error, msg)
|
127
|
+
end
|
128
|
+
|
129
|
+
def log_fatal(msg)
|
130
|
+
log(:fatal, msg)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Handles the result of a step
|
134
|
+
# @param [Step] step The step that was run
|
135
|
+
# @param [Object] result The result of the step
|
136
|
+
# @raise [StandardError] If the step failed and the workflow is not configured to ignore failures
|
137
|
+
def handle_result(step, result)
|
138
|
+
@last_result = result unless step.name == :clean_up # Don't overwrite the last result with the clean_up step
|
139
|
+
case result
|
140
|
+
when :retry_workflow
|
141
|
+
log_warn("step '#{step.name}' failed and requested a workflow retry")
|
142
|
+
log_debug("step '#{step.name}' failed with error: #{step.last_error.message}")
|
143
|
+
log_debug("step '#{step.name}' failed with error: #{step.last_error.backtrace.join("\n")}")
|
144
|
+
@last_error = step.last_error
|
145
|
+
retry_workflow
|
146
|
+
when :fail
|
147
|
+
log_warn("step '#{step.name}' failed")
|
148
|
+
log_debug(step.last_error.message)
|
149
|
+
log_debug(step.last_error.backtrace.join("\n"))
|
150
|
+
@last_error = step.last_error
|
151
|
+
if ignore_failures?
|
152
|
+
log_warn("ignoring failure of step '#{step.name}'")
|
153
|
+
@completed_steps << step
|
154
|
+
else
|
155
|
+
log_error("failed with error: #{@last_error.message}")
|
156
|
+
raise @last_error
|
157
|
+
end
|
158
|
+
else
|
159
|
+
log_info("step '#{step.name}' succeeded")
|
160
|
+
log_debug("step '#{step.name}' returned: #{result}")
|
161
|
+
@completed_steps << step
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Retries the workflow if it is retryable
|
166
|
+
# @raise [StandardError] If the workflow is not retryable or has exceeded the maximum number of retries
|
167
|
+
def retry_workflow
|
168
|
+
if @workflow_runs < @retry_max
|
169
|
+
log_info("Retrying workflow (attempt #{@workflow_runs + 1} of #{@retry_max})")
|
170
|
+
sleep @retry_delay if @retry_delay > 0
|
171
|
+
clean_up
|
172
|
+
run
|
173
|
+
else
|
174
|
+
log_fatal('Workflow is not retryable or has exceeded the maximum number of retries')
|
175
|
+
raise @last_error
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Adds a default clean_up step if one is not defined
|
180
|
+
# @return [Step] The default clean_up step
|
181
|
+
def default_clean_up
|
182
|
+
add_step(:clean_up) do
|
183
|
+
log_info('No clean_up step defined, skipping')
|
184
|
+
true
|
185
|
+
end
|
186
|
+
@steps.last
|
187
|
+
end
|
188
|
+
|
189
|
+
# Runs the clean_up step
|
190
|
+
def clean_up
|
191
|
+
cleanup_step = @steps.find { |step| step.name == :clean_up } || default_clean_up
|
192
|
+
result = cleanup_step.run
|
193
|
+
handle_result(cleanup_step, result)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|