ductwork 0.26.0 → 1.0.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/audit-clock-drift/SKILL.md +11 -0
  3. data/.claude/skills/audit-database-indexes/SKILL.md +11 -0
  4. data/.claude/skills/audit-database-support/SKILL.md +18 -0
  5. data/.claude/skills/audit-durability/SKILL.md +17 -0
  6. data/.saturnci/database.yml +1 -0
  7. data/CHANGELOG-PRO.md +60 -0
  8. data/CHANGELOG.md +144 -1
  9. data/CLAUDE.md +34 -0
  10. data/README.md +24 -14
  11. data/app/controllers/ductwork/application_controller.rb +3 -3
  12. data/app/controllers/ductwork/dashboards_controller.rb +1 -1
  13. data/app/controllers/ductwork/pipelines_controller.rb +4 -3
  14. data/app/helpers/ductwork/application_helper.rb +2 -2
  15. data/app/views/ductwork/dashboards/show.html.erb +12 -12
  16. data/app/views/ductwork/pipelines/index.html.erb +12 -12
  17. data/app/views/ductwork/pipelines/show.html.erb +13 -13
  18. data/app/views/ductwork/step_errors/index.html.erb +2 -2
  19. data/lib/ductwork/branch_claim.rb +185 -0
  20. data/lib/ductwork/cli.rb +73 -24
  21. data/lib/ductwork/configuration.rb +73 -3
  22. data/lib/ductwork/context.rb +17 -19
  23. data/lib/ductwork/database_clock.rb +84 -0
  24. data/lib/ductwork/dsl/branch_builder.rb +6 -1
  25. data/lib/ductwork/dsl/definition_builder.rb +24 -1
  26. data/lib/ductwork/{job_claim.rb → execution_claim.rb} +7 -6
  27. data/lib/ductwork/fault_injection.rb +35 -0
  28. data/lib/ductwork/migration_helper.rb +1 -1
  29. data/lib/ductwork/models/advancement.rb +48 -0
  30. data/lib/ductwork/models/attempt.rb +9 -0
  31. data/lib/ductwork/models/availability.rb +2 -0
  32. data/lib/ductwork/models/branch.rb +800 -0
  33. data/lib/ductwork/models/branch_link.rb +10 -0
  34. data/lib/ductwork/models/execution.rb +199 -2
  35. data/lib/ductwork/models/job.rb +8 -121
  36. data/lib/ductwork/models/pipeline.rb +150 -272
  37. data/lib/ductwork/models/process.rb +177 -5
  38. data/lib/ductwork/models/result.rb +2 -1
  39. data/lib/ductwork/models/run.rb +119 -1
  40. data/lib/ductwork/models/step.rb +26 -6
  41. data/lib/ductwork/models/transition.rb +15 -0
  42. data/lib/ductwork/models/tuple.rb +1 -4
  43. data/lib/ductwork/models/workflow.rb +3 -0
  44. data/lib/ductwork/optimistic_locking_execution_claim.rb +97 -0
  45. data/lib/ductwork/processes/health_check.rb +87 -0
  46. data/lib/ductwork/processes/job_worker.rb +86 -20
  47. data/lib/ductwork/processes/job_worker_runner.rb +25 -37
  48. data/lib/ductwork/processes/pipeline_advancer.rb +48 -76
  49. data/lib/ductwork/processes/pipeline_advancer_runner.rb +34 -44
  50. data/lib/ductwork/processes/process_supervisor.rb +87 -10
  51. data/lib/ductwork/processes/thread_supervisor.rb +55 -21
  52. data/lib/ductwork/processes/thread_supervisor_runner.rb +8 -5
  53. data/lib/ductwork/processes/worker_health_check.rb +55 -0
  54. data/lib/ductwork/row_locking_execution_claim.rb +77 -0
  55. data/lib/ductwork/testing/rspec.rb +28 -11
  56. data/lib/ductwork/version.rb +1 -1
  57. data/lib/ductwork.rb +5 -0
  58. data/lib/generators/ductwork/install/install_generator.rb +14 -4
  59. data/lib/generators/ductwork/install/templates/config/ductwork.yml +4 -0
  60. data/lib/generators/ductwork/install/templates/db/create_ductwork_advancements.rb +29 -0
  61. data/lib/generators/ductwork/install/templates/db/create_ductwork_attempts.rb +20 -0
  62. data/lib/generators/ductwork/install/templates/db/create_ductwork_availabilities.rb +18 -5
  63. data/lib/generators/ductwork/install/templates/db/create_ductwork_branch_links.rb +27 -0
  64. data/lib/generators/ductwork/install/templates/db/create_ductwork_branches.rb +37 -0
  65. data/lib/generators/ductwork/install/templates/db/create_ductwork_executions.rb +8 -1
  66. data/lib/generators/ductwork/install/templates/db/create_ductwork_pipelines.rb +0 -8
  67. data/lib/generators/ductwork/install/templates/db/create_ductwork_processes.rb +2 -0
  68. data/lib/generators/ductwork/install/templates/db/create_ductwork_results.rb +1 -0
  69. data/lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb +45 -6
  70. data/lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb +20 -5
  71. data/lib/generators/ductwork/install/templates/db/create_ductwork_transitions.rb +43 -0
  72. data/lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb +3 -3
  73. data/lib/generators/ductwork/update/templates/db/add_crash_count_to_ductwork_advancements.rb +7 -0
  74. data/lib/generators/ductwork/update/templates/db/add_crash_count_to_ductwork_executions.rb +14 -0
  75. data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_results.rb +7 -0
  76. data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_runs.rb +9 -0
  77. data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_transitions.rb +16 -0
  78. data/lib/generators/ductwork/update/templates/db/add_pipeline_started_index_to_ductwork_runs.rb +7 -0
  79. data/lib/generators/ductwork/update/templates/db/add_process_id_to_ductwork_executions.rb +35 -0
  80. data/lib/generators/ductwork/update/templates/db/add_role_to_ductwork_processes.rb +20 -0
  81. data/lib/generators/ductwork/update/templates/db/associate_branches_to_runs.rb +74 -0
  82. data/lib/generators/ductwork/update/templates/db/associate_steps_to_branches.rb +24 -0
  83. data/lib/generators/ductwork/update/templates/db/associate_steps_to_runs.rb +72 -0
  84. data/lib/generators/ductwork/update/templates/db/associate_tuples_to_runs.rb +73 -0
  85. data/lib/generators/ductwork/update/templates/db/backfill_branch_ids_on_steps.rb +21 -0
  86. data/lib/generators/ductwork/update/templates/db/create_ductwork_advancements.rb +28 -0
  87. data/lib/generators/ductwork/update/templates/db/create_ductwork_branch_links.rb +27 -0
  88. data/lib/generators/ductwork/update/templates/db/create_ductwork_branches.rb +37 -0
  89. data/lib/generators/ductwork/update/templates/db/create_ductwork_runs.rb +55 -0
  90. data/lib/generators/ductwork/update/templates/db/create_ductwork_transitions.rb +32 -0
  91. data/lib/generators/ductwork/update/templates/db/denormalize_pipeline_klass_on_availabilities.rb +11 -4
  92. data/lib/generators/ductwork/update/templates/db/migrate_tables_to_uuid_primary_key.rb +5 -0
  93. data/lib/generators/ductwork/update/templates/db/rename_runs_to_attempts.rb +7 -0
  94. data/lib/generators/ductwork/update/templates/db/update_process_associations.rb +29 -0
  95. data/lib/generators/ductwork/update/update_generator.rb +99 -0
  96. metadata +47 -6
  97. data/lib/ductwork/optimistic_locking_job_claim.rb +0 -88
  98. data/lib/ductwork/row_locking_job_claim.rb +0 -75
@@ -2,16 +2,13 @@
2
2
 
3
3
  module Ductwork
4
4
  class Pipeline < Ductwork::Record # rubocop:todo Metrics/ClassLength
5
- has_many :steps, class_name: "Ductwork::Step", foreign_key: "pipeline_id", dependent: :destroy
6
- has_many :tuples, class_name: "Ductwork::Tuple", foreign_key: "pipeline_id", dependent: :destroy
5
+ has_many :runs,
6
+ class_name: "Ductwork::Run",
7
+ foreign_key: "pipeline_id",
8
+ dependent: :destroy
7
9
 
8
10
  validates :klass, presence: true
9
- validates :definition, presence: true
10
- validates :definition_sha1, presence: true
11
11
  validates :status, presence: true
12
- validates :started_at, presence: true
13
- validates :triggered_at, presence: true
14
- validates :last_advanced_at, presence: true
15
12
 
16
13
  enum :status,
17
14
  pending: "pending",
@@ -31,6 +28,7 @@ module Ductwork
31
28
  end
32
29
 
33
30
  class DefinitionError < StandardError; end
31
+ class ReviveError < StandardError; end
34
32
 
35
33
  class << self
36
34
  attr_reader :pipeline_definition
@@ -53,31 +51,42 @@ module Ductwork
53
51
  Ductwork.defined_pipelines << name.to_s
54
52
  end
55
53
 
56
- def trigger(*args)
54
+ def trigger(*args) # rubocop:todo Metrics
57
55
  if pipeline_definition.nil?
58
56
  raise DefinitionError, "Pipeline must be defined before triggering"
59
57
  end
60
58
 
59
+ now = Time.current
61
60
  node = pipeline_definition.dig(:nodes, 0)
62
61
  klass = pipeline_definition.dig(:edges, node, :klass)
63
62
  definition = JSON.dump(pipeline_definition)
64
63
 
65
- pipeline = Record.transaction do
64
+ pipeline = Record.transaction do # rubocop:todo Metrics/BlockLength
66
65
  p = create!(
67
66
  klass: name.to_s,
67
+ status: :in_progress
68
+ )
69
+ run = p.runs.create!(
70
+ pipeline_klass: name.to_s,
68
71
  status: :in_progress,
69
72
  definition: definition,
70
73
  definition_sha1: Digest::SHA1.hexdigest(definition),
71
- triggered_at: Time.current,
72
- started_at: Time.current,
73
- last_advanced_at: Time.current
74
+ triggered_at: now,
75
+ started_at: now
74
76
  )
75
- step = p.steps.create!(
77
+ branch = run.branches.create!(
78
+ pipeline_klass: name.to_s,
79
+ status: :in_progress,
80
+ started_at: now,
81
+ last_advanced_at: now
82
+ )
83
+ step = branch.steps.create!(
84
+ run: run,
76
85
  node: node,
77
86
  klass: klass,
78
87
  status: :in_progress,
79
88
  to_transition: :start,
80
- started_at: Time.current
89
+ started_at: now
81
90
  )
82
91
  Ductwork::Job.enqueue(step, *args)
83
92
 
@@ -94,306 +103,175 @@ module Ductwork
94
103
  end
95
104
  end
96
105
 
97
- def advance!
98
- # NOTE: if we've expanded the pipeline there could be a lot of
99
- # advancing records which may cause memory issues. something to
100
- # watch out for here and maybe add in config to use AR relation
101
- # at certain counts or even memory limits.
102
- advancing_steps = steps.advancing.pluck(:id, :node, :klass)
103
- advancing_ids = advancing_steps.map(&:first)
104
- edges = find_edges(advancing_steps)
106
+ def current_run
107
+ runs.in_progress.sole
108
+ end
105
109
 
106
- Ductwork::Record.transaction do
107
- if edges.nil? || edges.all? { |_, attrs| attrs[:to].blank? }
108
- conditionally_complete_pipeline(advancing_ids)
109
- else
110
- advance_to_next_steps_by_type(edges, advancing_ids)
111
- end
110
+ def revive!(duplicate_context: false)
111
+ if !halted?
112
+ raise ReviveError, "Cannot revive #{status} pipeline"
112
113
  end
113
- end
114
114
 
115
- def parsed_definition
116
- @parsed_definition ||= JSON.parse(definition).with_indifferent_access
117
- end
115
+ last_run = runs.order(started_at: :desc).first
118
116
 
119
- def complete!
120
- update!(status: :completed, completed_at: Time.current)
117
+ if last_run.blank?
118
+ raise ReviveError, "Cannot revive pipeline without previous run"
119
+ end
121
120
 
122
- Ductwork.logger.info(
123
- msg: "Pipeline completed",
124
- pipeline_id: id,
125
- role: :pipeline_advancer
126
- )
127
- end
121
+ now = Time.current
122
+ new_run = last_run.dup
123
+ new_run.triggered_at = now
124
+ new_run.started_at = now
125
+ new_run.status = "in_progress"
126
+ new_run.on_halt_dispatched_at = nil
128
127
 
129
- def halt!
130
- update!(status: :halted, halted_at: Time.current)
128
+ Ductwork::Record.transaction do
129
+ lock!
130
+ new_run.save!
131
+ duplicate_successful_branches_and_steps(new_run, last_run, now)
132
+ duplicate_halted_branches_and_steps(new_run, last_run, now)
133
+ conditionally_duplicate_context(last_run, new_run, now, duplicate_context)
134
+ duplicate_all_branch_links(new_run, last_run)
135
+ in_progress!
136
+ end
131
137
 
132
- Ductwork.logger.info(
133
- msg: "Pipeline halted",
134
- pipeline_id: id,
135
- pipeline_klass: klass
136
- )
138
+ self
137
139
  end
138
140
 
139
141
  private
140
142
 
141
- def create_step_and_enqueue_job(edge:, input_arg:, node: nil)
142
- status = :in_progress
143
- started_at = Time.current
144
- # NOTE: "chain" is used by ActiveRecord so we have to call
145
- # this enum value "default" :sad:
146
- to_transition = edge[:type] == "chain" ? "default" : edge[:type]
147
- node ||= edge[:to].sole
148
- klass = parsed_definition.dig(:edges, node, :klass)
149
-
150
- next_step = steps.create!(node:, klass:, status:, to_transition:, started_at:)
151
- Ductwork::Job.enqueue(next_step, input_arg)
152
- end
153
-
154
- def find_edges(advancing_steps)
155
- if advancing_steps.any?
156
- nodes = advancing_steps.map(&:second)
143
+ def duplicate_successful_branches_and_steps(new_run, last_run, now)
144
+ status = %i[advancing waiting completed]
157
145
 
158
- parsed_definition.fetch(:edges, {}).select { |k| k.in?(nodes) }
159
- end
160
- end
146
+ last_run.branches.where(status:).find_each do |branch|
147
+ new_branch = branch.dup
148
+ new_branch.run = new_run
149
+ new_branch.started_at = now
150
+ new_branch.completed_at = now
161
151
 
162
- def conditionally_complete_pipeline(advancing_ids)
163
- steps
164
- .where(id: advancing_ids)
165
- .update_all(status: :completed, completed_at: Time.current)
152
+ new_branch.save!
166
153
 
167
- remaining = steps
168
- .where(status: %w[in_progress pending advancing])
169
- .where.not(id: advancing_ids)
170
- .exists?
154
+ branch.steps.where(status:).find_each do |step|
155
+ new_step = step.dup
156
+ new_step.source_step = step
157
+ new_step.branch = new_branch
158
+ new_step.run = new_run
159
+ new_step.started_at = now
160
+ new_step.completed_at = now
171
161
 
172
- if !remaining
173
- complete!
174
- end
175
- end
176
-
177
- def advance_to_next_steps_by_type(edges, advancing_ids)
178
- steps.where(id: advancing_ids).update_all(status: :completed, completed_at: Time.current)
179
-
180
- if edges.all? { |_, attrs| attrs[:type] == "combine" }
181
- conditionally_combine_next_steps(edges, advancing_ids)
182
- else
183
- edges.each do |node, attrs|
184
- if attrs[:type] == "collapse"
185
- conditionally_collapse_next_steps(node, attrs, advancing_ids)
186
- else
187
- advance_non_merging_steps(attrs, advancing_ids)
188
- end
162
+ new_step.save!
189
163
  end
190
164
  end
191
- log_pipeline_advanced(edges)
192
165
  end
193
166
 
194
- def advance_non_merging_steps(edge, advancing_ids)
195
- to_transition = edge[:type]
196
- klass = edge[:klass]
197
-
198
- steps.where(id: advancing_ids, klass: klass).find_each do |step|
199
- if to_transition.in?(%w[chain divide divert converge])
200
- advance_to_next_steps(step.id, edge)
201
- elsif to_transition == "expand"
202
- expand_to_next_steps(step.id, edge)
203
- else
204
- Ductwork.logger.error(
205
- msg: "Invalid To Transition",
206
- to_transition: to_transition,
207
- pipeline_id: id,
208
- role: :pipeline_advancer
209
- )
210
- end
211
- end
212
- end
213
-
214
- def advance_to_next_steps(step_id, edge)
215
- if edge[:type] == "divert"
216
- advance_to_divert_step(step_id, edge)
217
- else
218
- advance_to_array_steps(step_id, edge)
167
+ def duplicate_halted_branches_and_steps(new_run, last_run, now)
168
+ last_run.branches.where(status: :halted).find_each do |branch|
169
+ new_branch = branch.dup
170
+ new_branch.run = new_run
171
+ new_branch.status = "in_progress"
172
+ new_branch.started_at = now
173
+ new_branch.completed_at = nil
174
+ new_branch.last_advanced_at = now
175
+
176
+ new_branch.save!
177
+ revive_branch_steps(branch, new_branch, new_run, now)
219
178
  end
220
179
  end
221
180
 
222
- def advance_to_divert_step(step_id, edge)
223
- input_arg = Ductwork::Job.find_by(step_id:).return_value
224
- node = edge[:to][input_arg.to_s] || edge[:to]["otherwise"]
181
+ def revive_branch_steps(branch, new_branch, new_run, now)
182
+ duplicate_prior_steps_as_completed(branch, new_branch, new_run, now)
225
183
 
226
- if node.nil?
227
- halt!
184
+ if branch.job_retries_exhausted?
185
+ re_enqueue_failed_step(branch, new_branch, new_run, now)
228
186
  else
229
- create_step_and_enqueue_job(edge:, input_arg:, node:)
187
+ re_advance_latest_step(branch, new_branch, new_run, now)
230
188
  end
231
189
  end
232
190
 
233
- def advance_to_array_steps(step_id, edge)
234
- too_many = edge[:to].tally.any? do |to_klass, count|
235
- depth = Ductwork
236
- .configuration
237
- .steps_max_depth(pipeline: klass, step: to_klass)
191
+ def duplicate_prior_steps_as_completed(branch, new_branch, new_run, now)
192
+ latest_step = branch.steps.order(started_at: :desc).first
238
193
 
239
- depth != -1 && count > depth
240
- end
194
+ return if latest_step.blank?
241
195
 
242
- if too_many
243
- halt!
244
- else
245
- edge[:to].each do |node|
246
- input_arg = Ductwork::Job.find_by(step_id:).return_value
247
- create_step_and_enqueue_job(edge:, input_arg:, node:)
248
- end
249
- end
250
- end
196
+ status = %i[advancing waiting completed]
197
+ id = latest_step.id
251
198
 
252
- def conditionally_combine_next_steps(edges, advancing_ids)
253
- if steps.where(status: %w[pending in_progress], node: edges.keys).none?
254
- combine_next_steps(edges, advancing_ids)
255
- else
256
- Ductwork.logger.debug(
257
- msg: "Not all divided steps have completed; not combining",
258
- pipeline_id: id,
259
- role: :pipeline_advancer
260
- )
261
- end
262
- end
199
+ branch.steps.where(status:).where.not(id:).find_each do |step|
200
+ new_step = step.dup
201
+ new_step.source_step = step
202
+ new_step.branch = new_branch
203
+ new_step.run = new_run
204
+ new_step.started_at = now
205
+ new_step.completed_at = now
263
206
 
264
- def combine_next_steps(edges, advancing_ids)
265
- edge = edges.values.sample
266
- groups = steps
267
- .where(id: advancing_ids)
268
- .group(:node)
269
- .count
270
- .keys
271
- .map { |node| steps.where(id: advancing_ids).where(node:) }
272
-
273
- groups.first.zip(*groups[1..]).each do |group|
274
- input_arg = Ductwork::Job
275
- .where(step_id: group.map(&:id))
276
- .map(&:return_value)
277
- create_step_and_enqueue_job(edge:, input_arg:)
207
+ new_step.save!
278
208
  end
279
209
  end
280
210
 
281
- def expand_to_next_steps(step_id, edge)
282
- next_klass = parsed_definition.dig(:edges, edge[:to].sole, :klass)
283
- return_value = Ductwork::Job
284
- .find_by(step_id:)
285
- .return_value
286
- max_depth = Ductwork.configuration.steps_max_depth(pipeline: klass, step: next_klass)
287
-
288
- if max_depth != -1 && return_value.count > max_depth
289
- halt!
290
- elsif return_value.none?
291
- complete!
292
- else
293
- bulk_create_steps_and_jobs(edge:, return_value:)
294
- end
295
- end
296
-
297
- def bulk_create_steps_and_jobs(edge:, return_value:) # rubocop:todo Metrics
298
- # NOTE: "chain" is used by ActiveRecord so we have to call
299
- # this enum value "default" :sad:
300
- to_transition = edge[:type] == "chain" ? "default" : edge[:type]
301
- node ||= edge[:to].sole
302
- step_klass = parsed_definition.dig(:edges, node, :klass)
303
- now = Time.current
304
-
305
- Array(return_value).each_slice(1_000).each do |batch| # rubocop:todo Metrics/BlockLength
306
- step_rows = []
307
- job_rows = []
308
- execution_rows = []
309
- availability_rows = []
310
-
311
- batch.each do |value| # rubocop:todo Metrics/BlockLength
312
- step_id = SecureRandom.uuid_v7
313
- job_id = SecureRandom.uuid_v7
314
- execution_id = SecureRandom.uuid_v7
315
- availability_id = SecureRandom.uuid_v7
316
-
317
- step_rows << {
318
- id: step_id,
319
- pipeline_id: id,
320
- node: node,
321
- klass: step_klass,
322
- status: "in_progress",
323
- to_transition: to_transition,
324
- started_at: now,
325
- created_at: now,
326
- updated_at: now,
327
- }
328
- job_rows << {
329
- id: job_id,
330
- step_id: step_id,
331
- input_args: JSON.dump({ args: [value] }),
332
- klass: step_klass,
333
- started_at: now,
334
- created_at: now,
335
- updated_at: now,
336
- }
337
- execution_rows << {
338
- id: execution_id,
339
- job_id: job_id,
340
- retry_count: 0,
341
- started_at: now,
342
- created_at: now,
343
- updated_at: now,
344
- }
345
- availability_rows << {
346
- id: availability_id,
347
- execution_id: execution_id,
348
- pipeline_klass: klass,
349
- started_at: now,
350
- created_at: now,
351
- updated_at: now,
352
- }
353
- end
354
-
355
- Ductwork::Step.insert_all!(step_rows)
356
- Ductwork::Job.insert_all!(job_rows)
357
- Ductwork::Execution.insert_all!(execution_rows)
358
- Ductwork::Availability.insert_all!(availability_rows)
359
-
360
- Ductwork.logger.info(
361
- msg: "Job batch enqueued",
362
- count: batch.count,
363
- job_klass: step_klass
364
- )
365
- end
211
+ def re_enqueue_failed_step(branch, new_branch, new_run, now)
212
+ failed_step = branch.steps.find_by(status: :failed)
213
+ args = JSON.parse(failed_step.job.input_args).fetch("args")
214
+ step = new_branch.steps.create!(
215
+ run: new_run,
216
+ source_step: failed_step,
217
+ node: failed_step.node,
218
+ klass: failed_step.klass,
219
+ to_transition: failed_step.to_transition,
220
+ status: :in_progress,
221
+ started_at: now
222
+ )
223
+ Ductwork::Job.enqueue(step, *args)
366
224
  end
367
225
 
368
- def conditionally_collapse_next_steps(node, edge, advancing_ids)
369
- if steps.where(status: %w[pending in_progress], node: node).none?
370
- collapse_next_steps(edge, advancing_ids)
371
- else
372
- Ductwork.logger.debug(
373
- msg: "Not all expanded steps have completed; not collapsing",
374
- pipeline_id: id,
375
- role: :pipeline_advancer
376
- )
377
- end
226
+ def re_advance_latest_step(branch, new_branch, new_run, now)
227
+ latest_step = branch.steps.order(started_at: :desc).first
228
+ step = new_branch.steps.create!(
229
+ run: new_run,
230
+ source_step: latest_step,
231
+ node: latest_step.node,
232
+ klass: latest_step.klass,
233
+ to_transition: latest_step.to_transition,
234
+ status: :advancing,
235
+ started_at: now
236
+ )
237
+ job = latest_step.job.dup
238
+ job.step = step
239
+ job.started_at = now
240
+ job.completed_at = now
241
+ job.save!
378
242
  end
379
243
 
380
- def collapse_next_steps(edge, advancing_ids)
381
- input_arg = []
244
+ def conditionally_duplicate_context(last_run, new_run, now, duplicate_context)
245
+ if duplicate_context
246
+ last_run.tuples.find_each do |tuple|
247
+ new_tuple = tuple.dup
248
+ new_tuple.run = new_run
249
+ new_tuple.first_set_at = now
250
+ new_tuple.last_set_at = now
382
251
 
383
- Ductwork::Job.where(step_id: advancing_ids).find_each do |job|
384
- input_arg << job.return_value
252
+ new_tuple.save!
253
+ end
385
254
  end
386
-
387
- create_step_and_enqueue_job(edge:, input_arg:)
388
255
  end
389
256
 
390
- def log_pipeline_advanced(edges)
391
- Ductwork.logger.info(
392
- msg: "Pipeline advanced",
393
- pipeline_id: id,
394
- transitions: edges.map { |_, v| v.dig(-1, :type) },
395
- role: :pipeline_advancer
396
- )
257
+ def duplicate_all_branch_links(new_run, last_run)
258
+ map = Ductwork::Step
259
+ .where(branch_id: new_run.branches.select(:id))
260
+ .where.not(source_step_id: nil)
261
+ .joins("INNER JOIN ductwork_steps src ON src.id = ductwork_steps.source_step_id")
262
+ .pluck("src.branch_id", "ductwork_steps.branch_id")
263
+ .uniq
264
+ .to_h
265
+ old_branch_ids = last_run.branches.select(:id)
266
+
267
+ Ductwork::BranchLink
268
+ .where(parent_branch_id: old_branch_ids, child_branch_id: old_branch_ids)
269
+ .find_each do |old_link|
270
+ Ductwork::BranchLink.create!(
271
+ parent_branch_id: map.fetch(old_link.parent_branch_id),
272
+ child_branch_id: map.fetch(old_link.child_branch_id)
273
+ )
274
+ end
397
275
  end
398
276
  end
399
277
  end