online_migrations 0.25.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +18 -73
  4. data/docs/0.27-upgrade.md +24 -0
  5. data/docs/background_data_migrations.md +200 -101
  6. data/docs/background_schema_migrations.md +2 -2
  7. data/docs/configuring.md +8 -0
  8. data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
  9. data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
  10. data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +1 -1
  11. data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
  12. data/lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt +34 -0
  13. data/lib/generators/online_migrations/templates/{background_data_migration.rb.tt → data_migration.rb.tt} +8 -9
  14. data/lib/generators/online_migrations/templates/initializer.rb.tt +22 -25
  15. data/lib/generators/online_migrations/templates/install_migration.rb.tt +9 -40
  16. data/lib/generators/online_migrations/upgrade_generator.rb +16 -8
  17. data/lib/online_migrations/active_record_batch_enumerator.rb +8 -0
  18. data/lib/online_migrations/background_data_migrations/backfill_column.rb +50 -0
  19. data/lib/online_migrations/background_data_migrations/config.rb +62 -0
  20. data/lib/online_migrations/{background_migrations → background_data_migrations}/copy_column.rb +15 -28
  21. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_associated_records.rb +9 -5
  22. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_orphaned_records.rb +5 -9
  23. data/lib/online_migrations/background_data_migrations/migration.rb +312 -0
  24. data/lib/online_migrations/{background_migrations → background_data_migrations}/migration_helpers.rb +72 -61
  25. data/lib/online_migrations/background_data_migrations/migration_job.rb +158 -0
  26. data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +65 -0
  27. data/lib/online_migrations/{background_migrations → background_data_migrations}/perform_action_on_relation.rb +5 -5
  28. data/lib/online_migrations/{background_migrations → background_data_migrations}/reset_counters.rb +5 -5
  29. data/lib/online_migrations/background_data_migrations/scheduler.rb +78 -0
  30. data/lib/online_migrations/background_data_migrations/ticker.rb +62 -0
  31. data/lib/online_migrations/background_schema_migrations/config.rb +2 -2
  32. data/lib/online_migrations/background_schema_migrations/migration.rb +57 -127
  33. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +26 -47
  34. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +43 -97
  35. data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
  36. data/lib/online_migrations/batch_iterator.rb +7 -4
  37. data/lib/online_migrations/change_column_type_helpers.rb +75 -14
  38. data/lib/online_migrations/command_checker.rb +32 -20
  39. data/lib/online_migrations/config.rb +12 -4
  40. data/lib/online_migrations/data_migration.rb +127 -0
  41. data/lib/online_migrations/error_messages.rb +16 -0
  42. data/lib/online_migrations/index_definition.rb +1 -1
  43. data/lib/online_migrations/lock_retrier.rb +5 -2
  44. data/lib/online_migrations/migration.rb +8 -1
  45. data/lib/online_migrations/schema_cache.rb +0 -78
  46. data/lib/online_migrations/schema_statements.rb +18 -74
  47. data/lib/online_migrations/shard_aware.rb +44 -0
  48. data/lib/online_migrations/utils.rb +1 -20
  49. data/lib/online_migrations/verbose_sql_logs.rb +1 -7
  50. data/lib/online_migrations/version.rb +1 -1
  51. data/lib/online_migrations.rb +19 -19
  52. metadata +25 -24
  53. data/lib/online_migrations/background_migration.rb +0 -64
  54. data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
  55. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
  56. data/lib/online_migrations/background_migrations/config.rb +0 -74
  57. data/lib/online_migrations/background_migrations/migration.rb +0 -329
  58. data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
  59. data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
  60. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
  61. data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
  62. data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
  63. data/lib/online_migrations/background_migrations/scheduler.rb +0 -42
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # Class representing configuration options for background migrations.
6
- class Config
7
- # The path where generated background migrations will be placed
8
- # @return [String] defaults to "lib"
9
- attr_accessor :migrations_path
10
-
11
- # The module in which background migrations will be placed
12
- # @return [String] defaults to "OnlineMigrations::BackgroundMigrations"
13
- attr_accessor :migrations_module
14
-
15
- # The number of rows to process in a single background migration run
16
- # @return [Integer] defaults to 1_000
17
- #
18
- attr_accessor :batch_size
19
-
20
- # The smaller batches size that the batches will be divided into
21
- # @return [Integer] defaults to 100
22
- #
23
- attr_accessor :sub_batch_size
24
-
25
- # The pause interval between each background migration job's execution (in seconds)
26
- # @return [Integer] defaults to 0
27
- #
28
- attr_accessor :batch_pause
29
-
30
- # The number of milliseconds to sleep between each sub_batch execution
31
- # @return [Integer] defaults to 100 milliseconds
32
- #
33
- attr_accessor :sub_batch_pause_ms
34
-
35
- # Maximum number of batch run attempts
36
- #
37
- # When attempts are exhausted, the individual batch is marked as failed.
38
- # @return [Integer] defaults to 5
39
- #
40
- attr_accessor :batch_max_attempts
41
-
42
- # The number of seconds that must pass before the running job is considered stuck
43
- #
44
- # @return [Integer] defaults to 1 hour
45
- #
46
- attr_accessor :stuck_jobs_timeout
47
-
48
- # The callback to perform when an error occurs in the migration job.
49
- #
50
- # @example
51
- # OnlineMigrations.config.background_migrations.error_handler = ->(error, errored_job) do
52
- # Bugsnag.notify(error) do |notification|
53
- # notification.add_metadata(:background_migration, { name: errored_job.migration_name })
54
- # end
55
- # end
56
- #
57
- # @return [Proc] the callback to perform when an error occurs in the migration job
58
- #
59
- attr_accessor :error_handler
60
-
61
- def initialize
62
- @migrations_path = "lib"
63
- @migrations_module = "OnlineMigrations::BackgroundMigrations"
64
- @batch_size = 1_000
65
- @sub_batch_size = 100
66
- @batch_pause = 0.seconds
67
- @sub_batch_pause_ms = 100
68
- @batch_max_attempts = 5
69
- @stuck_jobs_timeout = 1.hour
70
- @error_handler = ->(error, errored_job) {}
71
- end
72
- end
73
- end
74
- end
@@ -1,329 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- module BackgroundMigrations
5
- # Class representing background data migration.
6
- #
7
- # @note The records of this class should not be created manually, but via
8
- # `enqueue_background_data_migration` helper inside migrations.
9
- #
10
- class Migration < ApplicationRecord
11
- STATUSES = [
12
- :enqueued, # The migration has been enqueued by the user.
13
- :running, # The migration is being performed by a migration executor.
14
- :paused, # The migration was paused in the middle of the run by the user.
15
- :finishing, # The migration is being manually finishing inline by the user.
16
- :failed, # The migration raises an exception when running.
17
- :succeeded, # The migration finished without error.
18
- :cancelled, # The migration was cancelled by the user.
19
- ]
20
-
21
- self.table_name = :background_migrations
22
-
23
- scope :queue_order, -> { order(created_at: :asc) }
24
- scope :parents, -> { where(parent_id: nil) }
25
- scope :runnable, -> { where(composite: false) }
26
- scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
27
- scope :except_succeeded, -> { where.not(status: :succeeded) }
28
- scope :for_migration_name, ->(migration_name) { where(migration_name: normalize_migration_name(migration_name)) }
29
- scope :for_configuration, ->(migration_name, arguments) do
30
- for_migration_name(migration_name).where("arguments = ?", arguments.to_json)
31
- end
32
-
33
- alias_attribute :name, :migration_name
34
-
35
- enum :status, STATUSES.index_with(&:to_s)
36
-
37
- belongs_to :parent, class_name: name, optional: true, inverse_of: :children
38
- has_many :children, class_name: name, foreign_key: :parent_id, dependent: :delete_all, inverse_of: :parent
39
- has_many :migration_jobs, dependent: :delete_all, inverse_of: :migration
40
-
41
- validates :migration_name, :batch_column_name, presence: true
42
-
43
- validates :batch_size, :sub_batch_size, presence: true, numericality: { greater_than: 0 }
44
- validates :min_value, :max_value, presence: true, numericality: { greater_than: 0, unless: :composite? }
45
-
46
- validates :batch_pause, :sub_batch_pause_ms, presence: true,
47
- numericality: { greater_than_or_equal_to: 0 }
48
- validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true, unless: :composite?
49
- validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
50
-
51
- validate :validate_batch_column_values
52
- validate :validate_batch_sizes
53
- validate :validate_jobs_status, if: :status_changed?
54
-
55
- validates_with BackgroundMigrationClassValidator
56
- validates_with MigrationStatusValidator, on: :update
57
-
58
- before_validation :set_defaults
59
- before_update :copy_attributes_to_children, if: :composite?
60
-
61
- # @private
62
- def self.normalize_migration_name(migration_name)
63
- namespace = ::OnlineMigrations.config.background_migrations.migrations_module
64
- migration_name.sub(/^(::)?#{namespace}::/, "")
65
- end
66
-
67
- def migration_name=(class_name)
68
- class_name = class_name.name if class_name.is_a?(Class)
69
- write_attribute(:migration_name, self.class.normalize_migration_name(class_name))
70
- end
71
- alias name= migration_name=
72
-
73
- def completed?
74
- succeeded? || failed?
75
- end
76
-
77
- # Overwrite enum's generated method to correctly work for composite migrations.
78
- def paused!
79
- return super if !composite?
80
-
81
- transaction do
82
- super
83
- children.each { |child| child.paused! if child.enqueued? || child.running? }
84
- end
85
- end
86
-
87
- # Overwrite enum's generated method to correctly work for composite migrations.
88
- def running!
89
- return super if !composite?
90
-
91
- transaction do
92
- super
93
- children.each { |child| child.running! if child.paused? }
94
- end
95
- end
96
-
97
- # Overwrite enum's generated method to correctly work for composite migrations.
98
- def cancelled!
99
- return super if !composite?
100
-
101
- transaction do
102
- super
103
- children.each { |child| child.cancelled! if !child.succeeded? }
104
- end
105
- end
106
- alias cancel cancelled!
107
-
108
- def pausable?
109
- true
110
- end
111
-
112
- def can_be_paused?
113
- enqueued? || running?
114
- end
115
-
116
- def can_be_cancelled?
117
- !succeeded? && !cancelled?
118
- end
119
-
120
- def last_job
121
- migration_jobs.order(:max_value).last
122
- end
123
-
124
- # Returns the progress of the background migration.
125
- #
126
- # @return [Float, nil]
127
- # - when background migration is configured to not track progress, returns `nil`
128
- # - otherwise returns value in range from 0.0 to 100.0
129
- #
130
- def progress
131
- if succeeded?
132
- 100.0
133
- elsif enqueued?
134
- 0.0
135
- elsif composite?
136
- rows_counts = children.to_a.pluck(:rows_count)
137
- if rows_counts.none?(nil)
138
- total_rows_count = rows_counts.sum
139
- return 100.0 if total_rows_count == 0
140
-
141
- progresses = children.map do |child|
142
- child.progress * child.rows_count / total_rows_count # weighted progress
143
- end
144
-
145
- progresses.sum.round(2)
146
- end
147
- elsif rows_count
148
- if rows_count > 0 && rows_count > batch_size
149
- jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
150
- # The last migration job may need to process the amount of rows
151
- # less than the batch size, so we can get a value > 1.0.
152
- ([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
153
- else
154
- 0.0
155
- end
156
- end
157
- end
158
-
159
- def migration_class
160
- BackgroundMigration.named(migration_name)
161
- end
162
-
163
- def migration_object
164
- @migration_object ||= migration_class.new(*arguments)
165
- end
166
-
167
- def migration_relation
168
- migration_object.relation
169
- end
170
-
171
- def migration_model
172
- migration_relation.model
173
- end
174
-
175
- # Returns whether the interval between previous step run has passed.
176
- # @return [Boolean]
177
- #
178
- def interval_elapsed?
179
- last_job = migration_jobs.order(:updated_at).last
180
- return true if last_job.nil?
181
-
182
- last_job.enqueued? || (last_job.updated_at + batch_pause <= Time.current)
183
- end
184
-
185
- # Mark this migration as ready to be processed again.
186
- #
187
- # This method marks failed jobs as ready to be processed again, and
188
- # they will be picked up on the next Scheduler run.
189
- #
190
- def retry
191
- if composite? && failed?
192
- transaction do
193
- update!(status: :enqueued, finished_at: nil)
194
- children.failed.each(&:retry)
195
- end
196
-
197
- true
198
- elsif failed?
199
- transaction do
200
- parent.update!(status: :enqueued, finished_at: nil) if parent
201
- update!(status: :enqueued, started_at: nil, finished_at: nil)
202
-
203
- iterator = BatchIterator.new(migration_jobs.failed)
204
- iterator.each_batch(of: 100) do |batch|
205
- batch.each(&:retry)
206
- end
207
- end
208
-
209
- true
210
- else
211
- false
212
- end
213
- end
214
- alias retry_failed_jobs retry
215
-
216
- # @private
217
- def on_shard(&block)
218
- abstract_class = Utils.find_connection_class(migration_model)
219
-
220
- shard = (self.shard || abstract_class.default_shard).to_sym
221
- abstract_class.connected_to(shard: shard, role: :writing, &block)
222
- end
223
-
224
- # @private
225
- def reset_failed_jobs_attempts
226
- iterator = BatchIterator.new(migration_jobs.failed)
227
- iterator.each_batch(of: 100) do |relation|
228
- relation.update_all(status: :enqueued, attempts: 0)
229
- end
230
- end
231
-
232
- # @private
233
- def next_batch_range
234
- iterator = BatchIterator.new(migration_relation)
235
- batch_range = nil
236
-
237
- on_shard do
238
- # rubocop:disable Lint/UnreachableLoop
239
- iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value, finish: max_value) do |_relation, min_value, max_value|
240
- batch_range = [min_value, max_value]
241
-
242
- break
243
- end
244
- # rubocop:enable Lint/UnreachableLoop
245
- end
246
-
247
- return if batch_range.nil?
248
-
249
- min_value, max_value = batch_range
250
- return if min_value > self.max_value
251
-
252
- max_value = [max_value, self.max_value].min
253
-
254
- [min_value, max_value]
255
- end
256
-
257
- private
258
- def validate_batch_column_values
259
- if max_value.to_i < min_value.to_i
260
- errors.add(:base, "max_value should be greater than or equal to min_value")
261
- end
262
- end
263
-
264
- def validate_batch_sizes
265
- if sub_batch_size.to_i > batch_size.to_i
266
- errors.add(:base, "sub_batch_size should be smaller than or equal to batch_size")
267
- end
268
- end
269
-
270
- def validate_jobs_status
271
- if composite?
272
- if succeeded? && children.except_succeeded.exists?
273
- errors.add(:base, "all child migrations must be succeeded")
274
- elsif failed? && !children.failed.exists?
275
- errors.add(:base, "at least one child migration must be failed")
276
- end
277
- elsif succeeded? && migration_jobs.except_succeeded.exists?
278
- errors.add(:base, "all migration jobs must be succeeded")
279
- elsif failed? && !migration_jobs.failed.exists?
280
- errors.add(:base, "at least one migration job must be failed")
281
- end
282
- end
283
-
284
- def set_defaults
285
- if migration_relation.is_a?(ActiveRecord::Relation)
286
- self.batch_column_name ||= migration_relation.primary_key
287
-
288
- if composite?
289
- self.min_value = self.max_value = self.rows_count = -1 # not relevant
290
- else
291
- on_shard do
292
- # Getting exact min/max values can be a very heavy operation
293
- # and is not needed practically.
294
- self.min_value ||= 1
295
- self.max_value ||= migration_model.unscoped.maximum(batch_column_name) || self.min_value
296
-
297
- count = migration_object.count
298
- self.rows_count = count if count != :no_count
299
- end
300
- end
301
- end
302
-
303
- config = ::OnlineMigrations.config.background_migrations
304
- self.batch_size ||= config.batch_size
305
- self.sub_batch_size ||= config.sub_batch_size
306
- self.batch_pause ||= config.batch_pause
307
- self.sub_batch_pause_ms ||= config.sub_batch_pause_ms
308
- self.batch_max_attempts ||= config.batch_max_attempts
309
- end
310
-
311
- def copy_attributes_to_children
312
- attributes = [:batch_size, :sub_batch_size, :batch_pause, :sub_batch_pause_ms, :batch_max_attempts]
313
- updates = {}
314
- attributes.each do |attribute|
315
- updates[attribute] = read_attribute(attribute) if attribute_changed?(attribute)
316
- end
317
- children.active.update_all(updates) if updates.any?
318
- end
319
-
320
- def next_min_value
321
- if last_job
322
- last_job.max_value.next
323
- else
324
- min_value
325
- end
326
- end
327
- end
328
- end
329
- end
@@ -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