online_migrations 0.24.0 → 0.26.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +18 -73
  4. data/docs/background_schema_migrations.md +2 -1
  5. data/docs/configuring.md +8 -0
  6. data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
  7. data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +31 -0
  8. data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
  9. data/lib/generators/online_migrations/templates/initializer.rb.tt +3 -0
  10. data/lib/generators/online_migrations/templates/install_migration.rb.tt +2 -0
  11. data/lib/generators/online_migrations/templates/migration.rb.tt +2 -2
  12. data/lib/generators/online_migrations/upgrade_generator.rb +4 -0
  13. data/lib/online_migrations/background_migrations/migration.rb +16 -22
  14. data/lib/online_migrations/background_migrations/migration_job.rb +8 -7
  15. data/lib/online_migrations/background_migrations/migration_job_runner.rb +3 -1
  16. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +2 -1
  17. data/lib/online_migrations/background_migrations/migration_runner.rb +13 -6
  18. data/lib/online_migrations/background_schema_migrations/migration.rb +30 -25
  19. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +1 -1
  20. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +6 -2
  21. data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +6 -2
  22. data/lib/online_migrations/batch_iterator.rb +7 -4
  23. data/lib/online_migrations/change_column_type_helpers.rb +72 -12
  24. data/lib/online_migrations/command_checker.rb +32 -20
  25. data/lib/online_migrations/config.rb +8 -0
  26. data/lib/online_migrations/error_messages.rb +16 -0
  27. data/lib/online_migrations/index_definition.rb +1 -1
  28. data/lib/online_migrations/lock_retrier.rb +6 -9
  29. data/lib/online_migrations/migration.rb +8 -1
  30. data/lib/online_migrations/schema_cache.rb +0 -78
  31. data/lib/online_migrations/schema_statements.rb +20 -81
  32. data/lib/online_migrations/utils.rb +1 -20
  33. data/lib/online_migrations/verbose_sql_logs.rb +1 -7
  34. data/lib/online_migrations/version.rb +1 -1
  35. data/lib/online_migrations.rb +1 -8
  36. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99170e244f0f008e8d097222c88a96555692aa5afc136087d03341866ed8412c
4
- data.tar.gz: 6068a722274c73d8a0ab3c1bed4d1f2673cd99d15ca77c24f7fff5af75e77129
3
+ metadata.gz: cbca384d5eaf4ab575e80c60160d42326ef86f1baf21419f290663e0d9698eb0
4
+ data.tar.gz: b90abf4e278ee1b2ab27e60472f6bb127b124797084f53205a0929a5cc98268b
5
5
  SHA512:
6
- metadata.gz: 9d2a127aee978d36676d8bdd85352487f66a14289cca28173eb1126aee65ceeea5dda067171ffb320b7c0cb2be903712ab158af12312452180b8e56da1e7b641
7
- data.tar.gz: 305f89162f87a3746d91bfdfacae00b7d0f2a374f27755c846cd4386d65694442b70fc6963a7dec8845c192eca2cc625bb020c7c4aec664320a395b124bb3e71
6
+ metadata.gz: eea056dd2b0c5a24213543d96d6b2ca5a1c230c38aa37f475d674157bc354a8d0c1cb39e6e2c0a6e86867b184e82b51ec9ad0452eed00e1a7b7a6098b203c3ff
7
+ data.tar.gz: 9636d4b788d655ad04d0141cd28ae61c053b15d83f00c63b236ee181feeb438c18f2a8f34da49b26d955ff3c004c053f0d570d00ff6801e47e15c438b09f2c51
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.26.0 (2025-04-28)
4
+
5
+ - Drop support for Ruby < 3.1 and Rails < 7.1
6
+ - Add check for `change_column` for columns with check constraints
7
+
8
+ - Allow to require safety reason explanation when calling `safery_assured`
9
+
10
+ ```ruby
11
+ # config/initializers/online_migrations.rb
12
+ config.require_safety_assured_reason = true
13
+
14
+ # in migration
15
+ safety_assured("Table is small") do
16
+ add_index :users, :email, unique: true
17
+ end
18
+ ```
19
+
20
+ ## 0.25.0 (2025-02-03)
21
+
22
+ - Track start/finish time of background data migrations
23
+
24
+ Note: Make sure to run `bin/rails generate online_migrations:upgrade` if using background migrations.
25
+
26
+ - Add new state for errored background migrations
27
+
28
+ * **errored** - migration raised an error during last run
29
+ * **failed** - migration raises an error when running and retry attempts exceeded
30
+
31
+ - Fix thread safety issue for lock retrier
32
+
33
+ Note: Lock retrier changed its API (`LockRetrier#connection` accessor was removed and
34
+ `LockRetrier#with_lock_retries` now accepts a connection argument). This change might be of interest
35
+ if you implemented a custom lock retrier class.
36
+
3
37
  ## 0.24.0 (2025-01-20)
4
38
 
5
39
  - Add ability to run a separate background migrations scheduler per shard
data/README.md CHANGED
@@ -16,8 +16,8 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Ruby 3.0+
20
- - Rails 7.0+
19
+ - Ruby 3.1+
20
+ - Rails 7.1+
21
21
  - PostgreSQL 12+
22
22
 
23
23
  For older Ruby and Rails versions you can use older versions of this gem.
@@ -64,76 +64,32 @@ An operation is classified as dangerous if it either:
64
64
 
65
65
  ## Example
66
66
 
67
- Consider the following migration:
68
-
69
- ```ruby
70
- class AddAdminToUsers < ActiveRecord::Migration[8.0]
71
- def change
72
- add_column :users, :admin, :boolean, default: false, null: false
73
- end
74
- end
75
- ```
76
-
77
- If the `users` table is large, running this migration on a live PostgreSQL < 11 database will likely cause downtime.
78
-
79
- A safer approach would be to run something like the following:
80
-
81
- ```ruby
82
- class AddAdminToUsers < ActiveRecord::Migration[8.0]
83
- # Do not wrap the migration in a transaction so that locks are held for a shorter time.
84
- disable_ddl_transaction!
85
-
86
- def up
87
- # Lower PostgreSQL's lock timeout to avoid statement queueing.
88
- execute "SET lock_timeout TO '5s'" # The lock_timeout duration is customizable.
89
-
90
- # Add the column without the default value and the not-null constraint.
91
- add_column :users, :admin, :boolean
92
-
93
- # Set the column's default value.
94
- change_column_default :users, :admin, false
95
-
96
- # Backfill the column in batches.
97
- User.in_batches.update_all(admin: false)
98
-
99
- # Add the not-null constraint. Beforehand, set a short statement timeout so that
100
- # Postgres does not spend too much time performing the full table scan to verify
101
- # the column contains no nulls.
102
- execute "SET statement_timeout TO '5s'"
103
- change_column_null :users, :admin, false
104
- end
105
-
106
- def down
107
- remove_column :users, :admin
108
- end
109
- end
110
- ```
111
-
112
- When you actually run the original migration, you will get an error message:
67
+ When you run a migration that's potentially dangerous, you'll see an error message like:
113
68
 
114
69
  ```txt
115
70
  ⚠️ [online_migrations] Dangerous operation detected ⚠️
116
71
 
117
- Adding a column with a non-null default blocks reads and writes while the entire table is rewritten.
118
-
72
+ Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
119
73
  A safer approach is to:
120
- 1. add the column without a default value
121
- 2. change the column default
122
- 3. backfill existing rows with the new value
123
- 4. add the NOT NULL constraint
124
74
 
125
- add_column_with_default takes care of all this steps:
75
+ 1. Ignore the column:
126
76
 
127
- class AddAdminToUsers < ActiveRecord::Migration[8.0]
128
- disable_ddl_transaction!
77
+ class User < ApplicationRecord
78
+ self.ignored_columns += ["name"]
79
+ end
129
80
 
130
- def change
131
- add_column_with_default :users, :admin, :boolean, default: false, null: false
81
+ 2. Deploy
82
+ 3. Wrap column removing in a safety_assured { ... } block
83
+
84
+ class RemoveColumn < ActiveRecord::Migration[8.0]
85
+ def change
86
+ safety_assured { remove_column :users, :name }
87
+ end
132
88
  end
133
- end
134
- ```
135
89
 
136
- It suggests how to safely implement a migration, which essentially runs the steps similar to described in the previous example.
90
+ 4. Remove column ignoring from step 1
91
+ 5. Deploy
92
+ ```
137
93
 
138
94
  ## Checks
139
95
 
@@ -1278,17 +1234,6 @@ Interesting reads:
1278
1234
  - [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)
1279
1235
  - [Avoiding integer overflows with zero downtime](https://buildkite.com/blog/avoiding-integer-overflows-with-zero-downtime)
1280
1236
 
1281
- ## Maybe TODO
1282
-
1283
- - support MySQL
1284
- - support other ORMs
1285
-
1286
- Background migrations:
1287
-
1288
- - extract as a separate gem
1289
- - add UI
1290
- - support batching over non-integer and multiple columns
1291
-
1292
1237
  ## Comparison to `strong_migrations`
1293
1238
 
1294
1239
  This gem was heavily inspired by the `strong_migrations` and GitLab's approaches to database migrations. This gem is a superset of `strong_migrations`, feature-wise, and has the same APIs.
@@ -134,7 +134,8 @@ Background Schema Migrations can be in various states during its execution:
134
134
 
135
135
  * **enqueued**: A migration has been enqueued by the user.
136
136
  * **running**: A migration is being performed by a migration executor.
137
- * **failed**: A migration raises an exception when running.
137
+ * **errored**: A migration raised an error during last run.
138
+ * **failed**: A migration raises an error when running and retry attempts exceeded.
138
139
  * **succeeded**: A migration finished without error.
139
140
  * **cancelled**: A migration was cancelled by the user.
140
141
 
data/docs/configuring.md CHANGED
@@ -38,6 +38,14 @@ config.disable_check(:remove_index)
38
38
 
39
39
  Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/error_messages.rb) for the list of keys.
40
40
 
41
+ ## Requiring safety_assured reason
42
+
43
+ To require safety reason explanation when calling `safery_assured` (disabled by default):
44
+
45
+ ```ruby
46
+ config.require_safety_assured_reason = true
47
+ ```
48
+
41
49
  ## Down Migrations / Rollbacks
42
50
 
43
51
  By default, checks are disabled when migrating down. Enable them with:
@@ -1,6 +1,6 @@
1
1
  class AddShardingToOnlineMigrations < <%= migration_parent %>
2
2
  def change
3
- safety_assured do
3
+ safety_assured("Table is small") do
4
4
  remove_index :background_migrations, [:migration_name, :arguments], unique: true
5
5
 
6
6
  change_table :background_migrations do |t|
@@ -0,0 +1,31 @@
1
+ class AddTimestampsToBackgroundMigrations < <%= migration_parent %>
2
+ def change
3
+ safety_assured("Table is small") do
4
+ add_column :background_migrations, :started_at, :datetime
5
+ add_column :background_migrations, :finished_at, :datetime
6
+
7
+ up_only do
8
+ # Set started_at.
9
+ execute(<<~SQL)
10
+ UPDATE background_migrations
11
+ SET started_at = (
12
+ SELECT min(started_at)
13
+ FROM background_migration_jobs
14
+ WHERE background_migration_jobs.migration_id = background_migrations.id
15
+ )
16
+ SQL
17
+
18
+ # Set finished_at.
19
+ execute(<<~SQL)
20
+ UPDATE background_migrations
21
+ SET finished_at = (
22
+ SELECT max(finished_at)
23
+ FROM background_migration_jobs
24
+ WHERE background_migration_jobs.migration_id = background_migrations.id
25
+ )
26
+ WHERE status IN ('failed', 'succeeded')
27
+ SQL
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,6 @@
1
1
  class BackgroundSchemaMigrationsChangeUniqueIndex < <%= migration_parent %>
2
2
  def change
3
- safety_assured do
3
+ safety_assured("Table is small") do
4
4
  remove_index :background_schema_migrations, name: :index_background_schema_migrations_on_unique_configuration
5
5
  add_index :background_schema_migrations, [:migration_name, :shard, :connection_class_name], unique: true,
6
6
  name: :index_background_schema_migrations_on_unique_configuration
@@ -12,6 +12,9 @@ OnlineMigrations.configure do |config|
12
12
  # Set the version of the production database so the right checks are run in development.
13
13
  # config.target_version = 17
14
14
 
15
+ # Configure whether to require safety reason explanation when calling #safery_assured.
16
+ config.require_safety_assured_reason = false
17
+
15
18
  # Configure whether to perform checks when migrating down.
16
19
  config.check_down = false
17
20
 
@@ -16,6 +16,8 @@ class InstallOnlineMigrations < <%= migration_parent %>
16
16
  t.string :status, default: "enqueued", null: false
17
17
  t.string :shard
18
18
  t.boolean :composite, default: false, null: false
19
+ t.datetime :started_at
20
+ t.datetime :finished_at
19
21
  t.timestamps
20
22
 
21
23
  t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
@@ -1,10 +1,10 @@
1
1
  class Enqueue<%= class_name %> < <%= migration_parent %>
2
2
  def up
3
- enqueue_background_data_migration("<%= class_name %>", ...args)
3
+ enqueue_background_data_migration("<%= class_name %>")
4
4
  end
5
5
 
6
6
  def down
7
7
  # Make sure to pass the same arguments as in the "up" method, if any.
8
- remove_background_data_migration("<%= class_name %>", ...args)
8
+ remove_background_data_migration("<%= class_name %>")
9
9
  end
10
10
  end
@@ -34,6 +34,10 @@ module OnlineMigrations
34
34
  migrations << "background_schema_migrations_change_unique_index"
35
35
  end
36
36
 
37
+ if !connection.column_exists?(BackgroundMigrations::Migration.table_name, :started_at)
38
+ migrations << "add_timestamps_to_background_migrations"
39
+ end
40
+
37
41
  migrations
38
42
  end
39
43
 
@@ -189,15 +189,23 @@ module OnlineMigrations
189
189
  #
190
190
  def retry
191
191
  if composite? && failed?
192
- children.failed.each(&:retry)
193
- enqueued!
192
+ transaction do
193
+ update!(status: :enqueued, finished_at: nil)
194
+ children.failed.each(&:retry)
195
+ end
196
+
194
197
  true
195
198
  elsif failed?
196
- iterator = BatchIterator.new(migration_jobs.failed)
197
- iterator.each_batch(of: 100) do |batch|
198
- batch.each(&:retry)
199
+ transaction do
200
+ parent.update!(status: :enqueued, finished_at: nil) if parent
201
+ update!(status: :enqueued, started_at: nil, finished_at: nil)
202
+
203
+ iterator = BatchIterator.new(migration_jobs.failed)
204
+ iterator.each_batch(of: 100) do |batch|
205
+ batch.each(&:retry)
206
+ end
199
207
  end
200
- enqueued!
208
+
201
209
  true
202
210
  else
203
211
  false
@@ -205,20 +213,6 @@ module OnlineMigrations
205
213
  end
206
214
  alias retry_failed_jobs retry
207
215
 
208
- # Returns the time this migration started running.
209
- def started_at
210
- # To be precise, we should get the minimum of `started_at` amongst the children jobs
211
- # (for simple migrations) and amongst the children migrations (for composite migrations).
212
- # But we do not have an appropriate index on the jobs table and using this will lead to
213
- # N+1 queries if used inside some dashboard, for example.
214
- created_at
215
- end
216
-
217
- # Returns the time this migration finished running.
218
- def finished_at
219
- updated_at if completed?
220
- end
221
-
222
216
  # @private
223
217
  def on_shard(&block)
224
218
  abstract_class = Utils.find_connection_class(migration_model)
@@ -229,9 +223,9 @@ module OnlineMigrations
229
223
 
230
224
  # @private
231
225
  def reset_failed_jobs_attempts
232
- iterator = BatchIterator.new(migration_jobs.failed.attempts_exceeded)
226
+ iterator = BatchIterator.new(migration_jobs.failed)
233
227
  iterator.each_batch(of: 100) do |relation|
234
- relation.update_all(attempts: 0)
228
+ relation.update_all(status: :enqueued, attempts: 0)
235
229
  end
236
230
  end
237
231
 
@@ -6,6 +6,7 @@ module OnlineMigrations
6
6
  STATUSES = [
7
7
  :enqueued,
8
8
  :running,
9
+ :errored,
9
10
  :failed,
10
11
  :succeeded,
11
12
  :cancelled,
@@ -13,7 +14,7 @@ module OnlineMigrations
13
14
 
14
15
  self.table_name = :background_migration_jobs
15
16
 
16
- scope :active, -> { where(status: [:enqueued, :running]) }
17
+ scope :active, -> { where(status: [:enqueued, :running, :errored]) }
17
18
  scope :completed, -> { where(status: [:failed, :succeeded]) }
18
19
  scope :stuck, -> do
19
20
  timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
@@ -21,14 +22,11 @@ module OnlineMigrations
21
22
  end
22
23
 
23
24
  scope :retriable, -> do
24
- failed_retriable = failed.where("attempts < max_attempts")
25
-
26
- stuck_sql = connection.unprepared_statement { stuck.to_sql }
27
- failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
25
+ stuck_sql = connection.unprepared_statement { stuck.to_sql }
28
26
 
29
27
  from(Arel.sql(<<~SQL))
30
28
  (
31
- (#{failed_retriable_sql})
29
+ (SELECT * FROM background_migration_jobs WHERE status = 'errored')
32
30
  UNION
33
31
  (#{stuck_sql})
34
32
  ) AS #{table_name}
@@ -36,7 +34,6 @@ module OnlineMigrations
36
34
  end
37
35
 
38
36
  scope :except_succeeded, -> { where.not(status: :succeeded) }
39
- scope :attempts_exceeded, -> { where("attempts >= max_attempts") }
40
37
 
41
38
  enum :status, STATUSES.index_with(&:to_s)
42
39
 
@@ -60,6 +57,10 @@ module OnlineMigrations
60
57
  running? && updated_at <= timeout.seconds.ago
61
58
  end
62
59
 
60
+ def attempts_exceeded?
61
+ attempts >= max_attempts
62
+ end
63
+
63
64
  # Mark this job as ready to be processed again.
64
65
  #
65
66
  # This is used when retrying failed jobs.
@@ -37,8 +37,10 @@ module OnlineMigrations
37
37
  rescue Exception => e # rubocop:disable Lint/RescueException
38
38
  backtrace_cleaner = ::OnlineMigrations.config.backtrace_cleaner
39
39
 
40
+ status = migration_job.attempts_exceeded? ? :failed : :errored
41
+
40
42
  migration_job.update!(
41
- status: :failed,
43
+ status: status,
42
44
  finished_at: Time.current,
43
45
  error_class: e.class.name,
44
46
  error_message: e.message,
@@ -6,7 +6,8 @@ module OnlineMigrations
6
6
  class MigrationJobStatusValidator < ActiveModel::Validator
7
7
  VALID_STATUS_TRANSITIONS = {
8
8
  "enqueued" => ["running", "cancelled"],
9
- "running" => ["succeeded", "failed", "cancelled"],
9
+ "running" => ["succeeded", "errored", "failed", "cancelled"],
10
+ "errored" => ["running", "failed", "cancelled"],
10
11
  "failed" => ["enqueued", "running", "cancelled"],
11
12
  }
12
13
 
@@ -34,9 +34,9 @@ module OnlineMigrations
34
34
  job_runner.run
35
35
  elsif !migration.migration_jobs.active.exists?
36
36
  if migration.migration_jobs.failed.exists?
37
- migration.failed!
37
+ migration.update!(status: :failed, finished_at: Time.current)
38
38
  else
39
- migration.succeeded!
39
+ migration.update!(status: :succeeded, finished_at: Time.current)
40
40
  end
41
41
 
42
42
  ActiveSupport::Notifications.instrument("completed.background_migrations", migration_payload)
@@ -100,8 +100,15 @@ module OnlineMigrations
100
100
  private
101
101
  def mark_as_running
102
102
  Migration.transaction do
103
- migration.running!
104
- migration.parent.running! if migration.parent && migration.parent.enqueued?
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
105
112
  end
106
113
  end
107
114
 
@@ -133,10 +140,10 @@ module OnlineMigrations
133
140
  parent.with_lock do
134
141
  children = parent.children.select(:status)
135
142
  if children.all?(&:succeeded?)
136
- parent.succeeded!
143
+ parent.update!(status: :succeeded, finished_at: Time.current)
137
144
  completed = true
138
145
  elsif children.any?(&:failed?)
139
- parent.failed!
146
+ parent.update!(status: :failed, finished_at: Time.current)
140
147
  completed = true
141
148
  end
142
149
  end
@@ -11,7 +11,8 @@ module OnlineMigrations
11
11
  STATUSES = [
12
12
  :enqueued, # The migration has been enqueued by the user.
13
13
  :running, # The migration is being performed by a migration executor.
14
- :failed, # The migration raises an exception when running.
14
+ :errored, # The migration raised an error during last run.
15
+ :failed, # The migration raises an error when running and retry attempts exceeded.
15
16
  :succeeded, # The migration finished without error.
16
17
  :cancelled, # The migration was cancelled by the user.
17
18
  ]
@@ -23,7 +24,7 @@ module OnlineMigrations
23
24
  scope :queue_order, -> { order(created_at: :asc) }
24
25
  scope :parents, -> { where(parent_id: nil) }
25
26
  scope :runnable, -> { where(composite: false) }
26
- scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
27
+ scope :active, -> { where(status: [:enqueued, :running, :errored]) }
27
28
  scope :except_succeeded, -> { where.not(status: :succeeded) }
28
29
 
29
30
  scope :stuck, -> do
@@ -33,14 +34,11 @@ module OnlineMigrations
33
34
  end
34
35
 
35
36
  scope :retriable, -> do
36
- failed_retriable = runnable.failed.where("attempts < max_attempts")
37
-
38
- stuck_sql = connection.unprepared_statement { stuck.to_sql }
39
- failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
37
+ stuck_sql = connection.unprepared_statement { stuck.to_sql }
40
38
 
41
39
  from(Arel.sql(<<~SQL))
42
40
  (
43
- (#{failed_retriable_sql})
41
+ (SELECT * FROM background_schema_migrations WHERE NOT composite AND status = 'errored')
44
42
  UNION
45
43
  (#{stuck_sql})
46
44
  ) AS #{table_name}
@@ -139,22 +137,27 @@ module OnlineMigrations
139
137
  #
140
138
  def retry
141
139
  if composite? && failed?
142
- children.failed.each(&:retry)
143
- update!(
144
- status: self.class.statuses[:enqueued],
145
- finished_at: nil
146
- )
140
+ transaction do
141
+ update!(status: :enqueued, finished_at: nil)
142
+ children.failed.each(&:retry)
143
+ end
144
+
147
145
  true
148
146
  elsif failed?
149
- update!(
150
- status: self.class.statuses[:enqueued],
151
- attempts: 0,
152
- started_at: nil,
153
- finished_at: nil,
154
- error_class: nil,
155
- error_message: nil,
156
- backtrace: nil
157
- )
147
+ transaction do
148
+ parent.update!(status: :enqueued, finished_at: nil) if parent
149
+
150
+ update!(
151
+ status: :enqueued,
152
+ attempts: 0,
153
+ started_at: nil,
154
+ finished_at: nil,
155
+ error_class: nil,
156
+ error_message: nil,
157
+ backtrace: nil
158
+ )
159
+ end
160
+
158
161
  true
159
162
  else
160
163
  false
@@ -191,10 +194,7 @@ module OnlineMigrations
191
194
  if index_addition?
192
195
  index = connection.indexes(table_name).find { |i| i.name == name }
193
196
  if index
194
- # Use index validity from https://github.com/rails/rails/pull/45160
195
- # when switching to ActiveRecord >= 7.1.
196
- schema = connection.send(:__schema_for_table, table_name)
197
- if connection.send(:__index_valid?, name, schema: schema)
197
+ if index.valid?
198
198
  return
199
199
  else
200
200
  connection.remove_index(table_name, name: name, algorithm: :concurrently)
@@ -203,6 +203,11 @@ module OnlineMigrations
203
203
  end
204
204
 
205
205
  connection.execute(definition)
206
+
207
+ # Outdated statistics + a new index can hurt performance of existing queries.
208
+ if OnlineMigrations.config.auto_analyze
209
+ connection.execute("ANALYZE #{table_name}")
210
+ end
206
211
  end
207
212
  end
208
213
  end
@@ -19,7 +19,7 @@ module OnlineMigrations
19
19
  return
20
20
  end
21
21
 
22
- if index_exists?(table_name, column_name, **options)
22
+ if index_exists?(table_name, column_name, name: index.name, **options)
23
23
  Utils.raise_or_say("Index creation was not enqueued because the index already exists.")
24
24
  return
25
25
  end
@@ -13,7 +13,7 @@ module OnlineMigrations
13
13
  def run
14
14
  return if migration.cancelled? || migration.succeeded?
15
15
 
16
- mark_as_running if migration.enqueued? || migration.failed?
16
+ mark_as_running if migration.enqueued? || migration.errored?
17
17
 
18
18
  if migration.composite?
19
19
  migration.children.each do |child_migration|
@@ -83,14 +83,18 @@ module OnlineMigrations
83
83
  rescue Exception => e # rubocop:disable Lint/RescueException
84
84
  backtrace_cleaner = ::OnlineMigrations.config.backtrace_cleaner
85
85
 
86
+ status = migration.attempts_exceeded? ? :failed : :errored
87
+
86
88
  migration.update!(
87
- status: :failed,
89
+ status: status,
88
90
  finished_at: Time.current,
89
91
  error_class: e.class.name,
90
92
  error_message: e.message,
91
93
  backtrace: backtrace_cleaner ? backtrace_cleaner.clean(e.backtrace) : e.backtrace
92
94
  )
93
95
 
96
+ complete_parent_if_needed(migration) if migration.parent.present?
97
+
94
98
  ::OnlineMigrations.config.background_schema_migrations.error_handler.call(e, migration)
95
99
  raise if Utils.run_background_migrations_inline?
96
100
  end
@@ -8,8 +8,12 @@ module OnlineMigrations
8
8
  # enqueued -> running occurs when the migration starts performing.
9
9
  "enqueued" => ["running", "cancelled"],
10
10
  # running -> succeeded occurs when the migration completes successfully.
11
- # running -> failed occurs when the migration raises an exception when running and retry attempts exceeded.
12
- "running" => ["succeeded", "failed", "cancelled"],
11
+ # running -> errored occurs when the migration raised an error during the last run.
12
+ # running -> failed occurs when the migration raises an error when running and retry attempts exceeded.
13
+ "running" => ["succeeded", "errored", "failed", "cancelled"],
14
+ # errored -> running occurs when previously errored migration starts running
15
+ # errored -> failed occurs when the migration raises an error when running and retry attempts exceeded.
16
+ "errored" => ["running", "failed", "cancelled"],
13
17
  # failed -> enqueued occurs when the failed migration is enqueued to be retried.
14
18
  # failed -> running occurs when the failed migration is retried.
15
19
  "failed" => ["enqueued", "running", "cancelled"],