online_migrations 0.26.0 → 0.27.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/docs/0.27-upgrade.md +24 -0
  4. data/docs/background_data_migrations.md +200 -101
  5. data/docs/background_schema_migrations.md +2 -2
  6. data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
  7. data/lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt +34 -0
  8. data/lib/generators/online_migrations/templates/{background_data_migration.rb.tt → data_migration.rb.tt} +8 -9
  9. data/lib/generators/online_migrations/templates/initializer.rb.tt +19 -25
  10. data/lib/generators/online_migrations/templates/install_migration.rb.tt +9 -40
  11. data/lib/generators/online_migrations/upgrade_generator.rb +16 -8
  12. data/lib/online_migrations/active_record_batch_enumerator.rb +8 -0
  13. data/lib/online_migrations/background_data_migrations/backfill_column.rb +50 -0
  14. data/lib/online_migrations/background_data_migrations/config.rb +62 -0
  15. data/lib/online_migrations/{background_migrations → background_data_migrations}/copy_column.rb +15 -28
  16. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_associated_records.rb +9 -5
  17. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_orphaned_records.rb +5 -9
  18. data/lib/online_migrations/background_data_migrations/migration.rb +312 -0
  19. data/lib/online_migrations/{background_migrations → background_data_migrations}/migration_helpers.rb +72 -61
  20. data/lib/online_migrations/background_data_migrations/migration_job.rb +158 -0
  21. data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +65 -0
  22. data/lib/online_migrations/{background_migrations → background_data_migrations}/perform_action_on_relation.rb +5 -5
  23. data/lib/online_migrations/{background_migrations → background_data_migrations}/reset_counters.rb +5 -5
  24. data/lib/online_migrations/background_data_migrations/scheduler.rb +78 -0
  25. data/lib/online_migrations/background_data_migrations/ticker.rb +62 -0
  26. data/lib/online_migrations/background_schema_migrations/config.rb +2 -2
  27. data/lib/online_migrations/background_schema_migrations/migration.rb +51 -123
  28. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +25 -46
  29. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +43 -97
  30. data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
  31. data/lib/online_migrations/change_column_type_helpers.rb +3 -2
  32. data/lib/online_migrations/config.rb +4 -4
  33. data/lib/online_migrations/data_migration.rb +127 -0
  34. data/lib/online_migrations/lock_retrier.rb +5 -2
  35. data/lib/online_migrations/schema_statements.rb +1 -1
  36. data/lib/online_migrations/shard_aware.rb +44 -0
  37. data/lib/online_migrations/version.rb +1 -1
  38. data/lib/online_migrations.rb +18 -11
  39. metadata +22 -21
  40. data/lib/online_migrations/background_migration.rb +0 -64
  41. data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
  42. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
  43. data/lib/online_migrations/background_migrations/config.rb +0 -74
  44. data/lib/online_migrations/background_migrations/migration.rb +0 -329
  45. data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
  46. data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
  47. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
  48. data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
  49. data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
  50. data/lib/online_migrations/background_migrations/scheduler.rb +0 -42
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- class MigrationJob < ApplicationRecord
6
- STATUSES = [
7
- :enqueued,
8
- :running,
9
- :errored,
10
- :failed,
11
- :succeeded,
12
- :cancelled,
13
- ]
14
-
15
- self.table_name = :background_migration_jobs
16
-
17
- scope :active, -> { where(status: [:enqueued, :running, :errored]) }
18
- scope :completed, -> { where(status: [:failed, :succeeded]) }
19
- scope :stuck, -> do
20
- timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
21
- running.where("updated_at <= ?", timeout.seconds.ago)
22
- end
23
-
24
- scope :retriable, -> do
25
- stuck_sql = connection.unprepared_statement { stuck.to_sql }
26
-
27
- from(Arel.sql(<<~SQL))
28
- (
29
- (SELECT * FROM background_migration_jobs WHERE status = 'errored')
30
- UNION
31
- (#{stuck_sql})
32
- ) AS #{table_name}
33
- SQL
34
- end
35
-
36
- scope :except_succeeded, -> { where.not(status: :succeeded) }
37
-
38
- enum :status, STATUSES.index_with(&:to_s)
39
-
40
- delegate :migration_name, :migration_class, :migration_object, :migration_relation, :batch_column_name,
41
- :arguments, :batch_pause, to: :migration
42
-
43
- belongs_to :migration, inverse_of: :migration_jobs
44
-
45
- validates :min_value, :max_value, presence: true, numericality: { greater_than: 0 }
46
- validate :values_in_migration_range, if: :min_value?
47
- validate :validate_values_order, if: :min_value?
48
-
49
- validates_with MigrationJobStatusValidator, on: :update
50
-
51
- before_create :copy_settings_from_migration
52
-
53
- # Whether the job is considered stuck (is running for some configured time).
54
- #
55
- def stuck?
56
- timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
57
- running? && updated_at <= timeout.seconds.ago
58
- end
59
-
60
- def attempts_exceeded?
61
- attempts >= max_attempts
62
- end
63
-
64
- # Mark this job as ready to be processed again.
65
- #
66
- # This is used when retrying failed jobs.
67
- #
68
- def retry
69
- if failed?
70
- transaction do
71
- update!(
72
- status: self.class.statuses[:enqueued],
73
- attempts: 0,
74
- started_at: nil,
75
- finished_at: nil,
76
- error_class: nil,
77
- error_message: nil,
78
- backtrace: nil
79
- )
80
- migration.enqueued! if migration.failed?
81
- end
82
- true
83
- else
84
- false
85
- end
86
- end
87
-
88
- private
89
- def values_in_migration_range
90
- if min_value < migration.min_value || max_value > migration.max_value
91
- errors.add(:base, "min_value and max_value should be in background migration values range")
92
- end
93
- end
94
-
95
- def validate_values_order
96
- if max_value.to_i < min_value.to_i
97
- errors.add(:base, "max_value should be greater than or equal to min_value")
98
- end
99
- end
100
-
101
- def copy_settings_from_migration
102
- self.batch_size = migration.batch_size
103
- self.sub_batch_size = migration.sub_batch_size
104
- self.pause_ms = migration.sub_batch_pause_ms
105
- self.max_attempts = migration.batch_max_attempts
106
- end
107
- end
108
- end
109
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # @private
6
- class MigrationJobRunner
7
- attr_reader :migration_job
8
-
9
- delegate :migration, :attempts, :migration_relation, :migration_object, :sub_batch_size,
10
- :batch_column_name, :min_value, :max_value, :pause_ms, to: :migration_job
11
-
12
- def initialize(migration_job)
13
- @migration_job = migration_job
14
- end
15
-
16
- def run
17
- job_payload = { background_migration_job: migration_job }
18
- if migration_job.attempts >= 1
19
- ActiveSupport::Notifications.instrument("retried.background_migrations", job_payload)
20
- end
21
-
22
- migration_job.update!(
23
- attempts: attempts + 1,
24
- status: :running,
25
- started_at: Time.current,
26
- finished_at: nil,
27
- error_class: nil,
28
- error_message: nil,
29
- backtrace: nil
30
- )
31
-
32
- ActiveSupport::Notifications.instrument("process_batch.background_migrations", job_payload) do
33
- migration.on_shard { run_batch }
34
- end
35
-
36
- migration_job.update!(status: :succeeded, finished_at: Time.current)
37
- rescue Exception => e # rubocop:disable Lint/RescueException
38
- backtrace_cleaner = ::OnlineMigrations.config.backtrace_cleaner
39
-
40
- status = migration_job.attempts_exceeded? ? :failed : :errored
41
-
42
- migration_job.update!(
43
- status: status,
44
- finished_at: Time.current,
45
- error_class: e.class.name,
46
- error_message: e.message,
47
- backtrace: backtrace_cleaner ? backtrace_cleaner.clean(e.backtrace) : e.backtrace
48
- )
49
-
50
- ::OnlineMigrations.config.background_migrations.error_handler.call(e, migration_job)
51
- raise if Utils.run_background_migrations_inline?
52
- end
53
-
54
- private
55
- def run_batch
56
- iterator = ::OnlineMigrations::BatchIterator.new(migration_relation)
57
-
58
- iterator.each_batch(of: sub_batch_size, column: batch_column_name,
59
- start: min_value, finish: max_value) do |sub_batch|
60
- migration_object.process_batch(sub_batch)
61
- sleep(pause_ms * 0.001) if pause_ms > 0
62
- end
63
- end
64
- end
65
- end
66
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # @private
6
- class MigrationJobStatusValidator < ActiveModel::Validator
7
- VALID_STATUS_TRANSITIONS = {
8
- "enqueued" => ["running", "cancelled"],
9
- "running" => ["succeeded", "errored", "failed", "cancelled"],
10
- "errored" => ["running", "failed", "cancelled"],
11
- "failed" => ["enqueued", "running", "cancelled"],
12
- }
13
-
14
- def validate(record)
15
- return if !record.status_changed?
16
-
17
- previous_status, new_status = record.status_change
18
- valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
19
-
20
- if !valid_new_statuses.include?(new_status)
21
- record.errors.add(
22
- :status,
23
- "cannot transition background migration job from status #{previous_status} to #{new_status}"
24
- )
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # Runs single background migration.
6
- class MigrationRunner
7
- attr_reader :migration
8
-
9
- def initialize(migration)
10
- @migration = migration
11
- end
12
-
13
- # Runs one background migration job.
14
- def run_migration_job
15
- raise "Should not be called on a composite (with sharding) migration" if migration.composite?
16
- return if migration.cancelled? || migration.succeeded?
17
-
18
- mark_as_running if migration.enqueued?
19
- migration_payload = notifications_payload(migration)
20
-
21
- if !migration.migration_jobs.exists?
22
- ActiveSupport::Notifications.instrument("started.background_migrations", migration_payload)
23
- end
24
-
25
- if should_throttle?
26
- ActiveSupport::Notifications.instrument("throttled.background_migrations", migration_payload)
27
- return
28
- end
29
-
30
- next_migration_job = find_or_create_next_migration_job
31
-
32
- if next_migration_job
33
- job_runner = MigrationJobRunner.new(next_migration_job)
34
- job_runner.run
35
- elsif !migration.migration_jobs.active.exists?
36
- if migration.migration_jobs.failed.exists?
37
- migration.update!(status: :failed, finished_at: Time.current)
38
- else
39
- migration.update!(status: :succeeded, finished_at: Time.current)
40
- end
41
-
42
- ActiveSupport::Notifications.instrument("completed.background_migrations", migration_payload)
43
-
44
- complete_parent_if_needed(migration) if migration.parent.present?
45
- end
46
-
47
- next_migration_job
48
- end
49
-
50
- # Runs the background migration until completion.
51
- #
52
- # @note This method should not be used in production environments
53
- #
54
- def run_all_migration_jobs
55
- run_inline = OnlineMigrations.config.run_background_migrations_inline
56
- if run_inline && !run_inline.call
57
- raise "This method is not intended for use in production environments"
58
- end
59
-
60
- return if migration.completed? || migration.cancelled?
61
-
62
- mark_as_running
63
-
64
- if migration.composite?
65
- migration.children.each do |child_migration|
66
- runner = self.class.new(child_migration)
67
- runner.run_all_migration_jobs
68
- end
69
- else
70
- while migration.running?
71
- run_migration_job
72
- end
73
- end
74
- end
75
-
76
- # Finishes the background migration.
77
- #
78
- # Keep running until the migration is finished.
79
- #
80
- def finish
81
- return if migration.completed? || migration.cancelled?
82
-
83
- if migration.composite?
84
- migration.children.each do |child_migration|
85
- runner = self.class.new(child_migration)
86
- runner.finish
87
- end
88
- else
89
- # Mark is as finishing to avoid being picked up
90
- # by the background migrations scheduler.
91
- migration.finishing!
92
- migration.reset_failed_jobs_attempts
93
-
94
- while migration.finishing?
95
- run_migration_job
96
- end
97
- end
98
- end
99
-
100
- private
101
- def mark_as_running
102
- Migration.transaction do
103
- migration.update!(status: :running, started_at: Time.current, finished_at: nil)
104
-
105
- if (parent = migration.parent)
106
- if parent.started_at
107
- parent.update!(status: :running, finished_at: nil)
108
- else
109
- parent.update!(status: :running, started_at: Time.current, finished_at: nil)
110
- end
111
- end
112
- end
113
- end
114
-
115
- def should_throttle?
116
- ::OnlineMigrations.config.throttler.call
117
- end
118
-
119
- def find_or_create_next_migration_job
120
- min_value, max_value = migration.next_batch_range
121
-
122
- if min_value && max_value
123
- create_migration_job!(min_value, max_value)
124
- else
125
- migration.migration_jobs.enqueued.first || migration.migration_jobs.retriable.first
126
- end
127
- end
128
-
129
- def create_migration_job!(min_value, max_value)
130
- migration.migration_jobs.create!(
131
- min_value: min_value,
132
- max_value: max_value
133
- )
134
- end
135
-
136
- def complete_parent_if_needed(migration)
137
- parent = migration.parent
138
- completed = false
139
-
140
- parent.with_lock do
141
- children = parent.children.select(:status)
142
- if children.all?(&:succeeded?)
143
- parent.update!(status: :succeeded, finished_at: Time.current)
144
- completed = true
145
- elsif children.any?(&:failed?)
146
- parent.update!(status: :failed, finished_at: Time.current)
147
- completed = true
148
- end
149
- end
150
-
151
- if completed
152
- ActiveSupport::Notifications.instrument("completed.background_migrations", notifications_payload(migration))
153
- end
154
- end
155
-
156
- def notifications_payload(migration)
157
- { background_migration: migration }
158
- end
159
- end
160
- end
161
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # @private
6
- class MigrationStatusValidator < ActiveModel::Validator
7
- VALID_STATUS_TRANSITIONS = {
8
- # enqueued -> running occurs when the migration starts performing.
9
- # enqueued -> paused occurs when the migration is paused before starting.
10
- "enqueued" => ["running", "paused", "cancelled"],
11
- # running -> paused occurs when a user pauses the migration as
12
- # it's performing.
13
- # running -> finishing occurs when a user manually finishes the migration.
14
- # running -> succeeded occurs when the migration completes successfully.
15
- # running -> failed occurs when the migration raises an exception when running.
16
- "running" => [
17
- "paused",
18
- "finishing",
19
- "succeeded",
20
- "failed",
21
- "cancelled",
22
- ],
23
- # finishing -> succeeded occurs when the migration completes successfully.
24
- # finishing -> failed occurs when the migration raises an exception when running.
25
- "finishing" => ["succeeded", "failed", "cancelled"],
26
- # paused -> running occurs when the migration is resumed after being paused.
27
- "paused" => ["running", "cancelled"],
28
- # failed -> enqueued occurs when the failed migration jobs are retried after being failed.
29
- # failed -> running occurs when the failed migration is retried.
30
- "failed" => ["enqueued", "running", "cancelled"],
31
- }
32
-
33
- def validate(record)
34
- return if !record.status_changed?
35
-
36
- previous_status, new_status = record.status_change
37
- valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
38
-
39
- if !valid_new_statuses.include?(new_status)
40
- record.errors.add(
41
- :status,
42
- "cannot transition background migration from status #{previous_status} to #{new_status}"
43
- )
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # Class responsible for scheduling background migrations.
6
- #
7
- # It selects a single runnable background migration and runs it one step (one batch) at a time.
8
- # A migration is considered runnable if it is not completed and the time interval between
9
- # successive runs has passed.
10
- #
11
- # Scheduler should be configured to run periodically, for example, via cron.
12
- #
13
- # @example Run via whenever
14
- # # add this to schedule.rb
15
- # every 1.minute do
16
- # runner "OnlineMigrations.run_background_data_migrations"
17
- # end
18
- #
19
- # @example Run via whenever (specific shard)
20
- # every 1.minute do
21
- # runner "OnlineMigrations.run_background_data_migrations(shard: :shard_two)"
22
- # end
23
- #
24
- class Scheduler
25
- def self.run(**options)
26
- new.run(**options)
27
- end
28
-
29
- # Runs Scheduler
30
- def run(**options)
31
- active_migrations = Migration.runnable.active.queue_order
32
- active_migrations = active_migrations.where(shard: options[:shard]) if options.key?(:shard)
33
- runnable_migration = active_migrations.select(&:interval_elapsed?).first
34
-
35
- if runnable_migration
36
- runner = MigrationRunner.new(runnable_migration)
37
- runner.run_migration_job
38
- end
39
- end
40
- end
41
- end
42
- end