online_migrations 0.26.0 → 0.27.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -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 +160 -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 +17 -4
  32. data/lib/online_migrations/config.rb +4 -4
  33. data/lib/online_migrations/data_migration.rb +127 -0
  34. data/lib/online_migrations/error_messages.rb +2 -0
  35. data/lib/online_migrations/lock_retrier.rb +5 -2
  36. data/lib/online_migrations/schema_statements.rb +1 -1
  37. data/lib/online_migrations/shard_aware.rb +44 -0
  38. data/lib/online_migrations/version.rb +1 -1
  39. data/lib/online_migrations.rb +18 -11
  40. metadata +22 -21
  41. data/lib/online_migrations/background_migration.rb +0 -64
  42. data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
  43. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
  44. data/lib/online_migrations/background_migrations/config.rb +0 -74
  45. data/lib/online_migrations/background_migrations/migration.rb +0 -329
  46. data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
  47. data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
  48. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
  49. data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
  50. data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
  51. data/lib/online_migrations/background_migrations/scheduler.rb +0 -42
@@ -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
@@ -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