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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/docs/0.27-upgrade.md +24 -0
- data/docs/background_data_migrations.md +200 -101
- data/docs/background_schema_migrations.md +2 -2
- data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
- data/lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt +34 -0
- data/lib/generators/online_migrations/templates/{background_data_migration.rb.tt → data_migration.rb.tt} +8 -9
- data/lib/generators/online_migrations/templates/initializer.rb.tt +19 -25
- data/lib/generators/online_migrations/templates/install_migration.rb.tt +9 -40
- data/lib/generators/online_migrations/upgrade_generator.rb +16 -8
- data/lib/online_migrations/active_record_batch_enumerator.rb +8 -0
- data/lib/online_migrations/background_data_migrations/backfill_column.rb +50 -0
- data/lib/online_migrations/background_data_migrations/config.rb +62 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/copy_column.rb +15 -28
- data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_associated_records.rb +9 -5
- data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_orphaned_records.rb +5 -9
- data/lib/online_migrations/background_data_migrations/migration.rb +312 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/migration_helpers.rb +72 -61
- data/lib/online_migrations/background_data_migrations/migration_job.rb +160 -0
- data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +65 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/perform_action_on_relation.rb +5 -5
- data/lib/online_migrations/{background_migrations → background_data_migrations}/reset_counters.rb +5 -5
- data/lib/online_migrations/background_data_migrations/scheduler.rb +78 -0
- data/lib/online_migrations/background_data_migrations/ticker.rb +62 -0
- data/lib/online_migrations/background_schema_migrations/config.rb +2 -2
- data/lib/online_migrations/background_schema_migrations/migration.rb +51 -123
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +25 -46
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +43 -97
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
- data/lib/online_migrations/change_column_type_helpers.rb +17 -4
- data/lib/online_migrations/config.rb +4 -4
- data/lib/online_migrations/data_migration.rb +127 -0
- data/lib/online_migrations/error_messages.rb +2 -0
- data/lib/online_migrations/lock_retrier.rb +5 -2
- data/lib/online_migrations/schema_statements.rb +1 -1
- data/lib/online_migrations/shard_aware.rb +44 -0
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +18 -11
- metadata +22 -21
- data/lib/online_migrations/background_migration.rb +0 -64
- data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
- data/lib/online_migrations/background_migrations/config.rb +0 -74
- data/lib/online_migrations/background_migrations/migration.rb +0 -329
- data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
- data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
- 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
|