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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +18 -73
- data/docs/background_schema_migrations.md +2 -1
- data/docs/configuring.md +8 -0
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +31 -0
- data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/initializer.rb.tt +3 -0
- data/lib/generators/online_migrations/templates/install_migration.rb.tt +2 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +2 -2
- data/lib/generators/online_migrations/upgrade_generator.rb +4 -0
- data/lib/online_migrations/background_migrations/migration.rb +16 -22
- data/lib/online_migrations/background_migrations/migration_job.rb +8 -7
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +3 -1
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +2 -1
- data/lib/online_migrations/background_migrations/migration_runner.rb +13 -6
- data/lib/online_migrations/background_schema_migrations/migration.rb +30 -25
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +1 -1
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +6 -2
- data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +6 -2
- data/lib/online_migrations/batch_iterator.rb +7 -4
- data/lib/online_migrations/change_column_type_helpers.rb +72 -12
- data/lib/online_migrations/command_checker.rb +32 -20
- data/lib/online_migrations/config.rb +8 -0
- data/lib/online_migrations/error_messages.rb +16 -0
- data/lib/online_migrations/index_definition.rb +1 -1
- data/lib/online_migrations/lock_retrier.rb +6 -9
- data/lib/online_migrations/migration.rb +8 -1
- data/lib/online_migrations/schema_cache.rb +0 -78
- data/lib/online_migrations/schema_statements.rb +20 -81
- data/lib/online_migrations/utils.rb +1 -20
- data/lib/online_migrations/verbose_sql_logs.rb +1 -7
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +1 -8
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cbca384d5eaf4ab575e80c60160d42326ef86f1baf21419f290663e0d9698eb0
|
4
|
+
data.tar.gz: b90abf4e278ee1b2ab27e60472f6bb127b124797084f53205a0929a5cc98268b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
20
|
-
- Rails 7.
|
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
|
-
|
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
|
-
|
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
|
-
|
75
|
+
1. Ignore the column:
|
126
76
|
|
127
|
-
class
|
128
|
-
|
77
|
+
class User < ApplicationRecord
|
78
|
+
self.ignored_columns += ["name"]
|
79
|
+
end
|
129
80
|
|
130
|
-
|
131
|
-
|
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
|
-
|
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
|
-
* **
|
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:
|
@@ -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 %>"
|
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 %>"
|
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
|
-
|
193
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
(
|
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:
|
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
|
-
|
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
|
-
:
|
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: [
|
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
|
-
|
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
|
-
(
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
150
|
-
status:
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
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.
|
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:
|
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 ->
|
12
|
-
|
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"],
|