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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +18 -73
- 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/docs/configuring.md +8 -0
- data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
- 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 +22 -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 +158 -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 +57 -127
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +26 -47
- 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/batch_iterator.rb +7 -4
- data/lib/online_migrations/change_column_type_helpers.rb +75 -14
- data/lib/online_migrations/command_checker.rb +32 -20
- data/lib/online_migrations/config.rb +12 -4
- data/lib/online_migrations/data_migration.rb +127 -0
- data/lib/online_migrations/error_messages.rb +16 -0
- data/lib/online_migrations/index_definition.rb +1 -1
- data/lib/online_migrations/lock_retrier.rb +5 -2
- data/lib/online_migrations/migration.rb +8 -1
- data/lib/online_migrations/schema_cache.rb +0 -78
- data/lib/online_migrations/schema_statements.rb +18 -74
- data/lib/online_migrations/shard_aware.rb +44 -0
- data/lib/online_migrations/utils.rb +1 -20
- data/lib/online_migrations/verbose_sql_logs.rb +1 -7
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +19 -19
- metadata +25 -24
- 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,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
|