online_migrations 0.14.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +13 -9
  4. data/docs/{background_migrations.md → background_data_migrations.md} +26 -40
  5. data/docs/background_schema_migrations.md +163 -0
  6. data/docs/configuring.md +35 -4
  7. data/lib/generators/online_migrations/background_migration_generator.rb +12 -1
  8. data/lib/generators/online_migrations/install_generator.rb +1 -1
  9. data/lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt +29 -0
  10. data/lib/generators/online_migrations/templates/initializer.rb.tt +24 -11
  11. data/lib/generators/online_migrations/templates/install_migration.rb.tt +75 -0
  12. data/lib/generators/online_migrations/templates/migration.rb.tt +7 -48
  13. data/lib/generators/online_migrations/upgrade_generator.rb +5 -0
  14. data/lib/online_migrations/application_record.rb +11 -0
  15. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -7
  16. data/lib/online_migrations/background_migrations/config.rb +27 -24
  17. data/lib/online_migrations/background_migrations/delete_associated_records.rb +1 -1
  18. data/lib/online_migrations/background_migrations/migration.rb +1 -8
  19. data/lib/online_migrations/background_migrations/migration_helpers.rb +16 -2
  20. data/lib/online_migrations/background_migrations/migration_job.rb +5 -2
  21. data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -1
  22. data/lib/online_migrations/background_migrations/migration_runner.rb +1 -1
  23. data/lib/online_migrations/background_schema_migrations/config.rb +40 -0
  24. data/lib/online_migrations/background_schema_migrations/migration.rb +205 -0
  25. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +76 -0
  26. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +110 -0
  27. data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +33 -0
  28. data/lib/online_migrations/background_schema_migrations/scheduler.rb +30 -0
  29. data/lib/online_migrations/command_checker.rb +31 -1
  30. data/lib/online_migrations/command_recorder.rb +5 -0
  31. data/lib/online_migrations/config.rb +32 -0
  32. data/lib/online_migrations/error_messages.rb +3 -3
  33. data/lib/online_migrations/lock_retrier.rb +0 -2
  34. data/lib/online_migrations/schema_statements.rb +1 -0
  35. data/lib/online_migrations/utils.rb +12 -0
  36. data/lib/online_migrations/version.rb +1 -1
  37. data/lib/online_migrations.rb +19 -2
  38. metadata +13 -4
  39. data/lib/online_migrations/background_migrations/application_record.rb +0 -13
@@ -1,51 +1,10 @@
1
- class InstallOnlineMigrations < <%= migration_parent %>
2
- def change
3
- create_table :background_migrations do |t|
4
- t.bigint :parent_id
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
- t.index [:migration_id, :max_value], name: :index_background_migration_jobs_on_max_value
47
- t.index [:migration_id, :status, :updated_at], name: :index_background_migration_jobs_on_updated_at
48
- t.index [:migration_id, :finished_at], name: :index_background_migration_jobs_on_finished_at
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
- # Allows to throttle background migrations based on external signal (e.g. database health)
43
- #
44
- # It will be called before each batch run.
45
- # If throttled, the current run will be retried next time.
46
- #
47
- # @return [Proc]
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
- # The Active Support backtrace cleaner that will be used to clean the
61
- # backtrace of a migration job that errors.
62
- #
63
- # @return [ActiveSupport::BacktraceCleaner, nil] the backtrace cleaner to
64
- # use when cleaning a job's backtrace. Defaults to `Rails.backtrace_cleaner`
65
- #
66
- attr_accessor :backtrace_cleaner
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
- if !value.respond_to?(:call)
96
- raise ArgumentError, "background_migrations throttler must be a callable."
97
- end
98
-
99
- @throttler = value
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
@@ -21,7 +21,7 @@ module OnlineMigrations
21
21
  end
22
22
 
23
23
  def process_batch(relation)
24
- relation.delete_all(:delete_all)
24
+ relation.delete_all
25
25
  end
26
26
  end
27
27
  end
@@ -181,7 +181,7 @@ module OnlineMigrations
181
181
 
182
182
  # @private
183
183
  def on_shard(&block)
184
- abstract_class = find_abstract_class(migration_model)
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
- run_inline = OnlineMigrations.config.run_background_migrations_inline
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.background_migrations.backtrace_cleaner
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
@@ -105,7 +105,7 @@ module OnlineMigrations
105
105
  end
106
106
 
107
107
  def should_throttle?
108
- ::OnlineMigrations.config.background_migrations.throttler.call
108
+ ::OnlineMigrations.config.throttler.call
109
109
  end
110
110
 
111
111
  def find_or_create_next_migration_job
@@ -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