online_migrations 0.10.0 → 0.11.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 +33 -0
- data/README.md +34 -31
- data/docs/background_migrations.md +36 -4
- data/docs/configuring.md +3 -2
- data/lib/generators/online_migrations/install_generator.rb +3 -7
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +5 -3
- data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
- data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
- data/lib/online_migrations/background_migrations/application_record.rb +13 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
- data/lib/online_migrations/background_migrations/copy_column.rb +6 -20
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
- data/lib/online_migrations/background_migrations/migration.rb +123 -34
- data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
- data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
- data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
- data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
- data/lib/online_migrations/change_column_type_helpers.rb +68 -83
- data/lib/online_migrations/command_checker.rb +11 -29
- data/lib/online_migrations/config.rb +7 -15
- data/lib/online_migrations/copy_trigger.rb +15 -10
- data/lib/online_migrations/error_messages.rb +13 -25
- data/lib/online_migrations/foreign_keys_collector.rb +2 -2
- data/lib/online_migrations/indexes_collector.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +4 -9
- data/lib/online_migrations/schema_cache.rb +0 -6
- data/lib/online_migrations/schema_dumper.rb +1 -1
- data/lib/online_migrations/schema_statements.rb +64 -256
- data/lib/online_migrations/utils.rb +18 -56
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +7 -6
- metadata +8 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- data/lib/online_migrations/foreign_key_definition.rb +0 -17
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module OnlineMigrations
|
4
4
|
module BackgroundMigrations
|
5
|
-
class Migration <
|
5
|
+
class Migration < ApplicationRecord
|
6
6
|
STATUSES = [
|
7
7
|
:enqueued, # The migration has been enqueued by the user.
|
8
8
|
:running, # The migration is being performed by a migration executor.
|
@@ -15,25 +15,29 @@ module OnlineMigrations
|
|
15
15
|
self.table_name = :background_migrations
|
16
16
|
|
17
17
|
scope :queue_order, -> { order(created_at: :asc) }
|
18
|
+
scope :runnable, -> { where(composite: false) }
|
18
19
|
scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
|
20
|
+
scope :except_succeeded, -> { where.not(status: :succeeded) }
|
19
21
|
scope :for_migration_name, ->(migration_name) { where(migration_name: normalize_migration_name(migration_name)) }
|
20
22
|
scope :for_configuration, ->(migration_name, arguments) do
|
21
23
|
for_migration_name(migration_name).where("arguments = ?", arguments.to_json)
|
22
24
|
end
|
23
25
|
|
24
|
-
enum status: STATUSES.
|
26
|
+
enum status: STATUSES.index_with(&:to_s)
|
25
27
|
|
28
|
+
belongs_to :parent, class_name: name, optional: true
|
29
|
+
has_many :children, class_name: name, foreign_key: :parent_id
|
26
30
|
has_many :migration_jobs
|
27
31
|
|
28
32
|
validates :migration_name, :batch_column_name, presence: true
|
29
33
|
|
30
|
-
validates :
|
31
|
-
|
34
|
+
validates :batch_size, :sub_batch_size, presence: true, numericality: { greater_than: 0 }
|
35
|
+
validates :min_value, :max_value, presence: true, numericality: { greater_than: 0, unless: :composite? }
|
32
36
|
|
33
37
|
validates :batch_pause, :sub_batch_pause_ms, presence: true,
|
34
38
|
numericality: { greater_than_or_equal_to: 0 }
|
35
|
-
validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
36
|
-
validates :arguments, uniqueness: { scope: :migration_name }
|
39
|
+
validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true, unless: :composite?
|
40
|
+
validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
|
37
41
|
|
38
42
|
validate :validate_batch_column_values
|
39
43
|
validate :validate_batch_sizes
|
@@ -43,6 +47,8 @@ module OnlineMigrations
|
|
43
47
|
validates_with MigrationStatusValidator, on: :update
|
44
48
|
|
45
49
|
before_validation :set_defaults
|
50
|
+
before_create :create_child_migrations, if: :composite?
|
51
|
+
before_update :copy_attributes_to_children, if: :composite?
|
46
52
|
|
47
53
|
# @private
|
48
54
|
def self.normalize_migration_name(migration_name)
|
@@ -58,28 +64,53 @@ module OnlineMigrations
|
|
58
64
|
succeeded? || failed?
|
59
65
|
end
|
60
66
|
|
67
|
+
# Overwrite enum's generated method to correctly work for composite migrations.
|
68
|
+
def paused!
|
69
|
+
return super if !composite?
|
70
|
+
|
71
|
+
transaction do
|
72
|
+
super
|
73
|
+
children.each { |child| child.paused! if child.enqueued? || child.running? }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Overwrite enum's generated method to correctly work for composite migrations.
|
78
|
+
def running!
|
79
|
+
return super if !composite?
|
80
|
+
|
81
|
+
transaction do
|
82
|
+
super
|
83
|
+
children.each { |child| child.running! if child.paused? }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
61
87
|
def last_job
|
62
|
-
migration_jobs.order(max_value
|
88
|
+
migration_jobs.order(:max_value).last
|
63
89
|
end
|
64
90
|
|
65
91
|
def last_completed_job
|
66
|
-
migration_jobs.completed.order(finished_at
|
92
|
+
migration_jobs.completed.order(:finished_at).last
|
67
93
|
end
|
68
94
|
|
69
95
|
# Returns the progress of the background migration.
|
70
96
|
#
|
71
97
|
# @return [Float, nil]
|
72
|
-
# - when background migration is configured to not
|
73
|
-
# - otherwise returns value in range
|
98
|
+
# - when background migration is configured to not track progress, returns `nil`
|
99
|
+
# - otherwise returns value in range from 0.0 to 100.0
|
74
100
|
#
|
75
101
|
def progress
|
76
102
|
if succeeded?
|
77
|
-
|
103
|
+
100.0
|
104
|
+
elsif composite?
|
105
|
+
progresses = children.map(&:progress).compact
|
106
|
+
if progresses.any?
|
107
|
+
(progresses.sum / progresses.size).round(2)
|
108
|
+
end
|
78
109
|
elsif rows_count
|
79
110
|
jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
|
80
111
|
# The last migration job may need to process the amount of rows
|
81
112
|
# less than the batch size, so we can get a value > 1.0.
|
82
|
-
[jobs_rows_count.to_f / rows_count, 1.0].min
|
113
|
+
([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
|
83
114
|
end
|
84
115
|
end
|
85
116
|
|
@@ -95,13 +126,19 @@ module OnlineMigrations
|
|
95
126
|
migration_object.relation
|
96
127
|
end
|
97
128
|
|
129
|
+
def migration_model
|
130
|
+
migration_relation.model
|
131
|
+
end
|
132
|
+
|
98
133
|
# Returns whether the interval between previous step run has passed.
|
99
134
|
# @return [Boolean]
|
100
135
|
#
|
101
136
|
def interval_elapsed?
|
102
|
-
|
137
|
+
last_active_job = migration_jobs.active.order(:updated_at).last
|
138
|
+
|
139
|
+
if last_active_job && !last_active_job.stuck?
|
103
140
|
false
|
104
|
-
elsif (job = last_completed_job)
|
141
|
+
elsif batch_pause > 0 && (job = last_completed_job)
|
105
142
|
job.finished_at + batch_pause <= Time.current
|
106
143
|
else
|
107
144
|
true
|
@@ -123,6 +160,14 @@ module OnlineMigrations
|
|
123
160
|
end
|
124
161
|
end
|
125
162
|
|
163
|
+
# @private
|
164
|
+
def on_shard(&block)
|
165
|
+
abstract_class = find_abstract_class(migration_model)
|
166
|
+
|
167
|
+
shard = (self.shard || abstract_class.default_shard).to_sym
|
168
|
+
abstract_class.connected_to(shard: shard, role: :writing, &block)
|
169
|
+
end
|
170
|
+
|
126
171
|
# @private
|
127
172
|
def reset_failed_jobs_attempts
|
128
173
|
iterator = BatchIterator.new(migration_jobs.failed.attempts_exceeded)
|
@@ -138,16 +183,10 @@ module OnlineMigrations
|
|
138
183
|
|
139
184
|
# rubocop:disable Lint/UnreachableLoop
|
140
185
|
iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
batch_range = relation.pluck("MIN(#{quoted_column}), MAX(#{quoted_column})").first
|
145
|
-
else
|
146
|
-
min = relation.arel_table[batch_column_name].minimum
|
147
|
-
max = relation.arel_table[batch_column_name].maximum
|
186
|
+
min = relation.arel_table[batch_column_name].minimum
|
187
|
+
max = relation.arel_table[batch_column_name].maximum
|
188
|
+
batch_range = relation.pick(min, max)
|
148
189
|
|
149
|
-
batch_range = relation.pluck(min, max).first
|
150
|
-
end
|
151
190
|
break
|
152
191
|
end
|
153
192
|
# rubocop:enable Lint/UnreachableLoop
|
@@ -162,6 +201,10 @@ module OnlineMigrations
|
|
162
201
|
[min_value, max_value]
|
163
202
|
end
|
164
203
|
|
204
|
+
protected
|
205
|
+
attr_accessor :child
|
206
|
+
alias child? child
|
207
|
+
|
165
208
|
private
|
166
209
|
def validate_batch_column_values
|
167
210
|
if max_value.to_i < min_value.to_i
|
@@ -176,7 +219,13 @@ module OnlineMigrations
|
|
176
219
|
end
|
177
220
|
|
178
221
|
def validate_jobs_status
|
179
|
-
if
|
222
|
+
if composite?
|
223
|
+
if succeeded? && children.except_succeeded.exists?
|
224
|
+
errors.add(:base, "all child migrations must be succeeded")
|
225
|
+
elsif failed? && !children.failed.exists?
|
226
|
+
errors.add(:base, "at least one child migration must be failed")
|
227
|
+
end
|
228
|
+
elsif succeeded? && migration_jobs.except_succeeded.exists?
|
180
229
|
errors.add(:base, "all migration jobs must be succeeded")
|
181
230
|
elsif failed? && !migration_jobs.failed.exists?
|
182
231
|
errors.add(:base, "at least one migration job must be failed")
|
@@ -185,12 +234,30 @@ module OnlineMigrations
|
|
185
234
|
|
186
235
|
def set_defaults
|
187
236
|
if migration_relation.is_a?(ActiveRecord::Relation)
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
self.
|
237
|
+
if !child?
|
238
|
+
shards = Utils.shard_names(migration_model)
|
239
|
+
self.composite = shards.size > 1
|
240
|
+
end
|
241
|
+
|
242
|
+
self.batch_column_name ||= migration_relation.primary_key
|
243
|
+
|
244
|
+
if composite?
|
245
|
+
self.min_value = self.max_value = self.rows_count = -1 # not relevant
|
246
|
+
else
|
247
|
+
on_shard do
|
248
|
+
self.min_value ||= migration_relation.minimum(batch_column_name)
|
249
|
+
self.max_value ||= migration_relation.maximum(batch_column_name)
|
250
|
+
|
251
|
+
# This can be the case when run in development on empty tables
|
252
|
+
if min_value.nil?
|
253
|
+
# integer IDs minimum value is 1
|
254
|
+
self.min_value = self.max_value = 1
|
255
|
+
end
|
256
|
+
|
257
|
+
count = migration_object.count
|
258
|
+
self.rows_count = count if count != :no_count
|
259
|
+
end
|
260
|
+
end
|
194
261
|
end
|
195
262
|
|
196
263
|
config = ::OnlineMigrations.config.background_migrations
|
@@ -199,12 +266,27 @@ module OnlineMigrations
|
|
199
266
|
self.batch_pause ||= config.batch_pause
|
200
267
|
self.sub_batch_pause_ms ||= config.sub_batch_pause_ms
|
201
268
|
self.batch_max_attempts ||= config.batch_max_attempts
|
269
|
+
end
|
270
|
+
|
271
|
+
def create_child_migrations
|
272
|
+
shards = Utils.shard_names(migration_model)
|
202
273
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
274
|
+
children = shards.map do |shard|
|
275
|
+
child = Migration.new(migration_name: migration_name, arguments: arguments, shard: shard)
|
276
|
+
child.child = true
|
277
|
+
child
|
207
278
|
end
|
279
|
+
|
280
|
+
self.children = children
|
281
|
+
end
|
282
|
+
|
283
|
+
def copy_attributes_to_children
|
284
|
+
attributes = [:batch_size, :sub_batch_size, :batch_pause, :sub_batch_pause_ms, :batch_max_attempts]
|
285
|
+
updates = {}
|
286
|
+
attributes.each do |attribute|
|
287
|
+
updates[attribute] = read_attribute(attribute) if attribute_changed?(attribute)
|
288
|
+
end
|
289
|
+
children.update_all(updates) if updates.any?
|
208
290
|
end
|
209
291
|
|
210
292
|
def next_min_value
|
@@ -214,6 +296,13 @@ module OnlineMigrations
|
|
214
296
|
min_value
|
215
297
|
end
|
216
298
|
end
|
299
|
+
|
300
|
+
def find_abstract_class(model)
|
301
|
+
model.ancestors.find do |parent|
|
302
|
+
parent == ActiveRecord::Base ||
|
303
|
+
(parent.is_a?(Class) && parent.abstract_class?)
|
304
|
+
end
|
305
|
+
end
|
217
306
|
end
|
218
307
|
end
|
219
308
|
end
|
@@ -223,10 +223,6 @@ module OnlineMigrations
|
|
223
223
|
# For smaller tables it is probably better and easier to directly find and delete orpahed records.
|
224
224
|
#
|
225
225
|
def delete_orphaned_records_in_background(model_name, *associations, **options)
|
226
|
-
if Utils.ar_version <= 4.2
|
227
|
-
raise "#{__method__} does not support Active Record <= 4.2 yet"
|
228
|
-
end
|
229
|
-
|
230
226
|
model_name = model_name.name if model_name.is_a?(Class)
|
231
227
|
|
232
228
|
enqueue_background_migration(
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module OnlineMigrations
|
4
4
|
module BackgroundMigrations
|
5
|
-
class MigrationJob <
|
5
|
+
class MigrationJob < ApplicationRecord
|
6
6
|
STATUSES = [
|
7
7
|
:enqueued,
|
8
8
|
:running,
|
@@ -12,12 +12,11 @@ module OnlineMigrations
|
|
12
12
|
|
13
13
|
self.table_name = :background_migration_jobs
|
14
14
|
|
15
|
-
|
16
|
-
scope :
|
17
|
-
scope :completed, -> { where(status: [statuses[:failed], statuses[:succeeded]]) }
|
15
|
+
scope :active, -> { where(status: [:enqueued, :running]) }
|
16
|
+
scope :completed, -> { where(status: [:failed, :succeeded]) }
|
18
17
|
scope :stuck, -> do
|
19
|
-
timeout =
|
20
|
-
active.where("updated_at <= ?", timeout.ago)
|
18
|
+
timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
|
19
|
+
active.where("updated_at <= ?", timeout.seconds.ago)
|
21
20
|
end
|
22
21
|
|
23
22
|
scope :retriable, -> do
|
@@ -26,7 +25,7 @@ module OnlineMigrations
|
|
26
25
|
stuck_sql = connection.unprepared_statement { stuck.to_sql }
|
27
26
|
failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
|
28
27
|
|
29
|
-
from(Arel.sql(
|
28
|
+
from(Arel.sql(<<~SQL))
|
30
29
|
(
|
31
30
|
(#{failed_retriable_sql})
|
32
31
|
UNION
|
@@ -35,19 +34,16 @@ module OnlineMigrations
|
|
35
34
|
SQL
|
36
35
|
end
|
37
36
|
|
38
|
-
scope :except_succeeded, -> { where(
|
37
|
+
scope :except_succeeded, -> { where.not(status: :succeeded) }
|
39
38
|
scope :attempts_exceeded, -> { where("attempts >= max_attempts") }
|
40
39
|
|
41
|
-
enum status: STATUSES.
|
40
|
+
enum status: STATUSES.index_with(&:to_s)
|
42
41
|
|
43
42
|
delegate :migration_class, :migration_object, :migration_relation, :batch_column_name,
|
44
43
|
:arguments, :batch_pause, to: :migration
|
45
44
|
|
46
45
|
belongs_to :migration
|
47
46
|
|
48
|
-
# For Active Record 5.0+ this is validated by default from belongs_to
|
49
|
-
validates :migration, presence: true
|
50
|
-
|
51
47
|
validates :min_value, :max_value, presence: true, numericality: { greater_than: 0 }
|
52
48
|
validate :values_in_migration_range, if: :min_value?
|
53
49
|
validate :validate_values_order, if: :min_value?
|
@@ -56,6 +52,13 @@ module OnlineMigrations
|
|
56
52
|
|
57
53
|
before_create :copy_settings_from_migration
|
58
54
|
|
55
|
+
# Whether the job is considered stuck (is running for some configured time).
|
56
|
+
#
|
57
|
+
def stuck?
|
58
|
+
timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
|
59
|
+
running? && updated_at <= timeout.seconds.ago
|
60
|
+
end
|
61
|
+
|
59
62
|
# Mark this job as ready to be processed again.
|
60
63
|
#
|
61
64
|
# This is used when retrying failed jobs.
|
@@ -6,7 +6,7 @@ module OnlineMigrations
|
|
6
6
|
class MigrationJobRunner
|
7
7
|
attr_reader :migration_job
|
8
8
|
|
9
|
-
delegate :attempts, :migration_relation, :migration_object, :sub_batch_size,
|
9
|
+
delegate :migration, :attempts, :migration_relation, :migration_object, :sub_batch_size,
|
10
10
|
:batch_column_name, :min_value, :max_value, :pause_ms, to: :migration_job
|
11
11
|
|
12
12
|
def initialize(migration_job)
|
@@ -30,7 +30,7 @@ module OnlineMigrations
|
|
30
30
|
)
|
31
31
|
|
32
32
|
ActiveSupport::Notifications.instrument("process_batch.background_migrations", job_payload) do
|
33
|
-
run_batch
|
33
|
+
migration.on_shard { run_batch }
|
34
34
|
end
|
35
35
|
|
36
36
|
migration_job.update!(status: :succeeded, finished_at: Time.current)
|
@@ -12,8 +12,13 @@ module OnlineMigrations
|
|
12
12
|
|
13
13
|
# Runs one background migration job.
|
14
14
|
def run_migration_job
|
15
|
-
migration
|
16
|
-
|
15
|
+
raise "Should not be called on a composite (with sharding) migration" if migration.composite?
|
16
|
+
|
17
|
+
if migration.enqueued?
|
18
|
+
migration.running!
|
19
|
+
migration.parent.running! if migration.parent && migration.parent.enqueued?
|
20
|
+
end
|
21
|
+
migration_payload = notifications_payload(migration)
|
17
22
|
|
18
23
|
if !migration.migration_jobs.exists?
|
19
24
|
ActiveSupport::Notifications.instrument("started.background_migrations", migration_payload)
|
@@ -37,6 +42,8 @@ module OnlineMigrations
|
|
37
42
|
end
|
38
43
|
|
39
44
|
ActiveSupport::Notifications.instrument("completed.background_migrations", migration_payload)
|
45
|
+
|
46
|
+
complete_parent_if_needed(migration) if migration.parent.present?
|
40
47
|
end
|
41
48
|
|
42
49
|
next_migration_job
|
@@ -52,8 +59,15 @@ module OnlineMigrations
|
|
52
59
|
|
53
60
|
migration.running!
|
54
61
|
|
55
|
-
|
56
|
-
|
62
|
+
if migration.composite?
|
63
|
+
migration.children.each do |child_migration|
|
64
|
+
runner = self.class.new(child_migration)
|
65
|
+
runner.run_all_migration_jobs
|
66
|
+
end
|
67
|
+
else
|
68
|
+
while migration.running?
|
69
|
+
run_migration_job
|
70
|
+
end
|
57
71
|
end
|
58
72
|
end
|
59
73
|
|
@@ -64,13 +78,20 @@ module OnlineMigrations
|
|
64
78
|
def finish
|
65
79
|
return if migration.completed?
|
66
80
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
81
|
+
if migration.composite?
|
82
|
+
migration.children.each do |child_migration|
|
83
|
+
runner = self.class.new(child_migration)
|
84
|
+
runner.finish
|
85
|
+
end
|
86
|
+
else
|
87
|
+
# Mark is as finishing to avoid being picked up
|
88
|
+
# by the background migrations scheduler.
|
89
|
+
migration.finishing!
|
90
|
+
migration.reset_failed_jobs_attempts
|
91
|
+
|
92
|
+
while migration.finishing?
|
93
|
+
run_migration_job
|
94
|
+
end
|
74
95
|
end
|
75
96
|
end
|
76
97
|
|
@@ -95,6 +116,30 @@ module OnlineMigrations
|
|
95
116
|
max_value: max_value
|
96
117
|
)
|
97
118
|
end
|
119
|
+
|
120
|
+
def complete_parent_if_needed(migration)
|
121
|
+
parent = migration.parent
|
122
|
+
completed = false
|
123
|
+
|
124
|
+
parent.with_lock do
|
125
|
+
children = parent.children.select(:status)
|
126
|
+
if children.all?(&:succeeded?)
|
127
|
+
parent.succeeded!
|
128
|
+
completed = true
|
129
|
+
elsif children.any?(&:failed?)
|
130
|
+
parent.failed!
|
131
|
+
completed = true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if completed
|
136
|
+
ActiveSupport::Notifications.instrument("completed.background_migrations", notifications_payload(migration))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def notifications_payload(migration)
|
141
|
+
{ background_migration: migration }
|
142
|
+
end
|
98
143
|
end
|
99
144
|
end
|
100
145
|
end
|
@@ -26,7 +26,7 @@ module OnlineMigrations
|
|
26
26
|
counter_name = reflection.counter_cache_column
|
27
27
|
|
28
28
|
quoted_association_table = connection.quote_table_name(has_many_association.table_name)
|
29
|
-
count_subquery =
|
29
|
+
count_subquery = <<~SQL
|
30
30
|
SELECT COUNT(*)
|
31
31
|
FROM #{quoted_association_table}
|
32
32
|
WHERE #{quoted_association_table}.#{connection.quote_column_name(foreign_key)} =
|
@@ -41,8 +41,7 @@ module OnlineMigrations
|
|
41
41
|
names = Array.wrap(names)
|
42
42
|
options = names.extract_options!
|
43
43
|
touch_updates = touch_attributes_with_time(*names, **options)
|
44
|
-
|
45
|
-
updates << model.send(:sanitize_sql_for_assignment, touch_updates)
|
44
|
+
updates << model.sanitize_sql_for_assignment(touch_updates)
|
46
45
|
end
|
47
46
|
|
48
47
|
relation.update_all(updates.join(", "))
|
@@ -64,11 +63,6 @@ module OnlineMigrations
|
|
64
63
|
|
65
64
|
has_many_association = has_many.find do |association|
|
66
65
|
counter_cache_column = association.counter_cache_column
|
67
|
-
|
68
|
-
# Active Record <= 4.2 is able to return only explicitly provided `counter_cache` column.
|
69
|
-
if !counter_cache_column && Utils.ar_version <= 4.2
|
70
|
-
counter_cache_column = "#{association.name}_count"
|
71
|
-
end
|
72
66
|
counter_cache_column && counter_cache_column.to_sym == counter_association.to_sym
|
73
67
|
end
|
74
68
|
|
@@ -86,7 +80,7 @@ module OnlineMigrations
|
|
86
80
|
def touch_attributes_with_time(*names, time: nil)
|
87
81
|
attribute_names = timestamp_attributes_for_update & model.column_names
|
88
82
|
attribute_names |= names.map(&:to_s)
|
89
|
-
attribute_names.
|
83
|
+
attribute_names.index_with(time || Time.current)
|
90
84
|
end
|
91
85
|
|
92
86
|
def timestamp_attributes_for_update
|
@@ -4,15 +4,14 @@ module OnlineMigrations
|
|
4
4
|
module BackgroundMigrations
|
5
5
|
# Class responsible for scheduling background migrations.
|
6
6
|
# It selects runnable background migrations and runs them one step (one batch) at a time.
|
7
|
-
# A migration is considered runnable if it is not completed and time
|
7
|
+
# A migration is considered runnable if it is not completed and the time interval between
|
8
8
|
# successive runs has passed.
|
9
|
-
# Scheduler ensures (via advisory locks) that at most one background migration at a time is running per database.
|
10
9
|
#
|
11
|
-
# Scheduler should be
|
10
|
+
# Scheduler should be configured to run periodically, for example, via cron.
|
12
11
|
# @example Run via whenever
|
13
12
|
# # add this to schedule.rb
|
14
13
|
# every 1.minute do
|
15
|
-
# runner "OnlineMigrations
|
14
|
+
# runner "OnlineMigrations.run_background_migrations"
|
16
15
|
# end
|
17
16
|
#
|
18
17
|
class Scheduler
|
@@ -22,24 +21,15 @@ module OnlineMigrations
|
|
22
21
|
|
23
22
|
# Runs Scheduler
|
24
23
|
def run
|
25
|
-
active_migrations = Migration.active.queue_order
|
24
|
+
active_migrations = Migration.runnable.active.queue_order
|
26
25
|
runnable_migrations = active_migrations.select(&:interval_elapsed?)
|
27
26
|
|
28
27
|
runnable_migrations.each do |migration|
|
29
|
-
|
30
|
-
|
31
|
-
with_exclusive_lock(connection) do
|
32
|
-
run_migration_job(migration)
|
33
|
-
end
|
28
|
+
run_migration_job(migration)
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
37
32
|
private
|
38
|
-
def with_exclusive_lock(connection, &block)
|
39
|
-
lock = AdvisoryLock.new(name: "online_migrations_scheduler", connection: connection)
|
40
|
-
lock.with_lock(&block)
|
41
|
-
end
|
42
|
-
|
43
33
|
def run_migration_job(migration)
|
44
34
|
runner = MigrationRunner.new(migration)
|
45
35
|
runner.run_migration_job
|