ductwork 0.20.2 → 0.21.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.saturnci/Dockerfile +22 -0
  3. data/.saturnci/database.yml +5 -0
  4. data/.saturnci/docker-compose.yml +10 -0
  5. data/.saturnci/pre.sh +4 -0
  6. data/CHANGELOG.md +10 -0
  7. data/README.md +6 -0
  8. data/app/views/ductwork/pipelines/show.html.erb +1 -1
  9. data/lib/ductwork/context.rb +1 -0
  10. data/lib/ductwork/job_claim.rb +31 -0
  11. data/lib/ductwork/migration.rb +7 -0
  12. data/lib/ductwork/migration_helper.rb +42 -0
  13. data/lib/ductwork/models/job.rb +7 -57
  14. data/lib/ductwork/models/pipeline.rb +72 -6
  15. data/lib/ductwork/models/record.rb +8 -0
  16. data/lib/ductwork/optimistic_locking_job_claim.rb +88 -0
  17. data/lib/ductwork/processes/pipeline_advancer_runner.rb +1 -1
  18. data/lib/ductwork/row_locking_job_claim.rb +75 -0
  19. data/lib/ductwork/version.rb +1 -1
  20. data/lib/generators/ductwork/install/templates/db/create_ductwork_availabilities.rb +13 -5
  21. data/lib/generators/ductwork/install/templates/db/create_ductwork_executions.rb +9 -3
  22. data/lib/generators/ductwork/install/templates/db/create_ductwork_jobs.rb +9 -3
  23. data/lib/generators/ductwork/install/templates/db/create_ductwork_pipelines.rb +2 -2
  24. data/lib/generators/ductwork/install/templates/db/create_ductwork_processes.rb +2 -2
  25. data/lib/generators/ductwork/install/templates/db/create_ductwork_results.rb +9 -3
  26. data/lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb +9 -3
  27. data/lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb +9 -3
  28. data/lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb +9 -3
  29. data/lib/generators/ductwork/update/USAGE +9 -0
  30. data/lib/generators/ductwork/update/templates/db/denormalize_pipeline_klass_on_availabilities.rb +20 -0
  31. data/lib/generators/ductwork/update/templates/db/migrate_tables_to_uuid_primary_key.rb +118 -0
  32. data/lib/generators/ductwork/update/update_generator.rb +24 -0
  33. metadata +17 -4
  34. /data/{COMM-LICENSE.txt → PRO-LICENSE.txt} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66eeccc14571dbd9d0970843c38da7601f090a9fdcc3b1bbd3b0cad083d37197
4
- data.tar.gz: f277974af71bcbbbd33f1ad1398e79acac2f88c2c0afbbf0143b019e16e65d48
3
+ metadata.gz: 2029d11f63583fd4b5cb774bf3c249b30f933700852ea286c56ae9f6262aa584
4
+ data.tar.gz: 5c32000bd3409f54af3fc43aac6296e7fad458d8ae74336b4edb84310b22bf2a
5
5
  SHA512:
6
- metadata.gz: 9ebc366c05370b908eb35789aa096733cd2171023843b2472602b307182d063a22980559ffed3278ef2ce5a6a98ad41a4d2d9221d216ee4fc3259fedf0319e99
7
- data.tar.gz: 48211addcab05625ce589767370df9c28c7e1a4c44d2d69a71c4ec20906d371844af92ff4c5c937a5300fc490a6cca758fce6896bbc7e80b359d3a5105ec84bc
6
+ metadata.gz: 543e4ab981017a51580b7b3a2a750bbd52a1de313ec27556d1cbb72045b2f6a8fbe743e15585cc56e2682914f0040c91b31a1328be86097f1c08b0dadfcdc73a
7
+ data.tar.gz: 88da2d0dfa602e6850aacbe4fa20a256606eff5084b4d13826a0b715bf46a6a611dc39b4afa13cda35160a66bbf8226b45fc2e618a06178b023632ff98c6c35a
@@ -0,0 +1,22 @@
1
+ FROM ruby:4.0.1-slim
2
+
3
+ RUN apt-get update -qq && \
4
+ apt-get install --no-install-recommends -y \
5
+ build-essential \
6
+ git \
7
+ libsqlite3-dev \
8
+ libyaml-dev \
9
+ sqlite3 \
10
+ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \
11
+ && echo "saturnci-test" > /etc/machine-id
12
+
13
+ WORKDIR /code
14
+
15
+ COPY Gemfile Gemfile.lock ductwork.gemspec ./
16
+ COPY lib/ductwork/version.rb ./lib/ductwork/
17
+
18
+ RUN bundle install
19
+
20
+ COPY . .
21
+
22
+ CMD ["bundle", "exec", "rspec"]
@@ -0,0 +1,5 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/test.sqlite3
4
+ pool: 5
5
+ timeout: 5000
@@ -0,0 +1,10 @@
1
+ services:
2
+ saturn_test_app:
3
+ build:
4
+ context: ..
5
+ dockerfile: .saturnci/Dockerfile
6
+ environment:
7
+ - RAILS_ENV=test
8
+ volumes:
9
+ - ..:/code
10
+ working_dir: /code
data/.saturnci/pre.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ bundle install
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Ductwork Changelog
2
2
 
3
+ ## [0.21.0]
4
+
5
+ - feat: enqueue jobs in batches when expanding - this marks a major performance improvement
6
+ - fix: use node's defined klass when looking up max expansion depth
7
+ - feat!: change all tables' primary key type to UUID v7 - BREAKING CHANGE: be sure to run `bin/rails g ductwork:update` to get the database migrations required to migrate existing data
8
+ - chore!: drop support for ruby 3.2 - BREAKING CHANGE: we need UUID v7 from `SecureRandom` to use as primary keys for our tables; it is only available on Ruby 3.3+ (support ends in a ~month so this was going to happen anyway)
9
+ - feat: use row locking job claiming when the database supports it
10
+ - feat: denormalize pipeline klass column on `ductwork_availabilities` - be sure to run `bin/rails g ductwork:update` to get the latest migration
11
+ - fix: use pipeline class name in log message
12
+
3
13
  ## [0.20.2]
4
14
 
5
15
  - fix: complete pipeline when there are no steps to expand to
data/README.md CHANGED
@@ -25,6 +25,12 @@ Run the Rails generator to create the binstub, configuration file, and migration
25
25
  bin/rails generate ductwork:install
26
26
  ```
27
27
 
28
+ **NOTE**: run the update generator if you've already installed ductwork to get updates:
29
+
30
+ ```bash
31
+ bin/rails generate ductwork:update
32
+ ```
33
+
28
34
  Run migrations and you're ready to start building pipelines!
29
35
 
30
36
  ## Configuration
@@ -122,7 +122,7 @@
122
122
  <div class="grid">
123
123
  <div>
124
124
  <label>ID</label>
125
- <code>
125
+ <code style="display: block;">
126
126
  <%= step.id %>
127
127
  </code>
128
128
  </div>
@@ -21,6 +21,7 @@ module Ductwork
21
21
 
22
22
  def set(key, value, overwrite: false)
23
23
  attributes = {
24
+ id: SecureRandom.uuid_v7,
24
25
  pipeline_id: pipeline_id,
25
26
  key: key,
26
27
  serialized_value: Ductwork::Tuple.serialize(value),
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ class JobClaim
5
+ def initialize(klass)
6
+ @klass = klass
7
+ @adapter = Ductwork::Record.connection.adapter_name.downcase
8
+ end
9
+
10
+ def latest
11
+ claim = if supports_row_locking?
12
+ RowLockingJobClaim
13
+ else
14
+ OptimisticLockingJobClaim
15
+ end
16
+
17
+ claim.new(klass).latest
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :klass, :adapter
23
+
24
+ def supports_row_locking?
25
+ adapter.match?(/postgresql/i) ||
26
+ adapter.match?(/mysql2/i) ||
27
+ adapter.match?(/trilogy/i) ||
28
+ adapter.match?(/oracle_enhanced/i)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ class Migration < ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}".to_f]
5
+ include Ductwork::MigrationHelper
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ module MigrationHelper
5
+ def create_ductwork_table(table_name, &block)
6
+ if postgresql?
7
+ create_table table_name, id: :uuid, &block
8
+ else
9
+ create_table table_name, id: false do |table|
10
+ table.string :id, limit: 36, null: false, primary_key: true
11
+ block.call(table)
12
+ end
13
+ end
14
+ end
15
+
16
+ def belongs_to(table_object, association_name, **options)
17
+ full_options = if postgresql?
18
+ { type: uuid_column_type }.merge(options)
19
+ else
20
+ { type: uuid_column_type, limit: 36 }.merge(options)
21
+ end
22
+
23
+ table_object.belongs_to association_name, **full_options
24
+ end
25
+
26
+ def uuid_column_type
27
+ if postgresql?
28
+ :uuid
29
+ else
30
+ :string
31
+ end
32
+ end
33
+
34
+ def postgresql?
35
+ connection.adapter_name.match?(/postgresql/i)
36
+ end
37
+
38
+ def mysql?
39
+ connection.adapter_name.match?(/mysql/i)
40
+ end
41
+ end
42
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ductwork
4
- class Job < Ductwork::Record # rubocop:todo Metrics/ClassLength
4
+ class Job < Ductwork::Record
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
 
@@ -11,60 +11,8 @@ module Ductwork
11
11
 
12
12
  FAILED_EXECUTION_TIMEOUT = 10.seconds
13
13
 
14
- def self.claim_latest(klass) # rubocop:todo Metrics
15
- process_id = ::Process.pid
16
- id = Ductwork::Availability
17
- .joins(execution: { job: { step: :pipeline } })
18
- .where("ductwork_availabilities.started_at <= ?", Time.current)
19
- .where(completed_at: nil)
20
- .where(ductwork_pipelines: { klass: })
21
- .order(:created_at)
22
- .limit(1)
23
- .pluck(:id)
24
- .first
25
-
26
- if id.present?
27
- # TODO: probably makes sense to use SQL here instead of relying
28
- # on ActiveRecord to construct the correct `UPDATE` query
29
- rows_updated = nil
30
- Ductwork::Record.transaction do
31
- rows_updated = Ductwork::Availability
32
- .where(id: id, completed_at: nil)
33
- .update_all(completed_at: Time.current, process_id: process_id)
34
- Ductwork::Execution
35
- .joins(:availability)
36
- .where(completed_at: nil)
37
- .where(ductwork_availabilities: { id: })
38
- .update_all(process_id:)
39
- end
40
-
41
- if rows_updated == 1
42
- Ductwork.logger.debug(
43
- msg: "Job claimed",
44
- role: :job_worker,
45
- process_id: process_id,
46
- availability_id: id
47
- )
48
- job = Ductwork::Job
49
- .joins(executions: :availability)
50
- .find_by(ductwork_availabilities: { id:, process_id: })
51
-
52
- Ductwork::Record.transaction do
53
- job.step.in_progress!
54
- job.step.pipeline.in_progress!
55
- end
56
-
57
- job
58
- else
59
- Ductwork.logger.debug(
60
- msg: "Did not claim job, avoided race condition",
61
- role: :job_worker,
62
- process_id: process_id,
63
- availability_id: id
64
- )
65
- nil
66
- end
67
- end
14
+ def self.claim_latest(klass)
15
+ Ductwork::JobClaim.new(klass).latest
68
16
  end
69
17
 
70
18
  def self.enqueue(step, *args)
@@ -78,7 +26,8 @@ module Ductwork
78
26
  retry_count: 0
79
27
  )
80
28
  execution.create_availability!(
81
- started_at: Time.current
29
+ started_at: Time.current,
30
+ pipeline_klass: step.pipeline.klass
82
31
  )
83
32
 
84
33
  Ductwork.logger.info(
@@ -168,7 +117,8 @@ module Ductwork
168
117
  started_at: FAILED_EXECUTION_TIMEOUT.from_now
169
118
  )
170
119
  new_execution.create_availability!(
171
- started_at: FAILED_EXECUTION_TIMEOUT.from_now
120
+ started_at: FAILED_EXECUTION_TIMEOUT.from_now,
121
+ pipeline_klass: pipeline.klass
172
122
  )
173
123
  elsif execution.retry_count >= max_retry
174
124
  halted = true
@@ -259,7 +259,7 @@ module Ductwork
259
259
  end
260
260
 
261
261
  def expand_to_next_steps(step_id, edge)
262
- next_klass = edge[:to].sole
262
+ next_klass = parsed_definition.dig(:edges, edge[:to].sole, :klass)
263
263
  return_value = Ductwork::Job
264
264
  .find_by(step_id:)
265
265
  .return_value
@@ -270,12 +270,78 @@ module Ductwork
270
270
  elsif return_value.none?
271
271
  complete!
272
272
  else
273
- # TODO: Brainstorm on using `insert_all` instead of iterating.
274
- # Performance is bad when the return value has a lot of elements
275
- # and we create a step and job individually
276
- Array(return_value).each do |input_arg|
277
- create_step_and_enqueue_job(edge:, input_arg:)
273
+ bulk_create_steps_and_jobs(edge:, return_value:)
274
+ end
275
+ end
276
+
277
+ def bulk_create_steps_and_jobs(edge:, return_value:) # rubocop:todo Metrics
278
+ # NOTE: "chain" is used by ActiveRecord so we have to call
279
+ # this enum value "default" :sad:
280
+ to_transition = edge[:type] == "chain" ? "default" : edge[:type]
281
+ node ||= edge[:to].sole
282
+ step_klass = parsed_definition.dig(:edges, node, :klass)
283
+ now = Time.current
284
+
285
+ Array(return_value).each_slice(1_000).each do |batch| # rubocop:todo Metrics/BlockLength
286
+ step_rows = []
287
+ job_rows = []
288
+ execution_rows = []
289
+ availability_rows = []
290
+
291
+ batch.each do |value| # rubocop:todo Metrics/BlockLength
292
+ step_id = SecureRandom.uuid_v7
293
+ job_id = SecureRandom.uuid_v7
294
+ execution_id = SecureRandom.uuid_v7
295
+ availability_id = SecureRandom.uuid_v7
296
+
297
+ step_rows << {
298
+ id: step_id,
299
+ pipeline_id: id,
300
+ node: node,
301
+ klass: step_klass,
302
+ status: "in_progress",
303
+ to_transition: to_transition,
304
+ started_at: now,
305
+ created_at: now,
306
+ updated_at: now,
307
+ }
308
+ job_rows << {
309
+ id: job_id,
310
+ step_id: step_id,
311
+ input_args: JSON.dump({ args: [value] }),
312
+ klass: step_klass,
313
+ started_at: now,
314
+ created_at: now,
315
+ updated_at: now,
316
+ }
317
+ execution_rows << {
318
+ id: execution_id,
319
+ job_id: job_id,
320
+ retry_count: 0,
321
+ started_at: now,
322
+ created_at: now,
323
+ updated_at: now,
324
+ }
325
+ availability_rows << {
326
+ id: availability_id,
327
+ execution_id: execution_id,
328
+ pipeline_klass: klass,
329
+ started_at: now,
330
+ created_at: now,
331
+ updated_at: now,
332
+ }
278
333
  end
334
+
335
+ Ductwork::Step.insert_all!(step_rows)
336
+ Ductwork::Job.insert_all!(job_rows)
337
+ Ductwork::Execution.insert_all!(execution_rows)
338
+ Ductwork::Availability.insert_all!(availability_rows)
339
+
340
+ Ductwork.logger.info(
341
+ msg: "Job batch enqueued",
342
+ count: batch.count,
343
+ job_klass: step_klass
344
+ )
279
345
  end
280
346
  end
281
347
 
@@ -8,8 +8,16 @@ module Ductwork
8
8
  connects_to(database: { writing: Ductwork.configuration.database.to_sym })
9
9
  end
10
10
 
11
+ before_create :generate_uuid_v7
12
+
11
13
  def self.table_name_prefix
12
14
  "ductwork_"
13
15
  end
16
+
17
+ private
18
+
19
+ def generate_uuid_v7
20
+ self.id ||= SecureRandom.uuid_v7
21
+ end
14
22
  end
15
23
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ class OptimisticLockingJobClaim
5
+ def initialize(klass)
6
+ @id = nil
7
+ @job = nil
8
+ @klass = klass
9
+ @process_id = ::Process.pid
10
+ end
11
+
12
+ def latest
13
+ @id = latest_availability_id
14
+
15
+ if id.present?
16
+ rows_updated = claim_availability
17
+
18
+ if rows_updated == 1
19
+ Ductwork.logger.debug(
20
+ msg: "Job claimed",
21
+ role: :job_worker,
22
+ process_id: process_id,
23
+ availability_id: id
24
+ )
25
+
26
+ @job = find_job
27
+
28
+ update_state
29
+ else
30
+ Ductwork.logger.debug(
31
+ msg: "Did not claim job, avoided race condition",
32
+ role: :job_worker,
33
+ process_id: process_id,
34
+ availability_id: id
35
+ )
36
+ end
37
+ else
38
+ Ductwork.logger.debug(
39
+ msg: "No available job to claim",
40
+ role: :job_worker,
41
+ process_id: process_id,
42
+ pipeline: klass
43
+ )
44
+ end
45
+
46
+ job
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :id, :job, :klass, :process_id
52
+
53
+ def latest_availability_id
54
+ Ductwork::Availability
55
+ .where("ductwork_availabilities.started_at <= ?", Time.current)
56
+ .where(completed_at: nil, pipeline_klass: klass)
57
+ .order(:started_at)
58
+ .limit(1)
59
+ .pluck(:id)
60
+ .first
61
+ end
62
+
63
+ def claim_availability
64
+ Ductwork::Availability
65
+ .where(id: id, completed_at: nil)
66
+ .update_all(completed_at: Time.current, process_id: process_id)
67
+ end
68
+
69
+ def find_job
70
+ Ductwork::Job
71
+ .joins(executions: :availability)
72
+ .find_by!(ductwork_availabilities: { id:, process_id: })
73
+ end
74
+
75
+ def update_state
76
+ Ductwork::Record.transaction do
77
+ Ductwork::Execution
78
+ .joins(:availability)
79
+ .where(completed_at: nil)
80
+ .where(ductwork_availabilities: { id: })
81
+ .sole
82
+ .update!(process_id:)
83
+ job.step.in_progress!
84
+ job.step.pipeline.in_progress!
85
+ end
86
+ end
87
+ end
88
+ end
@@ -75,7 +75,7 @@ module Ductwork
75
75
  Ductwork.logger.warn(
76
76
  msg: "Restarted pipeline advancer",
77
77
  role: :pipeline_advancer_runner,
78
- pipeline: advancer.pipeline,
78
+ pipeline: advancer.pipeline.class.to_s,
79
79
  thread: advancer.name
80
80
  )
81
81
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ductwork
4
+ class RowLockingJobClaim
5
+ def initialize(klass)
6
+ @id = nil
7
+ @job = nil
8
+ @klass = klass
9
+ @process_id = ::Process.pid
10
+ end
11
+
12
+ def latest
13
+ rows_updated = attempt_availability_claim
14
+
15
+ if rows_updated == 1
16
+ Ductwork.logger.debug(
17
+ msg: "Job claimed",
18
+ role: :job_worker,
19
+ process_id: process_id,
20
+ availability_id: id
21
+ )
22
+ update_state
23
+ else
24
+ Ductwork.logger.debug(
25
+ msg: "No available job to claim",
26
+ role: :job_worker,
27
+ process_id: process_id,
28
+ pipeline: klass
29
+ )
30
+ end
31
+
32
+ job
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :id, :job, :klass, :process_id
38
+
39
+ def attempt_availability_claim
40
+ Ductwork::Record.transaction do
41
+ @id = Ductwork::Availability
42
+ .where("ductwork_availabilities.started_at <= ?", Time.current)
43
+ .where(completed_at: nil, pipeline_klass: klass)
44
+ .order(:started_at)
45
+ .lock("FOR UPDATE SKIP LOCKED")
46
+ .limit(1)
47
+ .ids
48
+ .first
49
+
50
+ if id.present?
51
+ Ductwork::Availability
52
+ .where(id: id, completed_at: nil)
53
+ .update_all(completed_at: Time.current, process_id: process_id)
54
+ else
55
+ 0
56
+ end
57
+ end
58
+ end
59
+
60
+ def update_state
61
+ Ductwork::Record.transaction do
62
+ execution = Ductwork::Execution
63
+ .joins(:availability)
64
+ .where(completed_at: nil)
65
+ .where(ductwork_availabilities: { id: })
66
+ .sole
67
+ @job = execution.job
68
+
69
+ execution.update!(process_id:)
70
+ job.step.in_progress!
71
+ job.step.pipeline.in_progress!
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ductwork
4
- VERSION = "0.20.2"
4
+ VERSION = "0.21.0"
5
5
  end
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkAvailabilities < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkAvailabilities < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_availabilities do |table|
6
- table.belongs_to :execution, index: false, null: false, foreign_key: { to_table: :ductwork_executions }
5
+ create_ductwork_table :ductwork_availabilities do |table|
6
+ belongs_to(
7
+ table,
8
+ :execution,
9
+ index: false,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_executions }
12
+ )
7
13
  table.timestamp :started_at, null: false
8
14
  table.timestamp :completed_at
9
15
  table.integer :process_id
16
+ table.string :pipeline_klass, null: false
10
17
  table.timestamps null: false
11
18
  end
12
19
 
13
20
  add_index :ductwork_availabilities, :execution_id, unique: true
14
21
  add_index :ductwork_availabilities, %i[id process_id]
15
22
  add_index :ductwork_availabilities,
16
- %i[completed_at started_at created_at],
17
- name: "index_ductwork_availabilities_on_claim_latest"
23
+ %i[pipeline_klass started_at],
24
+ name: "index_ductwork_availabilities_on_claim_latest",
25
+ where: "completed_at IS NULL"
18
26
  end
19
27
  end
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkExecutions < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkExecutions < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_executions do |table|
6
- table.belongs_to :job, index: true, null: false, foreign_key: { to_table: :ductwork_jobs }
5
+ create_ductwork_table :ductwork_executions do |table|
6
+ belongs_to(
7
+ table,
8
+ :job,
9
+ index: true,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_jobs }
12
+ )
7
13
  table.timestamp :started_at, null: false
8
14
  table.timestamp :completed_at
9
15
  table.integer :retry_count, null: false
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkJobs < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkJobs < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_jobs do |table|
6
- table.belongs_to :step, index: false, null: false, foreign_key: { to_table: :ductwork_steps }
5
+ create_ductwork_table :ductwork_jobs do |table|
6
+ belongs_to(
7
+ table,
8
+ :step,
9
+ index: false,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_steps }
12
+ )
7
13
  table.string :klass, null: false
8
14
  table.timestamp :started_at, null: false
9
15
  table.timestamp :completed_at
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkPipelines < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkPipelines < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_pipelines do |table|
5
+ create_ductwork_table :ductwork_pipelines do |table|
6
6
  table.string :klass, null: false
7
7
  table.text :definition, null: false
8
8
  table.string :definition_sha1, null: false
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkProcesses < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkProcesses < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_processes do |table|
5
+ create_ductwork_table :ductwork_processes do |table|
6
6
  table.integer :pid, null: false
7
7
  table.string :machine_identifier, null: false
8
8
  table.timestamp :last_heartbeat_at, null: false
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkResults < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkResults < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_results do |table|
6
- table.belongs_to :execution, index: false, null: false, foreign_key: { to_table: :ductwork_executions }
5
+ create_ductwork_table :ductwork_results do |table|
6
+ belongs_to(
7
+ table,
8
+ :execution,
9
+ index: false,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_executions }
12
+ )
7
13
  table.string :result_type, null: false
8
14
  table.string :error_klass
9
15
  table.string :error_message
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkRuns < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkRuns < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_runs do |table|
6
- table.belongs_to :execution, index: false, null: false, foreign_key: { to_table: :ductwork_executions }
5
+ create_ductwork_table :ductwork_runs do |table|
6
+ belongs_to(
7
+ table,
8
+ :execution,
9
+ index: false,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_executions }
12
+ )
7
13
  table.timestamp :started_at, null: false
8
14
  table.timestamp :completed_at
9
15
  table.timestamps null: false
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkSteps < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkSteps < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_steps do |table|
6
- table.belongs_to :pipeline, index: true, null: false, foreign_key: { to_table: :ductwork_pipelines }
5
+ create_ductwork_table :ductwork_steps do |table|
6
+ belongs_to(
7
+ table,
8
+ :pipeline,
9
+ index: true,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_pipelines }
12
+ )
7
13
  table.string :node, null: false
8
14
  table.string :klass, null: false
9
15
  table.string :to_transition, null: false
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateDuctworkTuples < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
+ class CreateDuctworkTuples < Ductwork::Migration
4
4
  def change
5
- create_table :ductwork_tuples do |table|
6
- table.belongs_to :pipeline, index: true, null: false, foreign_key: { to_table: :ductwork_pipelines }
5
+ create_ductwork_table :ductwork_tuples do |table|
6
+ belongs_to(
7
+ table,
8
+ :pipeline,
9
+ index: true,
10
+ null: false,
11
+ foreign_key: { to_table: :ductwork_pipelines }
12
+ )
7
13
  table.string :key, null: false
8
14
  table.string :serialized_value
9
15
  table.datetime :first_set_at, null: false
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Updates ductwork with necessary files and changes
3
+
4
+ Example:
5
+ bin/rails generate ductwork:update
6
+
7
+ This will perform the following:
8
+ Adds migrations
9
+
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DenormalizePipelineKlassOnAvailabilities < Ductwork::Migration
4
+ def change
5
+ add_column :ductwork_availabilities, :pipeline_klass, :string
6
+
7
+ # NOTE: change this how you see fit. everything is updated to a static,
8
+ # bogus value in case there are a lot of records.
9
+ Ductwork::Availability
10
+ .where(pipeline_klass: nil)
11
+ .update_all(pipeline_klass: "Pipeline")
12
+
13
+ change_column_null :ductwork_availabilities, :pipeline_klass, false
14
+ remove_index :ductwork_availabilities, name: "index_ductwork_availabilities_on_claim_latest"
15
+ add_index :ductwork_availabilities,
16
+ %i[pipeline_klass started_at],
17
+ name: "index_ductwork_availabilities_on_claim_latest",
18
+ where: "completed_at IS NULL"
19
+ end
20
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MigrateTablesToUuidPrimaryKey < Ductwork::Migration
4
+ TABLE_TO_FOREIGN_KEYS = {
5
+ ductwork_pipelines: [],
6
+ ductwork_steps: [:pipeline],
7
+ ductwork_jobs: [:step],
8
+ ductwork_executions: [:job],
9
+ ductwork_availabilities: [:execution],
10
+ ductwork_runs: [:execution],
11
+ ductwork_results: [:execution],
12
+ ductwork_tuples: [:pipeline],
13
+ ductwork_processes: [],
14
+ }.freeze
15
+ FOREIGN_KEY_TO_TABLES = {
16
+ pipeline: %i[ductwork_steps ductwork_tuples],
17
+ step: %i[ductwork_jobs],
18
+ job: %i[ductwork_executions],
19
+ execution: %i[ductwork_availabilities ductwork_runs ductwork_results],
20
+ availabilitie: [],
21
+ run: [],
22
+ result: [],
23
+ tuple: [],
24
+ processe: [],
25
+ }.freeze
26
+
27
+ def up
28
+ create_primary_key_uuid_columns
29
+ create_foreign_key_uuid_columns
30
+ backfill_uuid_columns
31
+ drop_old_foreign_keys
32
+ swap_primary_keys
33
+ swap_foreign_keys
34
+ end
35
+
36
+ def down
37
+ raise ActiveRecord::IrreversibleMigration,
38
+ "Cannot revert UUID migration — old integer IDs have been dropped. Restore from backup."
39
+ end
40
+
41
+ private
42
+
43
+ def create_primary_key_uuid_columns
44
+ TABLE_TO_FOREIGN_KEYS.each_key do |table|
45
+ add_column table, :uuid, uuid_column_type
46
+ end
47
+ end
48
+
49
+ def create_foreign_key_uuid_columns
50
+ TABLE_TO_FOREIGN_KEYS.each do |table, column_prefixes|
51
+ column_prefixes.each do |column_prefix|
52
+ add_column table, "#{column_prefix}_uuid", uuid_column_type
53
+ end
54
+ end
55
+ end
56
+
57
+ def backfill_uuid_columns
58
+ FOREIGN_KEY_TO_TABLES.each do |prefix, tables|
59
+ select_all("SELECT id from ductwork_#{prefix}s WHERE uuid IS NULL").each do |row|
60
+ uuid = connection.quote(SecureRandom.uuid_v7)
61
+ id = row["id"]
62
+
63
+ execute("UPDATE ductwork_#{prefix}s SET uuid = #{uuid} WHERE id = #{id}")
64
+ tables.each do |table|
65
+ execute("UPDATE #{table} SET #{prefix}_uuid = #{uuid} WHERE #{prefix}_id = #{id}")
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def drop_old_foreign_keys
72
+ FOREIGN_KEY_TO_TABLES.each do |prefix, tables|
73
+ tables.each do |table|
74
+ remove_foreign_key table, "ductwork_#{prefix}s", column: "#{prefix}_id", if_exists: true
75
+ end
76
+ end
77
+ end
78
+
79
+ def swap_primary_keys # rubocop:disable Metrics/PerceivedComplexity
80
+ TABLE_TO_FOREIGN_KEYS.each_key do |table|
81
+ if postgresql?
82
+ execute "ALTER TABLE #{table} DROP CONSTRAINT IF EXISTS #{table}_pkey"
83
+ elsif mysql?
84
+ execute "ALTER TABLE #{table} MODIFY id BIGINT NOT NULL"
85
+ execute "ALTER TABLE #{table} DROP PRIMARY KEY"
86
+ end
87
+
88
+ remove_column table, :id
89
+ rename_column table, :uuid, :id
90
+
91
+ if postgresql?
92
+ execute "ALTER TABLE #{table} ADD PRIMARY KEY (id)"
93
+ elsif mysql?
94
+ execute "ALTER TABLE #{table} MODIFY id VARCHAR(36) NOT NULL"
95
+ execute "ALTER TABLE #{table} ADD PRIMARY KEY (id)"
96
+ elsif !index_exists?(table, :id)
97
+ add_index table, :id, unique: true
98
+ end
99
+
100
+ change_column_null table, :id, false
101
+ end
102
+ end
103
+
104
+ def swap_foreign_keys
105
+ FOREIGN_KEY_TO_TABLES.each do |prefix, tables|
106
+ tables.each do |table|
107
+ remove_index table, "#{prefix}_id", if_exists: true
108
+ remove_column table, "#{prefix}_id"
109
+ rename_column table, "#{prefix}_uuid", "#{prefix}_id"
110
+ if !index_exists?(table, "#{prefix}_id")
111
+ add_index table, "#{prefix}_id"
112
+ end
113
+ change_column_null table, "#{prefix}_id", false
114
+ add_foreign_key table, "ductwork_#{prefix}s", column: "#{prefix}_id"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/migration"
4
+ require "rails/generators/active_record/migration"
5
+
6
+ module Ductwork
7
+ class UpdateGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def create_files
13
+ if Ductwork::Availability.column_names.exclude?("pipeline_klass")
14
+ migration_template "db/denormalize_pipeline_klass_on_availabilities.rb",
15
+ "db/migrate/denormalize_pipeline_klass_on_availabilities.rb"
16
+ end
17
+
18
+ if Ductwork::Pipeline.column_for_attribute("id").type != :uuid
19
+ migration_template "db/migrate_tables_to_uuid_primary_key.rb",
20
+ "db/migrate/migrate_tables_to_uuid_primary_key.rb"
21
+ end
22
+ end
23
+ end
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ductwork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.2
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Ewing
@@ -111,10 +111,14 @@ executables: []
111
111
  extensions: []
112
112
  extra_rdoc_files: []
113
113
  files:
114
+ - ".saturnci/Dockerfile"
115
+ - ".saturnci/database.yml"
116
+ - ".saturnci/docker-compose.yml"
117
+ - ".saturnci/pre.sh"
114
118
  - CHANGELOG-PRO.md
115
119
  - CHANGELOG.md
116
- - COMM-LICENSE.txt
117
120
  - LICENSE.txt
121
+ - PRO-LICENSE.txt
118
122
  - README.md
119
123
  - Rakefile
120
124
  - app/assets/images/ductwork/logo.png
@@ -141,7 +145,10 @@ files:
141
145
  - lib/ductwork/dsl/branch_builder.rb
142
146
  - lib/ductwork/dsl/definition_builder.rb
143
147
  - lib/ductwork/engine.rb
148
+ - lib/ductwork/job_claim.rb
144
149
  - lib/ductwork/machine_identifier.rb
150
+ - lib/ductwork/migration.rb
151
+ - lib/ductwork/migration_helper.rb
145
152
  - lib/ductwork/models/availability.rb
146
153
  - lib/ductwork/models/execution.rb
147
154
  - lib/ductwork/models/job.rb
@@ -152,6 +159,7 @@ files:
152
159
  - lib/ductwork/models/run.rb
153
160
  - lib/ductwork/models/step.rb
154
161
  - lib/ductwork/models/tuple.rb
162
+ - lib/ductwork/optimistic_locking_job_claim.rb
155
163
  - lib/ductwork/processes/job_worker.rb
156
164
  - lib/ductwork/processes/job_worker_runner.rb
157
165
  - lib/ductwork/processes/launcher.rb
@@ -161,6 +169,7 @@ files:
161
169
  - lib/ductwork/processes/process_supervisor_runner.rb
162
170
  - lib/ductwork/processes/thread_supervisor.rb
163
171
  - lib/ductwork/processes/thread_supervisor_runner.rb
172
+ - lib/ductwork/row_locking_job_claim.rb
164
173
  - lib/ductwork/running_context.rb
165
174
  - lib/ductwork/testing.rb
166
175
  - lib/ductwork/testing/helpers.rb
@@ -180,6 +189,10 @@ files:
180
189
  - lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb
181
190
  - lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb
182
191
  - lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb
192
+ - lib/generators/ductwork/update/USAGE
193
+ - lib/generators/ductwork/update/templates/db/denormalize_pipeline_klass_on_availabilities.rb
194
+ - lib/generators/ductwork/update/templates/db/migrate_tables_to_uuid_primary_key.rb
195
+ - lib/generators/ductwork/update/update_generator.rb
183
196
  homepage: https://github.com/ductwork/ductwork
184
197
  licenses:
185
198
  - LGPL-3.0-or-later
@@ -197,14 +210,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
197
210
  requirements:
198
211
  - - ">="
199
212
  - !ruby/object:Gem::Version
200
- version: 3.2.0
213
+ version: 3.3.0
201
214
  required_rubygems_version: !ruby/object:Gem::Requirement
202
215
  requirements:
203
216
  - - ">="
204
217
  - !ruby/object:Gem::Version
205
218
  version: '0'
206
219
  requirements: []
207
- rubygems_version: 4.0.3
220
+ rubygems_version: 4.0.6
208
221
  specification_version: 4
209
222
  summary: A Ruby pipeline framework
210
223
  test_files: []
File without changes