online_migrations 0.14.1 → 0.16.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 +13 -0
- data/README.md +13 -9
- data/docs/{background_migrations.md → background_data_migrations.md} +26 -40
- data/docs/background_schema_migrations.md +163 -0
- data/docs/configuring.md +35 -4
- data/lib/generators/online_migrations/background_migration_generator.rb +12 -1
- data/lib/generators/online_migrations/install_generator.rb +1 -1
- data/lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt +29 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +24 -11
- data/lib/generators/online_migrations/templates/install_migration.rb.tt +75 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +7 -48
- data/lib/generators/online_migrations/upgrade_generator.rb +5 -0
- data/lib/online_migrations/application_record.rb +11 -0
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -7
- data/lib/online_migrations/background_migrations/config.rb +27 -24
- data/lib/online_migrations/background_migrations/delete_associated_records.rb +1 -1
- data/lib/online_migrations/background_migrations/migration.rb +1 -8
- data/lib/online_migrations/background_migrations/migration_helpers.rb +16 -2
- data/lib/online_migrations/background_migrations/migration_job.rb +5 -2
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -1
- data/lib/online_migrations/background_migrations/migration_runner.rb +1 -1
- data/lib/online_migrations/background_schema_migrations/config.rb +40 -0
- data/lib/online_migrations/background_schema_migrations/migration.rb +205 -0
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +76 -0
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +110 -0
- data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +33 -0
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +30 -0
- data/lib/online_migrations/command_checker.rb +31 -1
- data/lib/online_migrations/command_recorder.rb +5 -0
- data/lib/online_migrations/config.rb +32 -0
- data/lib/online_migrations/error_messages.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +0 -2
- data/lib/online_migrations/schema_statements.rb +1 -0
- data/lib/online_migrations/utils.rb +12 -0
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +19 -2
- metadata +13 -4
- data/lib/online_migrations/background_migrations/application_record.rb +0 -13
@@ -1,51 +1,10 @@
|
|
1
|
-
class
|
2
|
-
def
|
3
|
-
|
4
|
-
|
5
|
-
t.string :migration_name, null: false
|
6
|
-
t.jsonb :arguments, default: [], null: false
|
7
|
-
t.string :batch_column_name, null: false
|
8
|
-
t.bigint :min_value, null: false
|
9
|
-
t.bigint :max_value, null: false
|
10
|
-
t.bigint :rows_count
|
11
|
-
t.integer :batch_size, null: false
|
12
|
-
t.integer :sub_batch_size, null: false
|
13
|
-
t.integer :batch_pause, null: false
|
14
|
-
t.integer :sub_batch_pause_ms, null: false
|
15
|
-
t.integer :batch_max_attempts, null: false
|
16
|
-
t.string :status, default: "enqueued", null: false
|
17
|
-
t.string :shard
|
18
|
-
t.boolean :composite, default: false, null: false
|
19
|
-
t.timestamps
|
20
|
-
|
21
|
-
t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
|
22
|
-
|
23
|
-
t.index [:migration_name, :arguments, :shard],
|
24
|
-
unique: true, name: :index_background_migrations_on_unique_configuration
|
25
|
-
end
|
26
|
-
|
27
|
-
create_table :background_migration_jobs do |t|
|
28
|
-
t.bigint :migration_id, null: false
|
29
|
-
t.bigint :min_value, null: false
|
30
|
-
t.bigint :max_value, null: false
|
31
|
-
t.integer :batch_size, null: false
|
32
|
-
t.integer :sub_batch_size, null: false
|
33
|
-
t.integer :pause_ms, null: false
|
34
|
-
t.datetime :started_at
|
35
|
-
t.datetime :finished_at
|
36
|
-
t.string :status, default: "enqueued", null: false
|
37
|
-
t.integer :max_attempts, null: false
|
38
|
-
t.integer :attempts, default: 0, null: false
|
39
|
-
t.string :error_class
|
40
|
-
t.string :error_message
|
41
|
-
t.string :backtrace, array: true
|
42
|
-
t.timestamps
|
43
|
-
|
44
|
-
t.foreign_key :background_migrations, column: :migration_id, on_delete: :cascade
|
1
|
+
class Enqueue<%= class_name %> < <%= migration_parent %>
|
2
|
+
def up
|
3
|
+
enqueue_background_migration("<%= class_name %>", ...args)
|
4
|
+
end
|
45
5
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
6
|
+
def down
|
7
|
+
# Make sure to pass the same arguments as in the "up" method, if any.
|
8
|
+
remove_background_migration("<%= class_name %>", ...args)
|
50
9
|
end
|
51
10
|
end
|
@@ -23,6 +23,11 @@ module OnlineMigrations
|
|
23
23
|
|
24
24
|
migrations = []
|
25
25
|
migrations << "add_sharding_to_online_migrations" if !columns.include?("shard")
|
26
|
+
|
27
|
+
if !connection.table_exists?(BackgroundSchemaMigrations::Migration.table_name)
|
28
|
+
migrations << "create_background_schema_migrations"
|
29
|
+
end
|
30
|
+
|
26
31
|
migrations
|
27
32
|
end
|
28
33
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# Base class for all records used by this gem.
|
5
|
+
#
|
6
|
+
# Can be extended to setup different database where all tables related to
|
7
|
+
# online_migrations will live.
|
8
|
+
class ApplicationRecord < ActiveRecord::Base
|
9
|
+
self.abstract_class = true
|
10
|
+
end
|
11
|
+
end
|
@@ -16,13 +16,6 @@ module OnlineMigrations
|
|
16
16
|
return
|
17
17
|
end
|
18
18
|
|
19
|
-
if relation.joins_values.present? && !record.batch_column_name.to_s.include?(".")
|
20
|
-
record.errors.add(
|
21
|
-
:batch_column_name,
|
22
|
-
"must be a fully-qualified column if you join a table"
|
23
|
-
)
|
24
|
-
end
|
25
|
-
|
26
19
|
if relation.arel.orders.present? || relation.arel.taken.present?
|
27
20
|
record.errors.add(
|
28
21
|
:migration_name,
|
@@ -39,17 +39,13 @@ module OnlineMigrations
|
|
39
39
|
#
|
40
40
|
attr_accessor :batch_max_attempts
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# @example
|
50
|
-
# OnlineMigrations.config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
51
|
-
#
|
52
|
-
attr_reader :throttler
|
42
|
+
def throttler
|
43
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
44
|
+
`config.background_migrations.throttler` is deprecated and will be removed.
|
45
|
+
Use `config.throttler` instead.
|
46
|
+
MSG
|
47
|
+
OnlineMigrations.config.throttler
|
48
|
+
end
|
53
49
|
|
54
50
|
# The number of seconds that must pass before the running job is considered stuck
|
55
51
|
#
|
@@ -57,13 +53,21 @@ module OnlineMigrations
|
|
57
53
|
#
|
58
54
|
attr_accessor :stuck_jobs_timeout
|
59
55
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
56
|
+
def backtrace_cleaner
|
57
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
58
|
+
`config.background_migrations.backtrace_cleaner` is deprecated and will be removed.
|
59
|
+
Use `config.backtrace_cleaner` instead.
|
60
|
+
MSG
|
61
|
+
OnlineMigrations.config.backtrace_cleaner
|
62
|
+
end
|
63
|
+
|
64
|
+
def backtrace_cleaner=(value)
|
65
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
66
|
+
`config.background_migrations.backtrace_cleaner=` is deprecated and will be removed.
|
67
|
+
Use `config.backtrace_cleaner=` instead.
|
68
|
+
MSG
|
69
|
+
OnlineMigrations.config.backtrace_cleaner = value
|
70
|
+
end
|
67
71
|
|
68
72
|
# The callback to perform when an error occurs in the migration job.
|
69
73
|
#
|
@@ -86,17 +90,16 @@ module OnlineMigrations
|
|
86
90
|
@batch_pause = 0.seconds
|
87
91
|
@sub_batch_pause_ms = 100
|
88
92
|
@batch_max_attempts = 5
|
89
|
-
@throttler = -> { false }
|
90
93
|
@stuck_jobs_timeout = 1.hour
|
91
94
|
@error_handler = ->(error, errored_job) {}
|
92
95
|
end
|
93
96
|
|
94
97
|
def throttler=(value)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
98
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
99
|
+
`config.background_migrations.throttler=` is deprecated and will be removed.
|
100
|
+
Use `config.throttler=` instead.
|
101
|
+
MSG
|
102
|
+
OnlineMigrations.config.throttler = value
|
100
103
|
end
|
101
104
|
end
|
102
105
|
end
|
@@ -181,7 +181,7 @@ module OnlineMigrations
|
|
181
181
|
|
182
182
|
# @private
|
183
183
|
def on_shard(&block)
|
184
|
-
abstract_class =
|
184
|
+
abstract_class = Utils.find_connection_class(migration_model)
|
185
185
|
|
186
186
|
shard = (self.shard || abstract_class.default_shard).to_sym
|
187
187
|
abstract_class.connected_to(shard: shard, role: :writing, &block)
|
@@ -290,13 +290,6 @@ module OnlineMigrations
|
|
290
290
|
min_value
|
291
291
|
end
|
292
292
|
end
|
293
|
-
|
294
|
-
def find_abstract_class(model)
|
295
|
-
model.ancestors.find do |parent|
|
296
|
-
parent == ActiveRecord::Base ||
|
297
|
-
(parent.is_a?(Class) && parent.abstract_class?)
|
298
|
-
end
|
299
|
-
end
|
300
293
|
end
|
301
294
|
end
|
302
295
|
end
|
@@ -372,8 +372,7 @@ module OnlineMigrations
|
|
372
372
|
def enqueue_background_migration(migration_name, *arguments, **options)
|
373
373
|
migration = create_background_migration(migration_name, *arguments, **options)
|
374
374
|
|
375
|
-
|
376
|
-
if run_inline && run_inline.call
|
375
|
+
if Utils.run_background_migrations_inline?
|
377
376
|
runner = MigrationRunner.new(migration)
|
378
377
|
runner.run_all_migration_jobs
|
379
378
|
end
|
@@ -381,11 +380,26 @@ module OnlineMigrations
|
|
381
380
|
migration
|
382
381
|
end
|
383
382
|
|
383
|
+
# Removes the background migration for the given class name and arguments, if exists.
|
384
|
+
#
|
385
|
+
# @param migration_name [String, Class] Background migration job class name
|
386
|
+
# @param arguments [Array] Extra arguments the migration was originally created with
|
387
|
+
#
|
388
|
+
# @example
|
389
|
+
# remove_background_migration("BackfillProjectIssuesCount")
|
390
|
+
#
|
391
|
+
def remove_background_migration(migration_name, *arguments)
|
392
|
+
migration_name = migration_name.name if migration_name.is_a?(Class)
|
393
|
+
Migration.for_configuration(migration_name, arguments).delete_all
|
394
|
+
end
|
395
|
+
|
384
396
|
# @private
|
385
397
|
def create_background_migration(migration_name, *arguments, **options)
|
386
398
|
options.assert_valid_keys(:batch_column_name, :min_value, :max_value, :batch_size, :sub_batch_size,
|
387
399
|
:batch_pause, :sub_batch_pause_ms, :batch_max_attempts)
|
388
400
|
|
401
|
+
migration_name = migration_name.name if migration_name.is_a?(Class)
|
402
|
+
|
389
403
|
migration = Migration.new(
|
390
404
|
migration_name: migration_name,
|
391
405
|
arguments: arguments,
|
@@ -44,7 +44,7 @@ module OnlineMigrations
|
|
44
44
|
enum status: STATUSES.index_with(&:to_s)
|
45
45
|
end
|
46
46
|
|
47
|
-
delegate :migration_class, :migration_object, :migration_relation, :batch_column_name,
|
47
|
+
delegate :migration_name, :migration_class, :migration_object, :migration_relation, :batch_column_name,
|
48
48
|
:arguments, :batch_pause, to: :migration
|
49
49
|
|
50
50
|
belongs_to :migration
|
@@ -73,7 +73,10 @@ module OnlineMigrations
|
|
73
73
|
status: self.class.statuses[:enqueued],
|
74
74
|
attempts: 0,
|
75
75
|
started_at: nil,
|
76
|
-
finished_at: nil
|
76
|
+
finished_at: nil,
|
77
|
+
error_class: nil,
|
78
|
+
error_message: nil,
|
79
|
+
backtrace: nil
|
77
80
|
)
|
78
81
|
end
|
79
82
|
|
@@ -35,7 +35,7 @@ module OnlineMigrations
|
|
35
35
|
|
36
36
|
migration_job.update!(status: :succeeded, finished_at: Time.current)
|
37
37
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
38
|
-
backtrace_cleaner = ::OnlineMigrations.config.
|
38
|
+
backtrace_cleaner = ::OnlineMigrations.config.backtrace_cleaner
|
39
39
|
|
40
40
|
migration_job.update!(
|
41
41
|
status: :failed,
|
@@ -46,6 +46,7 @@ module OnlineMigrations
|
|
46
46
|
)
|
47
47
|
|
48
48
|
::OnlineMigrations.config.background_migrations.error_handler.call(e, migration_job)
|
49
|
+
raise if Utils.run_background_migrations_inline?
|
49
50
|
end
|
50
51
|
|
51
52
|
private
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundSchemaMigrations
|
5
|
+
# Class representing configuration options for background schema migrations.
|
6
|
+
class Config
|
7
|
+
# Maximum number of run attempts
|
8
|
+
#
|
9
|
+
# When attempts are exhausted, the migration is marked as failed.
|
10
|
+
# @return [Integer] defaults to 5
|
11
|
+
#
|
12
|
+
attr_accessor :max_attempts
|
13
|
+
|
14
|
+
# Statement timeout value used when running background schema migration.
|
15
|
+
#
|
16
|
+
# @return [Integer] defaults to 1 hour
|
17
|
+
#
|
18
|
+
attr_accessor :statement_timeout
|
19
|
+
|
20
|
+
# The callback to perform when an error occurs in the migration.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# OnlineMigrations.config.background_schema_migrations.error_handler = ->(error, errored_migration) do
|
24
|
+
# Bugsnag.notify(error) do |notification|
|
25
|
+
# notification.add_metadata(:background_schema_migration, { name: errored_migration.name })
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @return [Proc] the callback to perform when an error occurs in the migration
|
30
|
+
#
|
31
|
+
attr_accessor :error_handler
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@max_attempts = 5
|
35
|
+
@statement_timeout = 1.hour
|
36
|
+
@error_handler = ->(error, errored_migration) {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundSchemaMigrations
|
5
|
+
# Class representing background schema migration.
|
6
|
+
#
|
7
|
+
# @note The records of this class should not be created manually, but via
|
8
|
+
# `enqueue_background_schema_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
|
+
:failed, # The migration raises an exception when running.
|
15
|
+
:succeeded, # The migration finished without error.
|
16
|
+
]
|
17
|
+
|
18
|
+
MAX_IDENTIFIER_LENGTH = 63
|
19
|
+
|
20
|
+
self.table_name = :background_schema_migrations
|
21
|
+
|
22
|
+
scope :queue_order, -> { order(created_at: :asc) }
|
23
|
+
scope :runnable, -> { where(composite: false) }
|
24
|
+
scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
|
25
|
+
scope :except_succeeded, -> { where.not(status: :succeeded) }
|
26
|
+
|
27
|
+
scope :stuck, -> do
|
28
|
+
runnable.active.where(<<~SQL)
|
29
|
+
updated_at <= NOW() - interval '1 second' * (COALESCE(statement_timeout, 60*60*24) + 60*10)
|
30
|
+
SQL
|
31
|
+
end
|
32
|
+
|
33
|
+
scope :retriable, -> do
|
34
|
+
failed_retriable = runnable.failed.where("attempts < max_attempts")
|
35
|
+
|
36
|
+
stuck_sql = connection.unprepared_statement { stuck.to_sql }
|
37
|
+
failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
|
38
|
+
|
39
|
+
from(Arel.sql(<<~SQL))
|
40
|
+
(
|
41
|
+
(#{failed_retriable_sql})
|
42
|
+
UNION
|
43
|
+
(#{stuck_sql})
|
44
|
+
) AS #{table_name}
|
45
|
+
SQL
|
46
|
+
end
|
47
|
+
|
48
|
+
alias_attribute :name, :migration_name
|
49
|
+
|
50
|
+
# Avoid deprecation warnings.
|
51
|
+
if Utils.ar_version >= 7
|
52
|
+
enum :status, STATUSES.index_with(&:to_s)
|
53
|
+
else
|
54
|
+
enum status: STATUSES.index_with(&:to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
belongs_to :parent, class_name: name, optional: true
|
58
|
+
has_many :children, class_name: name, foreign_key: :parent_id
|
59
|
+
|
60
|
+
validates :migration_name, presence: true, uniqueness: { scope: :shard }
|
61
|
+
validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
|
62
|
+
validates :definition, presence: true
|
63
|
+
|
64
|
+
validate :validate_children_statuses, if: -> { composite? && status_changed? }
|
65
|
+
validate :validate_connection_class, if: :connection_class_name?
|
66
|
+
validate :validate_table_exists
|
67
|
+
validates_with MigrationStatusValidator, on: :update
|
68
|
+
|
69
|
+
before_validation :set_defaults
|
70
|
+
|
71
|
+
def completed?
|
72
|
+
succeeded? || failed?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the progress of the background schema migration.
|
76
|
+
#
|
77
|
+
# @return [Float] value in range from 0.0 to 100.0
|
78
|
+
#
|
79
|
+
def progress
|
80
|
+
if succeeded?
|
81
|
+
100.0
|
82
|
+
elsif composite?
|
83
|
+
progresses = children.map(&:progress)
|
84
|
+
(progresses.sum.to_f / progresses.size).round(2)
|
85
|
+
else
|
86
|
+
0.0
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Mark this migration as ready to be processed again.
|
91
|
+
#
|
92
|
+
# This is used to manually retrying failed migrations.
|
93
|
+
#
|
94
|
+
def retry
|
95
|
+
if composite?
|
96
|
+
children.failed.each(&:retry)
|
97
|
+
elsif failed?
|
98
|
+
update!(
|
99
|
+
status: self.class.statuses[:enqueued],
|
100
|
+
attempts: 0,
|
101
|
+
started_at: nil,
|
102
|
+
finished_at: nil,
|
103
|
+
error_class: nil,
|
104
|
+
error_message: nil,
|
105
|
+
backtrace: nil
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @private
|
111
|
+
def connection_class
|
112
|
+
if connection_class_name && (klass = connection_class_name.safe_constantize)
|
113
|
+
Utils.find_connection_class(klass)
|
114
|
+
else
|
115
|
+
ActiveRecord::Base
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @private
|
120
|
+
def attempts_exceeded?
|
121
|
+
attempts >= max_attempts
|
122
|
+
end
|
123
|
+
|
124
|
+
# @private
|
125
|
+
def run
|
126
|
+
on_shard do
|
127
|
+
connection = connection_class.connection
|
128
|
+
|
129
|
+
connection.with_lock_retries do
|
130
|
+
statement_timeout = self.statement_timeout || OnlineMigrations.config.statement_timeout
|
131
|
+
|
132
|
+
with_statement_timeout(connection, statement_timeout) do
|
133
|
+
case definition
|
134
|
+
when /create (unique )?index/i
|
135
|
+
index = connection.indexes(table_name).find { |i| i.name == name }
|
136
|
+
if index
|
137
|
+
# Use index validity from https://github.com/rails/rails/pull/45160
|
138
|
+
# when switching to ActiveRecord >= 7.1.
|
139
|
+
schema = connection.send(:__schema_for_table, table_name)
|
140
|
+
if connection.send(:__index_valid?, name, schema: schema)
|
141
|
+
return
|
142
|
+
else
|
143
|
+
connection.remove_index(table_name, name: name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
connection.execute(definition)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
def validate_children_statuses
|
156
|
+
if composite?
|
157
|
+
if succeeded? && children.except_succeeded.exists?
|
158
|
+
errors.add(:base, "all child migrations must be succeeded")
|
159
|
+
elsif failed? && !children.failed.exists?
|
160
|
+
errors.add(:base, "at least one child migration must be failed")
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def validate_connection_class
|
166
|
+
klass = connection_class_name.safe_constantize
|
167
|
+
if !(klass < ActiveRecord::Base)
|
168
|
+
errors.add(:connection_class_name, "is not an ActiveRecord::Base child class")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def validate_table_exists
|
173
|
+
# Skip this validation if we have invalid connection class name.
|
174
|
+
return if errors.include?(:connection_class_name)
|
175
|
+
|
176
|
+
on_shard do
|
177
|
+
if !connection_class.connection.table_exists?(table_name)
|
178
|
+
errors.add(:table_name, "'#{table_name}' does not exist")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def set_defaults
|
184
|
+
config = ::OnlineMigrations.config.background_schema_migrations
|
185
|
+
self.max_attempts ||= config.max_attempts
|
186
|
+
self.statement_timeout ||= config.statement_timeout
|
187
|
+
end
|
188
|
+
|
189
|
+
def on_shard(&block)
|
190
|
+
shard = (self.shard || connection_class.default_shard).to_sym
|
191
|
+
connection_class.connected_to(shard: shard, role: :writing, &block)
|
192
|
+
end
|
193
|
+
|
194
|
+
def with_statement_timeout(connection, timeout)
|
195
|
+
return yield if timeout.nil?
|
196
|
+
|
197
|
+
prev_value = connection.select_value("SHOW statement_timeout")
|
198
|
+
connection.execute("SET statement_timeout TO #{connection.quote(timeout.in_milliseconds)}")
|
199
|
+
yield
|
200
|
+
ensure
|
201
|
+
connection.execute("SET statement_timeout TO #{connection.quote(prev_value)}")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundSchemaMigrations
|
5
|
+
module MigrationHelpers
|
6
|
+
def add_index_in_background(table_name, column_name, **options)
|
7
|
+
migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name)
|
8
|
+
|
9
|
+
if index_exists?(table_name, column_name, **options)
|
10
|
+
Utils.say("Index creation was not enqueued because the index already exists.")
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
options[:algorithm] = :concurrently
|
15
|
+
index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
|
16
|
+
|
17
|
+
create_index = ActiveRecord::ConnectionAdapters::CreateIndexDefinition.new(index, algorithm, if_not_exists)
|
18
|
+
schema_creation = ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaCreation.new(self)
|
19
|
+
definition = schema_creation.accept(create_index)
|
20
|
+
|
21
|
+
enqueue_background_schema_migration(index.name, table_name, definition: definition, **migration_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_index_in_background(table_name, column_name = nil, name:, **options)
|
25
|
+
raise ArgumentError, "Index name must be specified" if name.blank?
|
26
|
+
|
27
|
+
migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name)
|
28
|
+
|
29
|
+
if !index_exists?(table_name, column_name, **options, name: name)
|
30
|
+
Utils.say("Index deletion was not enqueued because the index does not exist.")
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
definition = "DROP INDEX CONCURRENTLY IF EXISTS #{quote_column_name(name)}"
|
35
|
+
enqueue_background_schema_migration(name, table_name, definition: definition, **migration_options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def enqueue_background_schema_migration(name, table_name, **options)
|
39
|
+
if options[:connection_class_name].nil? && Utils.multiple_databases?
|
40
|
+
raise ArgumentError, "You must pass a :connection_class_name when using multiple databases."
|
41
|
+
end
|
42
|
+
|
43
|
+
migration = create_background_schema_migration(name, table_name, **options)
|
44
|
+
|
45
|
+
run_inline = OnlineMigrations.config.run_background_migrations_inline
|
46
|
+
if run_inline && run_inline.call
|
47
|
+
runner = MigrationRunner.new(migration)
|
48
|
+
runner.run
|
49
|
+
end
|
50
|
+
|
51
|
+
migration
|
52
|
+
end
|
53
|
+
|
54
|
+
# @private
|
55
|
+
def create_background_schema_migration(migration_name, table_name, **options)
|
56
|
+
options.assert_valid_keys(:definition, :max_attempts, :statement_timeout, :connection_class_name)
|
57
|
+
migration = Migration.new(migration_name: migration_name, table_name: table_name, **options)
|
58
|
+
|
59
|
+
shards = Utils.shard_names(migration.connection_class)
|
60
|
+
if shards.size > 1
|
61
|
+
migration.children = shards.map do |shard|
|
62
|
+
child = migration.dup
|
63
|
+
child.shard = shard
|
64
|
+
child
|
65
|
+
end
|
66
|
+
|
67
|
+
migration.composite = true
|
68
|
+
end
|
69
|
+
|
70
|
+
# This will save all the records using a transaction.
|
71
|
+
migration.save!
|
72
|
+
migration
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|