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.
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