online_migrations 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|