ductwork 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/app/models/ductwork/job.rb +28 -9
- data/app/models/ductwork/pipeline.rb +130 -59
- data/lib/ductwork/dsl/definition_builder.rb +17 -13
- data/lib/ductwork/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c1f2564c8a446cc2daf77b882acfc6abab314e1a651bf1e7a6ab9c7432a77f6
|
|
4
|
+
data.tar.gz: ff33012c7f1dce6f9297c41185e97d8bffc4054ac26738c19459db50a70f6d19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdc5618e70f3b42f758ab6de842ff46732ee34435e82b2b04777aad7e203f0457fc00ce496fa23f2b64e232ecc81548bbff669fddfbe3cdb5c404b3d0036beea
|
|
7
|
+
data.tar.gz: e3697a5c616527013580003f6e0f131c4ddfabf871b01dc2f910527febdb19e2ead1e5f3f244a001242ff0876fe331f84eca116c24111b3fdeffbc05f7205289
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Ductwork Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0]
|
|
4
|
+
|
|
5
|
+
- fix: correctly create collapsing and combining steps and jobs for complex pipelines
|
|
6
|
+
- fix: add a new step and job for each active branch in a running pipeline
|
|
7
|
+
- fix: add a new node and edge for each active branch of the definition
|
|
8
|
+
- feat: add info-level logging for job events
|
|
9
|
+
- feat: add info-level logging for pipeline events
|
|
10
|
+
|
|
3
11
|
## [0.2.1]
|
|
4
12
|
|
|
5
13
|
- fix: do not splat arguments when executing a job nor triggering a pipeline
|
data/app/models/ductwork/job.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ductwork
|
|
4
|
-
class Job < Ductwork::Record
|
|
4
|
+
class Job < Ductwork::Record # rubocop:todo Metrics/ClassLength
|
|
5
5
|
belongs_to :step, class_name: "Ductwork::Step"
|
|
6
6
|
has_many :executions, class_name: "Ductwork::Execution", foreign_key: "job_id", dependent: :destroy
|
|
7
7
|
|
|
@@ -61,13 +61,13 @@ module Ductwork
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def self.enqueue(step, args)
|
|
64
|
-
Ductwork::Record.transaction do
|
|
65
|
-
|
|
64
|
+
job = Ductwork::Record.transaction do
|
|
65
|
+
j = step.create_job!(
|
|
66
66
|
klass: step.klass,
|
|
67
67
|
started_at: Time.current,
|
|
68
68
|
input_args: JSON.dump({ args: })
|
|
69
69
|
)
|
|
70
|
-
execution =
|
|
70
|
+
execution = j.executions.create!(
|
|
71
71
|
started_at: Time.current,
|
|
72
72
|
retry_count: 0
|
|
73
73
|
)
|
|
@@ -75,8 +75,16 @@ module Ductwork
|
|
|
75
75
|
started_at: Time.current
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
j
|
|
79
79
|
end
|
|
80
|
+
|
|
81
|
+
Ductwork.configuration.logger.info(
|
|
82
|
+
msg: "Job enqueued",
|
|
83
|
+
job_id: job.id,
|
|
84
|
+
job_klass: job.klass
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
job
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def execute(pipeline)
|
|
@@ -103,12 +111,13 @@ module Ductwork
|
|
|
103
111
|
execution_failed!(execution, run, e)
|
|
104
112
|
result = "failure"
|
|
105
113
|
ensure
|
|
106
|
-
logger.
|
|
107
|
-
msg: "
|
|
108
|
-
role: :job_worker,
|
|
114
|
+
logger.info(
|
|
115
|
+
msg: "Job executed",
|
|
109
116
|
pipeline: pipeline,
|
|
117
|
+
job_id: id,
|
|
110
118
|
job_klass: klass,
|
|
111
|
-
result: result
|
|
119
|
+
result: result,
|
|
120
|
+
role: :job_worker
|
|
112
121
|
)
|
|
113
122
|
end
|
|
114
123
|
end
|
|
@@ -167,6 +176,16 @@ module Ductwork
|
|
|
167
176
|
end
|
|
168
177
|
end
|
|
169
178
|
|
|
179
|
+
logger.warn(
|
|
180
|
+
msg: "Job errored",
|
|
181
|
+
error_klass: error.class.name,
|
|
182
|
+
error_message: error.message,
|
|
183
|
+
job_id: id,
|
|
184
|
+
job_klass: klass,
|
|
185
|
+
pipeline_id: pipeline.id,
|
|
186
|
+
role: :job_worker
|
|
187
|
+
)
|
|
188
|
+
|
|
170
189
|
# NOTE: perform lifecycle hook execution outside of the transaction as
|
|
171
190
|
# to not unnecessarily hold it open
|
|
172
191
|
if halted
|
|
@@ -56,8 +56,8 @@ module Ductwork
|
|
|
56
56
|
step_klass = pipeline_definition.dig(:nodes, 0)
|
|
57
57
|
definition = JSON.dump(pipeline_definition)
|
|
58
58
|
|
|
59
|
-
Record.transaction do
|
|
60
|
-
|
|
59
|
+
pipeline = Record.transaction do
|
|
60
|
+
p = create!(
|
|
61
61
|
klass: name.to_s,
|
|
62
62
|
status: :in_progress,
|
|
63
63
|
definition: definition,
|
|
@@ -65,7 +65,7 @@ module Ductwork
|
|
|
65
65
|
triggered_at: Time.current,
|
|
66
66
|
last_advanced_at: Time.current
|
|
67
67
|
)
|
|
68
|
-
step =
|
|
68
|
+
step = p.steps.create!(
|
|
69
69
|
klass: step_klass,
|
|
70
70
|
status: :in_progress,
|
|
71
71
|
step_type: :start,
|
|
@@ -73,24 +73,36 @@ module Ductwork
|
|
|
73
73
|
)
|
|
74
74
|
Ductwork::Job.enqueue(step, args)
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
p
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
Ductwork.configuration.logger.info(
|
|
80
|
+
msg: "Pipeline triggered",
|
|
81
|
+
pipeline_id: pipeline.id,
|
|
82
|
+
role: :application
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
pipeline
|
|
78
86
|
end
|
|
79
87
|
end
|
|
80
88
|
|
|
81
89
|
def advance!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
# NOTE: there could be A LOT of steps advancing for a single pipeline
|
|
91
|
+
# so instead of loading everything into memory and using ruby collection
|
|
92
|
+
# methods we make multiple queries. may need to revisist this once
|
|
93
|
+
# we do extensive load testing
|
|
94
|
+
advancing = steps.advancing
|
|
95
|
+
edges = if advancing.exists?
|
|
96
|
+
parsed_definition
|
|
97
|
+
.fetch(:edges, {})
|
|
98
|
+
.select { |k| k.in?(advancing.pluck(:klass)) }
|
|
99
|
+
end
|
|
86
100
|
|
|
87
101
|
Ductwork::Record.transaction do
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if edge.nil?
|
|
91
|
-
conditionally_complete_pipeline
|
|
102
|
+
if edges.nil? || edges.values.all?(&:empty?)
|
|
103
|
+
conditionally_complete_pipeline(advancing)
|
|
92
104
|
else
|
|
93
|
-
|
|
105
|
+
advance_to_next_steps_by_type(edges, advancing)
|
|
94
106
|
end
|
|
95
107
|
end
|
|
96
108
|
end
|
|
@@ -108,34 +120,64 @@ module Ductwork
|
|
|
108
120
|
@parsed_definition ||= JSON.parse(definition).with_indifferent_access
|
|
109
121
|
end
|
|
110
122
|
|
|
111
|
-
def conditionally_complete_pipeline
|
|
123
|
+
def conditionally_complete_pipeline(advancing)
|
|
124
|
+
advancing.update!(status: :completed, completed_at: Time.current)
|
|
125
|
+
|
|
112
126
|
if steps.where(status: %w[in_progress pending]).none?
|
|
113
127
|
update!(status: :completed, completed_at: Time.current)
|
|
128
|
+
|
|
129
|
+
Ductwork.configuration.logger.info(
|
|
130
|
+
msg: "Pipeline completed",
|
|
131
|
+
pipeline_id: id,
|
|
132
|
+
role: :pipeline_advancer
|
|
133
|
+
)
|
|
114
134
|
end
|
|
115
135
|
end
|
|
116
136
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
137
|
+
def advance_to_next_steps_by_type(edges, advancing)
|
|
138
|
+
if edges.all? { |_, v| v.dig(-1, :type) == "combine" }
|
|
139
|
+
conditionally_combine_next_steps(edges, advancing)
|
|
130
140
|
else
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
edges.each do |step_klass, step_edges|
|
|
142
|
+
edge = step_edges[-1]
|
|
143
|
+
# NOTE: "chain" is used by ActiveRecord so we have to call
|
|
144
|
+
# this enum value "default" :sad:
|
|
145
|
+
step_type = edge[:type] == "chain" ? "default" : edge[:type]
|
|
146
|
+
|
|
147
|
+
if step_type == "collapse"
|
|
148
|
+
conditionally_collapse_next_steps(step_klass, edge, advancing)
|
|
149
|
+
else
|
|
150
|
+
advance_non_merging_steps(step_klass, edges, advancing)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
135
153
|
end
|
|
154
|
+
advancing.update!(status: :completed, completed_at: Time.current)
|
|
155
|
+
log_pipeline_advanced(edges)
|
|
136
156
|
end
|
|
137
157
|
|
|
138
|
-
def
|
|
158
|
+
def advance_non_merging_steps(step_klass, edges, advancing)
|
|
159
|
+
advancing.where(klass: step_klass).find_each do |step|
|
|
160
|
+
edge = edges.dig(step.klass, -1)
|
|
161
|
+
# NOTE: "chain" is used by ActiveRecord so we have to call
|
|
162
|
+
# this enum value "default" :sad:
|
|
163
|
+
step_type = edge[:type] == "chain" ? "default" : edge[:type]
|
|
164
|
+
|
|
165
|
+
if step_type.in?(%w[default divide])
|
|
166
|
+
advance_to_next_steps(step_type, advancing, edge)
|
|
167
|
+
elsif step_type == "expand"
|
|
168
|
+
expand_to_next_steps(step_type, advancing, edge)
|
|
169
|
+
else
|
|
170
|
+
Ductwork.configuration.logger.error(
|
|
171
|
+
msg: "Invalid step type",
|
|
172
|
+
step_type: step_type,
|
|
173
|
+
pipeline_id: id,
|
|
174
|
+
role: :pipeline_advancer
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def advance_to_next_steps(step_type, advancing, edge)
|
|
139
181
|
edge[:to].each do |to_klass|
|
|
140
182
|
next_step = steps.create!(
|
|
141
183
|
klass: to_klass,
|
|
@@ -143,53 +185,82 @@ module Ductwork
|
|
|
143
185
|
step_type: step_type,
|
|
144
186
|
started_at: Time.current
|
|
145
187
|
)
|
|
146
|
-
Ductwork::Job.enqueue(next_step,
|
|
188
|
+
Ductwork::Job.enqueue(next_step, advancing.take.job.return_value)
|
|
147
189
|
end
|
|
148
190
|
end
|
|
149
191
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
192
|
+
def conditionally_combine_next_steps(edges, advancing)
|
|
193
|
+
if steps.where(status: %w[pending in_progress], klass: edges.keys).none?
|
|
194
|
+
combine_next_steps(edges, advancing)
|
|
195
|
+
else
|
|
196
|
+
Ductwork.configuration.logger.debug(
|
|
197
|
+
msg: "Not all divided steps have completed; not combining",
|
|
198
|
+
pipeline_id: id,
|
|
199
|
+
role: :pipeline_advancer
|
|
163
200
|
)
|
|
164
201
|
end
|
|
165
202
|
end
|
|
166
203
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
204
|
+
def combine_next_steps(edges, advancing)
|
|
205
|
+
klass = edges.values.sample.dig(-1, :to).sole
|
|
206
|
+
step_type = "combine"
|
|
207
|
+
groups = advancing
|
|
208
|
+
.group(:klass)
|
|
209
|
+
.count
|
|
210
|
+
.keys
|
|
211
|
+
.map { |k| advancing.where(klass: k) }
|
|
212
|
+
|
|
213
|
+
groups.first.zip(*groups[1..]).each do |group|
|
|
214
|
+
input_arg = Ductwork::Job
|
|
215
|
+
.where(step_id: group.map(&:id))
|
|
216
|
+
.map(&:return_value)
|
|
217
|
+
create_step_and_enqueue_job(klass:, step_type:, input_arg:)
|
|
174
218
|
end
|
|
175
219
|
end
|
|
176
220
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
input_arg = Job.where(
|
|
180
|
-
step: steps.completed.where(klass: step.klass)
|
|
181
|
-
).map(&:return_value)
|
|
221
|
+
def expand_to_next_steps(step_type, advancing, edge)
|
|
222
|
+
Array(advancing.take.job.return_value).each do |input_arg|
|
|
182
223
|
create_step_and_enqueue_job(
|
|
183
224
|
klass: edge[:to].sole,
|
|
184
225
|
step_type: step_type,
|
|
185
226
|
input_arg: input_arg
|
|
186
227
|
)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def conditionally_collapse_next_steps(step_klass, edge, advancing)
|
|
232
|
+
if steps.where(status: %w[pending in_progress], klass: step_klass).none?
|
|
233
|
+
collapse_next_steps(edge[:to].sole, advancing)
|
|
187
234
|
else
|
|
188
235
|
Ductwork.configuration.logger.debug(
|
|
189
|
-
msg: "Not all expanded steps have completed",
|
|
236
|
+
msg: "Not all expanded steps have completed; not collapsing",
|
|
237
|
+
pipeline_id: id,
|
|
190
238
|
role: :pipeline_advancer
|
|
191
239
|
)
|
|
192
240
|
end
|
|
193
241
|
end
|
|
242
|
+
|
|
243
|
+
def collapse_next_steps(klass, advancing)
|
|
244
|
+
step_type = "collapse"
|
|
245
|
+
input_arg = []
|
|
246
|
+
|
|
247
|
+
# NOTE: because of expanding based on return values, there
|
|
248
|
+
# could be A LOT of jobs so we want to use batch methods
|
|
249
|
+
# to avoid creating too many in-memory objects
|
|
250
|
+
Ductwork::Job.where(step_id: advancing.ids).find_each do |job|
|
|
251
|
+
input_arg << job.return_value
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
create_step_and_enqueue_job(klass:, step_type:, input_arg:)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def log_pipeline_advanced(edges)
|
|
258
|
+
Ductwork.configuration.logger.info(
|
|
259
|
+
msg: "Pipeline advanced",
|
|
260
|
+
pipeline_id: id,
|
|
261
|
+
transitions: edges.map { |_, v| v.dig(-1, :type) },
|
|
262
|
+
role: :pipeline_advancer
|
|
263
|
+
)
|
|
264
|
+
end
|
|
194
265
|
end
|
|
195
266
|
end
|
|
@@ -14,6 +14,7 @@ module Ductwork
|
|
|
14
14
|
}
|
|
15
15
|
@divisions = 0
|
|
16
16
|
@expansions = 0
|
|
17
|
+
@last_nodes = []
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def start(klass)
|
|
@@ -29,7 +30,7 @@ module Ductwork
|
|
|
29
30
|
def chain(klass)
|
|
30
31
|
validate_classes!(klass)
|
|
31
32
|
validate_definition_started!(action: "chaining")
|
|
32
|
-
|
|
33
|
+
add_edge_to_last_nodes(klass, type: :chain)
|
|
33
34
|
add_new_nodes(klass)
|
|
34
35
|
|
|
35
36
|
self
|
|
@@ -38,7 +39,7 @@ module Ductwork
|
|
|
38
39
|
def divide(to:)
|
|
39
40
|
validate_classes!(to)
|
|
40
41
|
validate_definition_started!(action: "dividing chain")
|
|
41
|
-
|
|
42
|
+
add_edge_to_last_nodes(*to, type: :divide)
|
|
42
43
|
add_new_nodes(*to)
|
|
43
44
|
|
|
44
45
|
@divisions += 1
|
|
@@ -79,7 +80,7 @@ module Ductwork
|
|
|
79
80
|
def expand(to:)
|
|
80
81
|
validate_classes!(to)
|
|
81
82
|
validate_definition_started!(action: "expanding chain")
|
|
82
|
-
|
|
83
|
+
add_edge_to_last_nodes(to, type: :expand)
|
|
83
84
|
add_new_nodes(to)
|
|
84
85
|
|
|
85
86
|
@expansions += 1
|
|
@@ -91,7 +92,7 @@ module Ductwork
|
|
|
91
92
|
validate_classes!(into)
|
|
92
93
|
validate_definition_started!(action: "collapsing steps")
|
|
93
94
|
validate_definition_expanded!
|
|
94
|
-
|
|
95
|
+
add_edge_to_last_nodes(into, type: :collapse)
|
|
95
96
|
add_new_nodes(into)
|
|
96
97
|
|
|
97
98
|
@expansions -= 1
|
|
@@ -117,7 +118,7 @@ module Ductwork
|
|
|
117
118
|
|
|
118
119
|
private
|
|
119
120
|
|
|
120
|
-
attr_reader :definition, :divisions, :expansions
|
|
121
|
+
attr_reader :definition, :divisions, :expansions, :last_nodes
|
|
121
122
|
|
|
122
123
|
def validate_classes!(klasses)
|
|
123
124
|
valid = Array(klasses).all? do |klass|
|
|
@@ -162,19 +163,22 @@ module Ductwork
|
|
|
162
163
|
end
|
|
163
164
|
|
|
164
165
|
def add_new_nodes(*klasses)
|
|
165
|
-
|
|
166
|
+
nodes = klasses.map(&:name)
|
|
167
|
+
@last_nodes = Array(nodes)
|
|
168
|
+
|
|
169
|
+
definition[:nodes].push(*nodes)
|
|
166
170
|
klasses.each do |klass|
|
|
167
171
|
definition[:edges][klass.name] ||= []
|
|
168
172
|
end
|
|
169
173
|
end
|
|
170
174
|
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
def add_edge_to_last_nodes(*klasses, type:)
|
|
176
|
+
last_nodes.each do |last_node|
|
|
177
|
+
definition[:edges][last_node] << {
|
|
178
|
+
to: klasses.map(&:name),
|
|
179
|
+
type: type,
|
|
180
|
+
}
|
|
181
|
+
end
|
|
178
182
|
end
|
|
179
183
|
end
|
|
180
184
|
end
|
data/lib/ductwork/version.rb
CHANGED