online_migrations 0.26.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 +14 -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 +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 +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 +3 -2
- data/lib/online_migrations/config.rb +4 -4
- data/lib/online_migrations/data_migration.rb +127 -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,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
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OnlineMigrations
|
4
|
-
module BackgroundMigrations
|
5
|
-
# @private
|
6
|
-
class MigrationStatusValidator < ActiveModel::Validator
|
7
|
-
VALID_STATUS_TRANSITIONS = {
|
8
|
-
# enqueued -> running occurs when the migration starts performing.
|
9
|
-
# enqueued -> paused occurs when the migration is paused before starting.
|
10
|
-
"enqueued" => ["running", "paused", "cancelled"],
|
11
|
-
# running -> paused occurs when a user pauses the migration as
|
12
|
-
# it's performing.
|
13
|
-
# running -> finishing occurs when a user manually finishes the migration.
|
14
|
-
# running -> succeeded occurs when the migration completes successfully.
|
15
|
-
# running -> failed occurs when the migration raises an exception when running.
|
16
|
-
"running" => [
|
17
|
-
"paused",
|
18
|
-
"finishing",
|
19
|
-
"succeeded",
|
20
|
-
"failed",
|
21
|
-
"cancelled",
|
22
|
-
],
|
23
|
-
# finishing -> succeeded occurs when the migration completes successfully.
|
24
|
-
# finishing -> failed occurs when the migration raises an exception when running.
|
25
|
-
"finishing" => ["succeeded", "failed", "cancelled"],
|
26
|
-
# paused -> running occurs when the migration is resumed after being paused.
|
27
|
-
"paused" => ["running", "cancelled"],
|
28
|
-
# failed -> enqueued occurs when the failed migration jobs are retried after being failed.
|
29
|
-
# failed -> running occurs when the failed migration is retried.
|
30
|
-
"failed" => ["enqueued", "running", "cancelled"],
|
31
|
-
}
|
32
|
-
|
33
|
-
def validate(record)
|
34
|
-
return if !record.status_changed?
|
35
|
-
|
36
|
-
previous_status, new_status = record.status_change
|
37
|
-
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
38
|
-
|
39
|
-
if !valid_new_statuses.include?(new_status)
|
40
|
-
record.errors.add(
|
41
|
-
:status,
|
42
|
-
"cannot transition background migration from status #{previous_status} to #{new_status}"
|
43
|
-
)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OnlineMigrations
|
4
|
-
module BackgroundMigrations
|
5
|
-
# Class responsible for scheduling background migrations.
|
6
|
-
#
|
7
|
-
# It selects a single runnable background migration and runs it one step (one batch) at a time.
|
8
|
-
# A migration is considered runnable if it is not completed and the time interval between
|
9
|
-
# successive runs has passed.
|
10
|
-
#
|
11
|
-
# Scheduler should be configured to run periodically, for example, via cron.
|
12
|
-
#
|
13
|
-
# @example Run via whenever
|
14
|
-
# # add this to schedule.rb
|
15
|
-
# every 1.minute do
|
16
|
-
# runner "OnlineMigrations.run_background_data_migrations"
|
17
|
-
# end
|
18
|
-
#
|
19
|
-
# @example Run via whenever (specific shard)
|
20
|
-
# every 1.minute do
|
21
|
-
# runner "OnlineMigrations.run_background_data_migrations(shard: :shard_two)"
|
22
|
-
# end
|
23
|
-
#
|
24
|
-
class Scheduler
|
25
|
-
def self.run(**options)
|
26
|
-
new.run(**options)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Runs Scheduler
|
30
|
-
def run(**options)
|
31
|
-
active_migrations = Migration.runnable.active.queue_order
|
32
|
-
active_migrations = active_migrations.where(shard: options[:shard]) if options.key?(:shard)
|
33
|
-
runnable_migration = active_migrations.select(&:interval_elapsed?).first
|
34
|
-
|
35
|
-
if runnable_migration
|
36
|
-
runner = MigrationRunner.new(runnable_migration)
|
37
|
-
runner.run_migration_job
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|