ductwork 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +168 -0
- data/README.md +154 -0
- data/Rakefile +10 -0
- data/app/models/ductwork/availability.rb +9 -0
- data/app/models/ductwork/execution.rb +13 -0
- data/app/models/ductwork/job.rb +181 -0
- data/app/models/ductwork/pipeline.rb +195 -0
- data/app/models/ductwork/process.rb +19 -0
- data/app/models/ductwork/record.rb +15 -0
- data/app/models/ductwork/result.rb +13 -0
- data/app/models/ductwork/run.rb +9 -0
- data/app/models/ductwork/step.rb +27 -0
- data/lib/ductwork/cli.rb +48 -0
- data/lib/ductwork/configuration.rb +145 -0
- data/lib/ductwork/dsl/branch_builder.rb +102 -0
- data/lib/ductwork/dsl/definition_builder.rb +153 -0
- data/lib/ductwork/engine.rb +14 -0
- data/lib/ductwork/machine_identifier.rb +11 -0
- data/lib/ductwork/processes/job_worker.rb +71 -0
- data/lib/ductwork/processes/job_worker_runner.rb +164 -0
- data/lib/ductwork/processes/pipeline_advancer.rb +91 -0
- data/lib/ductwork/processes/pipeline_advancer_runner.rb +169 -0
- data/lib/ductwork/processes/supervisor.rb +160 -0
- data/lib/ductwork/processes/supervisor_runner.rb +35 -0
- data/lib/ductwork/running_context.rb +22 -0
- data/lib/ductwork/testing/helpers.rb +18 -0
- data/lib/ductwork/testing/minitest.rb +8 -0
- data/lib/ductwork/testing/rspec.rb +63 -0
- data/lib/ductwork/testing.rb +15 -0
- data/lib/ductwork/version.rb +5 -0
- data/lib/ductwork.rb +77 -0
- data/lib/generators/ductwork/install/USAGE +11 -0
- data/lib/generators/ductwork/install/install_generator.rb +36 -0
- data/lib/generators/ductwork/install/templates/bin/ductwork +8 -0
- data/lib/generators/ductwork/install/templates/config/ductwork.yml +25 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_availabilities.rb +16 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_executions.rb +14 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_jobs.rb +17 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_pipelines.rb +19 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_processes.rb +14 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_results.rb +14 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb +12 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb +17 -0
- metadata +165 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Pipeline < Ductwork::Record # rubocop:todo Metrics/ClassLength
|
|
5
|
+
has_many :steps, class_name: "Ductwork::Step", foreign_key: "pipeline_id", dependent: :destroy
|
|
6
|
+
|
|
7
|
+
validates :klass, presence: true
|
|
8
|
+
validates :definition, presence: true
|
|
9
|
+
validates :definition_sha1, presence: true
|
|
10
|
+
validates :status, presence: true
|
|
11
|
+
validates :triggered_at, presence: true
|
|
12
|
+
validates :last_advanced_at, presence: true
|
|
13
|
+
|
|
14
|
+
enum :status,
|
|
15
|
+
pending: "pending",
|
|
16
|
+
in_progress: "in_progress",
|
|
17
|
+
halted: "halted",
|
|
18
|
+
completed: "completed"
|
|
19
|
+
|
|
20
|
+
def self.inherited(subclass)
|
|
21
|
+
super
|
|
22
|
+
|
|
23
|
+
subclass.class_eval do
|
|
24
|
+
default_scope { where(klass: name.to_s) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class DefinitionError < StandardError; end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
attr_reader :pipeline_definition
|
|
32
|
+
|
|
33
|
+
def define(&block)
|
|
34
|
+
if !block_given?
|
|
35
|
+
raise DefinitionError, "Definition block must be given"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if pipeline_definition
|
|
39
|
+
raise DefinitionError, "Pipeline has already been defined"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
builder = Ductwork::DSL::DefinitionBuilder.new
|
|
43
|
+
|
|
44
|
+
block.call(builder)
|
|
45
|
+
|
|
46
|
+
@pipeline_definition = builder.complete
|
|
47
|
+
|
|
48
|
+
Ductwork.defined_pipelines << name.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def trigger(*args)
|
|
52
|
+
if pipeline_definition.nil?
|
|
53
|
+
raise DefinitionError, "Pipeline must be defined before triggering"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
step_klass = pipeline_definition.dig(:nodes, 0)
|
|
57
|
+
definition = JSON.dump(pipeline_definition)
|
|
58
|
+
|
|
59
|
+
Record.transaction do
|
|
60
|
+
pipeline = create!(
|
|
61
|
+
klass: name.to_s,
|
|
62
|
+
status: :in_progress,
|
|
63
|
+
definition: definition,
|
|
64
|
+
definition_sha1: Digest::SHA1.hexdigest(definition),
|
|
65
|
+
triggered_at: Time.current,
|
|
66
|
+
last_advanced_at: Time.current
|
|
67
|
+
)
|
|
68
|
+
step = pipeline.steps.create!(
|
|
69
|
+
klass: step_klass,
|
|
70
|
+
status: :in_progress,
|
|
71
|
+
step_type: :start,
|
|
72
|
+
started_at: Time.current
|
|
73
|
+
)
|
|
74
|
+
Ductwork::Job.enqueue(step, *args)
|
|
75
|
+
|
|
76
|
+
pipeline
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def advance!
|
|
82
|
+
step = steps.advancing.take
|
|
83
|
+
edge = if step.present?
|
|
84
|
+
parsed_definition.dig(:edges, step.klass, 0)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Ductwork::Record.transaction do
|
|
88
|
+
steps.advancing.update!(status: :completed, completed_at: Time.current)
|
|
89
|
+
|
|
90
|
+
if edge.nil?
|
|
91
|
+
conditionally_complete_pipeline
|
|
92
|
+
else
|
|
93
|
+
advance_to_next_step_by_type(edge, step)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def create_step_and_enqueue_job(klass:, step_type:, input_arg:)
|
|
101
|
+
status = :in_progress
|
|
102
|
+
started_at = Time.current
|
|
103
|
+
next_step = steps.create!(klass:, status:, step_type:, started_at:)
|
|
104
|
+
Ductwork::Job.enqueue(next_step, input_arg)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parsed_definition
|
|
108
|
+
@parsed_definition ||= JSON.parse(definition).with_indifferent_access
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def conditionally_complete_pipeline
|
|
112
|
+
if steps.where(status: %w[in_progress pending]).none?
|
|
113
|
+
update!(status: :completed, completed_at: Time.current)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def advance_to_next_step_by_type(edge, step)
|
|
118
|
+
# NOTE: "chain" is used by ActiveRecord so we have to call
|
|
119
|
+
# this enum value "default" :sad:
|
|
120
|
+
step_type = edge[:type] == "chain" ? "default" : edge[:type]
|
|
121
|
+
|
|
122
|
+
if step_type.in?(%w[default divide])
|
|
123
|
+
advance_to_next_steps(step_type, step, edge)
|
|
124
|
+
elsif step_type == "combine"
|
|
125
|
+
combine_next_steps(step_type, edge)
|
|
126
|
+
elsif step_type == "expand"
|
|
127
|
+
expand_to_next_steps(step_type, step, edge)
|
|
128
|
+
elsif step_type == "collapse"
|
|
129
|
+
collapse_next_steps(step_type, step, edge)
|
|
130
|
+
else
|
|
131
|
+
Ductwork.configuration.logger.error(
|
|
132
|
+
msg: "Invalid step type",
|
|
133
|
+
role: :pipeline_advancer
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def advance_to_next_steps(step_type, step, edge)
|
|
139
|
+
edge[:to].each do |to_klass|
|
|
140
|
+
next_step = steps.create!(
|
|
141
|
+
klass: to_klass,
|
|
142
|
+
status: :in_progress,
|
|
143
|
+
step_type: step_type,
|
|
144
|
+
started_at: Time.current
|
|
145
|
+
)
|
|
146
|
+
Ductwork::Job.enqueue(next_step, step.job.return_value)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def combine_next_steps(step_type, edge)
|
|
151
|
+
previous_klasses = parsed_definition[:edges].select do |_, v|
|
|
152
|
+
v.dig(0, :to, 0) == edge[:to].sole && v.dig(0, :type) == "combine"
|
|
153
|
+
end.keys
|
|
154
|
+
|
|
155
|
+
if steps.not_completed.where(klass: previous_klasses).none?
|
|
156
|
+
input_arg = Job.where(
|
|
157
|
+
step: steps.completed.where(klass: previous_klasses)
|
|
158
|
+
).map(&:return_value)
|
|
159
|
+
create_step_and_enqueue_job(
|
|
160
|
+
klass: edge[:to].sole,
|
|
161
|
+
step_type: step_type,
|
|
162
|
+
input_arg: input_arg
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def expand_to_next_steps(step_type, step, edge)
|
|
168
|
+
step.job.return_value.each do |input_arg|
|
|
169
|
+
create_step_and_enqueue_job(
|
|
170
|
+
klass: edge[:to].sole,
|
|
171
|
+
step_type: step_type,
|
|
172
|
+
input_arg: input_arg
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def collapse_next_steps(step_type, step, edge)
|
|
178
|
+
if steps.not_completed.where(klass: step.klass).none?
|
|
179
|
+
input_arg = Job.where(
|
|
180
|
+
step: steps.completed.where(klass: step.klass)
|
|
181
|
+
).map(&:return_value)
|
|
182
|
+
create_step_and_enqueue_job(
|
|
183
|
+
klass: edge[:to].sole,
|
|
184
|
+
step_type: step_type,
|
|
185
|
+
input_arg: input_arg
|
|
186
|
+
)
|
|
187
|
+
else
|
|
188
|
+
Ductwork.configuration.logger.debug(
|
|
189
|
+
msg: "Not all expanded steps have completed",
|
|
190
|
+
role: :pipeline_advancer
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Process < Ductwork::Record
|
|
5
|
+
class NotFoundError < StandardError; end
|
|
6
|
+
|
|
7
|
+
validates :pid, uniqueness: { scope: :machine_identifier }
|
|
8
|
+
|
|
9
|
+
def self.report_heartbeat!
|
|
10
|
+
pid = ::Process.pid
|
|
11
|
+
machine_identifier = Ductwork::MachineIdentifier.fetch
|
|
12
|
+
|
|
13
|
+
find_by!(pid:, machine_identifier:)
|
|
14
|
+
.update!(last_heartbeat_at: Time.current)
|
|
15
|
+
rescue ActiveRecord::RecordNotFound
|
|
16
|
+
raise NotFoundError, "Process #{pid} not found"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Record < ActiveRecord::Base
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
|
|
7
|
+
if Ductwork.configuration.database.present?
|
|
8
|
+
connects_to(database: { writing: Ductwork.configuration.database.to_sym })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.table_name_prefix
|
|
12
|
+
"ductwork_"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Result < Ductwork::Record
|
|
5
|
+
belongs_to :execution, class_name: "Ductwork::Execution"
|
|
6
|
+
|
|
7
|
+
validates :result_type, presence: true
|
|
8
|
+
|
|
9
|
+
enum :result_type,
|
|
10
|
+
success: "success",
|
|
11
|
+
failure: "failure"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Step < Ductwork::Record
|
|
5
|
+
belongs_to :pipeline, class_name: "Ductwork::Pipeline"
|
|
6
|
+
has_one :job, class_name: "Ductwork::Job", foreign_key: "step_id"
|
|
7
|
+
|
|
8
|
+
validates :klass, presence: true
|
|
9
|
+
validates :status, presence: true
|
|
10
|
+
validates :step_type, presence: true
|
|
11
|
+
|
|
12
|
+
enum :status,
|
|
13
|
+
pending: "pending",
|
|
14
|
+
in_progress: "in_progress",
|
|
15
|
+
advancing: "advancing",
|
|
16
|
+
failed: "failed",
|
|
17
|
+
completed: "completed"
|
|
18
|
+
|
|
19
|
+
enum :step_type,
|
|
20
|
+
start: "start",
|
|
21
|
+
default: "default", # `chain` is used by AR
|
|
22
|
+
divide: "divide",
|
|
23
|
+
combine: "combine",
|
|
24
|
+
expand: "expand",
|
|
25
|
+
collapse: "collapse"
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ductwork/cli.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Ductwork
|
|
6
|
+
class CLI
|
|
7
|
+
class << self
|
|
8
|
+
def start!(args)
|
|
9
|
+
options = parse_options(args)
|
|
10
|
+
Ductwork.configuration = Configuration.new(**options)
|
|
11
|
+
Ductwork.configuration.logger = if Ductwork.configuration.logger_source == "rails"
|
|
12
|
+
Rails.logger
|
|
13
|
+
else
|
|
14
|
+
Ductwork::Configuration::DEFAULT_LOGGER
|
|
15
|
+
end
|
|
16
|
+
Ductwork.configuration.logger.level = Ductwork.configuration.logger_level
|
|
17
|
+
|
|
18
|
+
Ductwork::Processes::SupervisorRunner.start!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def parse_options(args)
|
|
24
|
+
options = {}
|
|
25
|
+
|
|
26
|
+
OptionParser.new do |op|
|
|
27
|
+
op.banner = "ductwork [options]"
|
|
28
|
+
|
|
29
|
+
op.on("-c", "--config PATH", "path to YAML config file") do |arg|
|
|
30
|
+
options[:path] = arg
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
op.on("-h", "--help", "Prints this help") do
|
|
34
|
+
puts op
|
|
35
|
+
exit
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
op.on("-v", "--version", "Prints the version") do
|
|
39
|
+
puts "Ductwork #{Ductwork::VERSION}"
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
end.parse!(args)
|
|
43
|
+
|
|
44
|
+
options
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_ENV = :default
|
|
6
|
+
DEFAULT_FILE_PATH = "config/ductwork.yml"
|
|
7
|
+
DEFAULT_JOB_WORKER_COUNT = 5 # threads
|
|
8
|
+
DEFAULT_JOB_WORKER_MAX_RETRY = 3 # attempts
|
|
9
|
+
DEFAULT_JOB_WORKER_POLLING_TIMEOUT = 1 # second
|
|
10
|
+
DEFAULT_JOB_WORKER_SHUTDOWN_TIMEOUT = 20 # seconds
|
|
11
|
+
DEFAULT_LOGGER_LEVEL = ::Logger::INFO
|
|
12
|
+
DEFAULT_LOGGER_SOURCE = "default" # `Logger` instance writing to STDOUT
|
|
13
|
+
DEFAULT_PIPELINE_POLLING_TIMEOUT = 1 # second
|
|
14
|
+
DEFAULT_PIPELINE_SHUTDOWN_TIMEOUT = 20 # seconds
|
|
15
|
+
DEFAULT_SUPERVISOR_POLLING_TIMEOUT = 1 # second
|
|
16
|
+
DEFAULT_SUPERVISOR_SHUTDOWN_TIMEOUT = 30 # seconds
|
|
17
|
+
DEFAULT_LOGGER = ::Logger.new($stdout)
|
|
18
|
+
PIPELINES_WILDCARD = "*"
|
|
19
|
+
|
|
20
|
+
attr_accessor :logger
|
|
21
|
+
attr_writer :job_worker_polling_timeout, :job_worker_shutdown_timeout,
|
|
22
|
+
:job_worker_max_retry,
|
|
23
|
+
:logger_level,
|
|
24
|
+
:pipeline_polling_timeout, :pipeline_shutdown_timeout,
|
|
25
|
+
:supervisor_polling_timeout, :supervisor_shutdown_timeout
|
|
26
|
+
|
|
27
|
+
def initialize(path: DEFAULT_FILE_PATH)
|
|
28
|
+
full_path = Pathname.new(path)
|
|
29
|
+
data = ActiveSupport::ConfigurationFile.parse(full_path).deep_symbolize_keys
|
|
30
|
+
env = defined?(Rails) ? Rails.env.to_sym : DEFAULT_ENV
|
|
31
|
+
@config = data[env]
|
|
32
|
+
rescue Errno::ENOENT
|
|
33
|
+
@config = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pipelines
|
|
37
|
+
raw_pipelines = config[:pipelines] || []
|
|
38
|
+
|
|
39
|
+
if raw_pipelines == PIPELINES_WILDCARD
|
|
40
|
+
Dir
|
|
41
|
+
.glob("**/*.rb", base: "app/pipelines")
|
|
42
|
+
.map { |path| path.delete_suffix(".rb").camelize }
|
|
43
|
+
else
|
|
44
|
+
raw_pipelines.map(&:strip)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def database
|
|
49
|
+
config[:database]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def job_worker_count(pipeline)
|
|
53
|
+
raw_count = config.dig(:job_worker, :count) || DEFAULT_JOB_WORKER_COUNT
|
|
54
|
+
|
|
55
|
+
if raw_count.is_a?(Hash)
|
|
56
|
+
raw_count[pipeline.to_sym]
|
|
57
|
+
else
|
|
58
|
+
raw_count
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def job_worker_max_retry
|
|
63
|
+
@job_worker_max_retry ||= fetch_job_worker_max_retry
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def job_worker_polling_timeout
|
|
67
|
+
@job_worker_polling_timeout ||= fetch_job_worker_polling_timeout
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def job_worker_shutdown_timeout
|
|
71
|
+
@job_worker_shutdown_timeout ||= fetch_job_worker_shutdown_timeout
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def logger_level
|
|
75
|
+
@logger_level ||= fetch_logger_level
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def logger_source
|
|
79
|
+
@logger_source ||= fetch_logger_source
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def pipeline_polling_timeout
|
|
83
|
+
@pipeline_polling_timeout ||= fetch_pipeline_polling_timeout
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def pipeline_shutdown_timeout
|
|
87
|
+
@pipeline_shutdown_timeout ||= fetch_pipeline_shutdown_timeout
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def supervisor_polling_timeout
|
|
91
|
+
@supervisor_polling_timeout ||= fetch_supervisor_polling_timeout
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def supervisor_shutdown_timeout
|
|
95
|
+
@supervisor_shutdown_timeout ||= fetch_supervisor_shutdown_timeout
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
attr_reader :config
|
|
101
|
+
|
|
102
|
+
def fetch_job_worker_max_retry
|
|
103
|
+
config.dig(:job_worker, :max_retry) ||
|
|
104
|
+
DEFAULT_JOB_WORKER_MAX_RETRY
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def fetch_job_worker_polling_timeout
|
|
108
|
+
config.dig(:job_worker, :polling_timeout) ||
|
|
109
|
+
DEFAULT_JOB_WORKER_POLLING_TIMEOUT
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_job_worker_shutdown_timeout
|
|
113
|
+
config.dig(:job_worker, :shutdown_timeout) ||
|
|
114
|
+
DEFAULT_JOB_WORKER_SHUTDOWN_TIMEOUT
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fetch_logger_level
|
|
118
|
+
config.dig(:logger, :level) || DEFAULT_LOGGER_LEVEL
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def fetch_logger_source
|
|
122
|
+
config.dig(:logger, :source) || DEFAULT_LOGGER_SOURCE
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def fetch_pipeline_polling_timeout
|
|
126
|
+
config.dig(:pipeline_advancer, :polling_timeout) ||
|
|
127
|
+
DEFAULT_PIPELINE_POLLING_TIMEOUT
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def fetch_pipeline_shutdown_timeout
|
|
131
|
+
config.dig(:pipeline_advancer, :shutdown_timeout) ||
|
|
132
|
+
DEFAULT_PIPELINE_SHUTDOWN_TIMEOUT
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def fetch_supervisor_polling_timeout
|
|
136
|
+
config.dig(:supervisor, :polling_timeout) ||
|
|
137
|
+
DEFAULT_SUPERVISOR_POLLING_TIMEOUT
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fetch_supervisor_shutdown_timeout
|
|
141
|
+
config.dig(:supervisor, :shutdown_timeout) ||
|
|
142
|
+
DEFAULT_SUPERVISOR_SHUTDOWN_TIMEOUT
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
module DSL
|
|
5
|
+
class BranchBuilder
|
|
6
|
+
class CollapseError < StandardError; end
|
|
7
|
+
|
|
8
|
+
attr_reader :last_node
|
|
9
|
+
|
|
10
|
+
def initialize(klass:, definition:)
|
|
11
|
+
@last_node = klass.name
|
|
12
|
+
@definition = definition
|
|
13
|
+
@expansions = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def chain(next_klass)
|
|
17
|
+
definition[:edges][last_node] << {
|
|
18
|
+
to: [next_klass.name],
|
|
19
|
+
type: :chain,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
definition[:nodes].push(next_klass.name)
|
|
23
|
+
definition[:edges][next_klass.name] ||= []
|
|
24
|
+
@last_node = next_klass.name
|
|
25
|
+
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def divide(to:)
|
|
30
|
+
definition[:edges][last_node] << {
|
|
31
|
+
to: to.map(&:name),
|
|
32
|
+
type: :divide,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
definition[:nodes].push(*to.map(&:name))
|
|
36
|
+
sub_branches = to.map do |klass|
|
|
37
|
+
definition[:edges][klass.name] ||= []
|
|
38
|
+
|
|
39
|
+
Ductwork::DSL::BranchBuilder.new(klass:, definition:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
yield sub_branches
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def combine(*branch_builders, into:)
|
|
48
|
+
definition[:edges][last_node] << {
|
|
49
|
+
to: [into.name],
|
|
50
|
+
type: :combine,
|
|
51
|
+
}
|
|
52
|
+
branch_builders.each do |branch|
|
|
53
|
+
definition[:edges][branch.last_node] << {
|
|
54
|
+
to: [into.name],
|
|
55
|
+
type: :combine,
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
definition[:nodes].push(into.name)
|
|
59
|
+
definition[:edges][into.name] ||= []
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def expand(to:)
|
|
65
|
+
definition[:edges][last_node] << {
|
|
66
|
+
to: [to.name],
|
|
67
|
+
type: :expand,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
definition[:nodes].push(to.name)
|
|
71
|
+
definition[:edges][to.name] ||= []
|
|
72
|
+
@last_node = to.name
|
|
73
|
+
@expansions += 1
|
|
74
|
+
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collapse(into:)
|
|
79
|
+
if expansions.zero?
|
|
80
|
+
raise CollapseError,
|
|
81
|
+
"Must expand pipeline definition before collapsing steps"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
definition[:edges][last_node] << {
|
|
85
|
+
to: [into.name],
|
|
86
|
+
type: :collapse,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
definition[:nodes].push(into.name)
|
|
90
|
+
definition[:edges][into.name] ||= []
|
|
91
|
+
@last_node = into.name
|
|
92
|
+
@expansions -= 1
|
|
93
|
+
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
attr_reader :definition, :expansions
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|