online_migrations 0.35.0 → 0.36.1
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 +14 -0
- data/README.md +2 -0
- data/lib/generators/online_migrations/templates/background_data_migrations_convert_cursor.rb.tt +9 -0
- data/lib/generators/online_migrations/upgrade_generator.rb +2 -0
- data/lib/online_migrations/background_data_migrations/migration.rb +13 -4
- data/lib/online_migrations/background_data_migrations/migration_job.rb +1 -0
- data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +0 -2
- data/lib/online_migrations/background_data_migrations/scheduler.rb +2 -3
- data/lib/online_migrations/background_schema_migrations/migration.rb +3 -7
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +2 -3
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
- data/lib/online_migrations/command_recorder.rb +153 -0
- data/lib/online_migrations/schema_dumper.rb +7 -1
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +6 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3d28d3f9893b7c26240f94069032fa65ede6b14a1fb79b8c205111b8eedd934
|
|
4
|
+
data.tar.gz: da807a14d87705650c05bb7d3151129c534a3c50344b60d0e8e865cdecaa04b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8101340c889e837247992f15201b6d1065ae9ea00493f245aad1fe092a7dd2def02965882a5cd29acb9af21504959fd8bbfd95c0f48f911af284ae8fa87baee
|
|
7
|
+
data.tar.gz: 916c40fec0d7bfe7d08d7402e13ad2b56989921074f566474aeef0280579064ea92377a5b621c427b64205ecf098f3868c104be5772e3a5dd76b68a76f1a16e0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## master (unreleased)
|
|
2
2
|
|
|
3
|
+
## 0.36.1 (2026-09-23)
|
|
4
|
+
|
|
5
|
+
- Fix compatibility with future Rails 8.2
|
|
6
|
+
- Fix pausing already paused background data migrations
|
|
7
|
+
|
|
8
|
+
## 0.36.0 (2026-07-20)
|
|
9
|
+
|
|
10
|
+
- Serialize to JSON `cursor` value for background data migrations
|
|
11
|
+
|
|
12
|
+
Note: Run `bin/rails generate online_migrations:upgrade` if using background migrations.
|
|
13
|
+
|
|
14
|
+
- Do not consider running background migrations as stuck
|
|
15
|
+
- Do not automatically retry stuck background migrations
|
|
16
|
+
|
|
3
17
|
## 0.35.0 (2026-07-01)
|
|
4
18
|
|
|
5
19
|
- Drop support for Ruby < 3.3
|
data/README.md
CHANGED
|
@@ -342,6 +342,8 @@ during writes works automatically). For most column type changes, this does not
|
|
|
342
342
|
|
|
343
343
|
```ruby
|
|
344
344
|
class CleanupChangeFilesSizeType < ActiveRecord::Migration[8.0]
|
|
345
|
+
disable_ddl_transaction!
|
|
346
|
+
|
|
345
347
|
def up
|
|
346
348
|
cleanup_column_type_change :files, :size
|
|
347
349
|
end
|
data/lib/generators/online_migrations/templates/background_data_migrations_convert_cursor.rb.tt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class BackgroundDataMigrationsConvertCursor < <%= migration_parent %>
|
|
2
|
+
def change
|
|
3
|
+
up_only do
|
|
4
|
+
OnlineMigrations::DataMigrations::Migration.find_each do |migration|
|
|
5
|
+
OnlineMigrations::DataMigrations::Migration.where(id: migration.id).update_all(cursor: migration.cursor)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -52,6 +52,8 @@ module OnlineMigrations
|
|
|
52
52
|
|
|
53
53
|
enum :status, STATUSES.index_with(&:to_s)
|
|
54
54
|
|
|
55
|
+
serialize :cursor, coder: JSON
|
|
56
|
+
|
|
55
57
|
validates :migration_name, presence: true
|
|
56
58
|
validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
|
|
57
59
|
|
|
@@ -66,6 +68,13 @@ module OnlineMigrations
|
|
|
66
68
|
migration_name.sub(/^(::)?#{namespace}::/, "")
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# TODO: delete in some future version
|
|
72
|
+
def cursor
|
|
73
|
+
self[:cursor]
|
|
74
|
+
rescue JSON::ParserError
|
|
75
|
+
cursor_before_type_cast
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def migration_name=(class_name)
|
|
70
79
|
class_name = class_name.name if class_name.is_a?(Class)
|
|
71
80
|
write_attribute(:migration_name, self.class.normalize_migration_name(class_name))
|
|
@@ -111,13 +120,13 @@ module OnlineMigrations
|
|
|
111
120
|
end
|
|
112
121
|
|
|
113
122
|
# Returns whether a migration is stuck, which is defined as having a status of
|
|
114
|
-
#
|
|
123
|
+
# cancelling or pausing, and not having been updated in the last 5 minutes.
|
|
115
124
|
#
|
|
116
125
|
# @return [Boolean] whether the migration is stuck.
|
|
117
126
|
#
|
|
118
127
|
def stuck?
|
|
119
128
|
stuck_timeout = OnlineMigrations.config.background_data_migrations.stuck_timeout
|
|
120
|
-
(
|
|
129
|
+
(cancelling? || pausing?) && updated_at <= stuck_timeout.ago
|
|
121
130
|
end
|
|
122
131
|
|
|
123
132
|
# @private
|
|
@@ -168,12 +177,12 @@ module OnlineMigrations
|
|
|
168
177
|
true
|
|
169
178
|
end
|
|
170
179
|
|
|
171
|
-
# Pause this data migration. No-op if migration is completed.
|
|
180
|
+
# Pause this data migration. No-op if migration is completed or already paused.
|
|
172
181
|
#
|
|
173
182
|
# @return [Boolean] whether this data migration was paused.
|
|
174
183
|
#
|
|
175
184
|
def pause
|
|
176
|
-
return false if completed?
|
|
185
|
+
return false if paused? || completed?
|
|
177
186
|
|
|
178
187
|
if pending? || enqueued? || delayed? || stuck? || errored?
|
|
179
188
|
paused!
|
|
@@ -13,14 +13,12 @@ module OnlineMigrations
|
|
|
13
13
|
# enqueued -> failed occurs when the migration job fails to be enqueued, or
|
|
14
14
|
# if the migration is deleted before is starts running.
|
|
15
15
|
"enqueued" => ["running", "paused", "cancelled", "failed"],
|
|
16
|
-
# running -> enqueued occurs when the migration is stuck and rescheduled by the scheduler.
|
|
17
16
|
# running -> succeeded occurs when the migration completes successfully.
|
|
18
17
|
# running -> pausing occurs when a user pauses the migration as it's performing.
|
|
19
18
|
# running -> cancelling occurs when a user cancels the migration as it's performing.
|
|
20
19
|
# running -> errored occurs when the migration raised an error during the last run.
|
|
21
20
|
# running -> failed occurs when the migration raises an error when running and retry attempts exceeded.
|
|
22
21
|
"running" => [
|
|
23
|
-
"enqueued",
|
|
24
22
|
"succeeded",
|
|
25
23
|
"pausing",
|
|
26
24
|
"cancelling",
|
|
@@ -36,11 +36,10 @@ module OnlineMigrations
|
|
|
36
36
|
migrations_to_enqueue = []
|
|
37
37
|
|
|
38
38
|
with_lock do
|
|
39
|
-
|
|
40
|
-
runnable_migrations = migrations_with_existing_classes(relation.pending) + stuck_migrations
|
|
39
|
+
runnable_migrations = migrations_with_existing_classes(relation.pending)
|
|
41
40
|
|
|
42
41
|
# Ensure no more than 'concurrency' migrations are running at the same time.
|
|
43
|
-
remaining_to_enqueue = concurrency -
|
|
42
|
+
remaining_to_enqueue = concurrency - relation.running.count
|
|
44
43
|
if remaining_to_enqueue > 0
|
|
45
44
|
runnable_migrations.take(remaining_to_enqueue).each do |migration|
|
|
46
45
|
migration.update!(status: :enqueued)
|
|
@@ -23,6 +23,7 @@ module OnlineMigrations
|
|
|
23
23
|
MAX_IDENTIFIER_LENGTH = 63
|
|
24
24
|
|
|
25
25
|
self.table_name = :background_schema_migrations
|
|
26
|
+
self.ignored_columns += ["parent_id", "composite"]
|
|
26
27
|
|
|
27
28
|
scope :queue_order, -> { order(created_at: :asc) }
|
|
28
29
|
scope :active, -> { where(status: [:pending, :running, :errored]) }
|
|
@@ -80,15 +81,10 @@ module OnlineMigrations
|
|
|
80
81
|
def progress
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
# Whether the migration is considered stuck
|
|
84
|
+
# Whether the migration is considered stuck.
|
|
84
85
|
#
|
|
85
86
|
def stuck?
|
|
86
|
-
|
|
87
|
-
running? && !index_build_in_progress?
|
|
88
|
-
else
|
|
89
|
-
stuck_timeout = (statement_timeout || 1.day) + 10.minutes
|
|
90
|
-
running? && updated_at <= stuck_timeout.seconds.ago
|
|
91
|
-
end
|
|
87
|
+
index_addition? && running? && !index_build_in_progress?
|
|
92
88
|
end
|
|
93
89
|
|
|
94
90
|
# Mark this migration as ready to be processed again.
|
|
@@ -44,9 +44,8 @@ module OnlineMigrations
|
|
|
44
44
|
# Background schema migrations could take a while to run. It is possible, that the process
|
|
45
45
|
# never reaches this (or the rescue below) line of code. E.g., when it is force quitted
|
|
46
46
|
# (SIGKILL etc.) and so the migration will end up in the "running" state and the query is
|
|
47
|
-
# still executing (or already finished) in the database. This migration can
|
|
48
|
-
# manually retried
|
|
49
|
-
# this migration is stuck.
|
|
47
|
+
# still executing (or already finished) in the database. This migration can be safely
|
|
48
|
+
# manually retried.
|
|
50
49
|
|
|
51
50
|
migration.update!(status: :succeeded, finished_at: Time.current)
|
|
52
51
|
|
|
@@ -35,8 +35,8 @@ module OnlineMigrations
|
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
def find_migration(**options)
|
|
38
|
-
|
|
39
|
-
runnable_migrations = (Migration.pending + Migration.errored
|
|
38
|
+
active_migrations = Migration.running.to_a
|
|
39
|
+
runnable_migrations = (Migration.pending + Migration.errored).sort_by(&:created_at)
|
|
40
40
|
|
|
41
41
|
if options.key?(:shard)
|
|
42
42
|
runnable_migrations = runnable_migrations.select { |migration| migration.shard.to_s == options[:shard].to_s }
|
|
@@ -153,4 +153,157 @@ module OnlineMigrations
|
|
|
153
153
|
[:add_text_limit_constraint, args]
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
|
+
|
|
157
|
+
# @private
|
|
158
|
+
# New format was introduced in https://github.com/rails/rails/pull/58239.
|
|
159
|
+
module CommandRecorder82
|
|
160
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS = [
|
|
161
|
+
:update_column_in_batches,
|
|
162
|
+
:initialize_column_rename,
|
|
163
|
+
:initialize_columns_rename,
|
|
164
|
+
:revert_initialize_column_rename,
|
|
165
|
+
:revert_initialize_columns_rename,
|
|
166
|
+
:finalize_column_rename,
|
|
167
|
+
:finalize_columns_rename,
|
|
168
|
+
:revert_finalize_column_rename,
|
|
169
|
+
:revert_finalize_columns_rename,
|
|
170
|
+
:initialize_table_rename,
|
|
171
|
+
:revert_initialize_table_rename,
|
|
172
|
+
:finalize_table_rename,
|
|
173
|
+
:revert_finalize_table_rename,
|
|
174
|
+
:swap_column_names,
|
|
175
|
+
:add_column_with_default,
|
|
176
|
+
:add_not_null_constraint,
|
|
177
|
+
:remove_not_null_constraint,
|
|
178
|
+
:add_text_limit_constraint,
|
|
179
|
+
:remove_text_limit_constraint,
|
|
180
|
+
:add_reference_concurrently,
|
|
181
|
+
:change_column_type_in_background,
|
|
182
|
+
:enqueue_background_data_migration,
|
|
183
|
+
:remove_background_data_migration,
|
|
184
|
+
|
|
185
|
+
# column type change helpers
|
|
186
|
+
:initialize_column_type_change,
|
|
187
|
+
:initialize_columns_type_change,
|
|
188
|
+
:revert_initialize_column_type_change,
|
|
189
|
+
:revert_initialize_columns_type_change,
|
|
190
|
+
:backfill_column_for_type_change,
|
|
191
|
+
:backfill_columns_for_type_change,
|
|
192
|
+
:finalize_column_type_change,
|
|
193
|
+
:finalize_columns_type_change,
|
|
194
|
+
:revert_finalize_column_type_change,
|
|
195
|
+
:cleanup_column_type_change,
|
|
196
|
+
:cleanup_columns_type_change,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS.each do |method|
|
|
200
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
201
|
+
def #{method}(*args, **kwargs, &block) # def create_table(*args, **kwargs, &block)
|
|
202
|
+
record(:"#{method}", args, kwargs, &block) # record(:create_table, args, kwargs, &block)
|
|
203
|
+
end # end
|
|
204
|
+
RUBY
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
module StraightReversions
|
|
209
|
+
{
|
|
210
|
+
initialize_column_rename: :revert_initialize_column_rename,
|
|
211
|
+
initialize_columns_rename: :revert_initialize_columns_rename,
|
|
212
|
+
finalize_column_rename: :revert_finalize_column_rename,
|
|
213
|
+
finalize_columns_rename: :revert_finalize_columns_rename,
|
|
214
|
+
initialize_table_rename: :revert_initialize_table_rename,
|
|
215
|
+
finalize_table_rename: :revert_finalize_table_rename,
|
|
216
|
+
add_not_null_constraint: :remove_not_null_constraint,
|
|
217
|
+
initialize_column_type_change: :revert_initialize_column_type_change,
|
|
218
|
+
initialize_columns_type_change: :revert_initialize_columns_type_change,
|
|
219
|
+
finalize_column_type_change: :revert_finalize_column_type_change,
|
|
220
|
+
finalize_columns_type_change: :revert_finalize_columns_type_change,
|
|
221
|
+
}.each do |cmd, inv|
|
|
222
|
+
[[inv, cmd], [cmd, inv]].each do |method, inverse|
|
|
223
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
224
|
+
def invert_#{method}(args, kwargs, &block) # def invert_create_table(args, kwargs, &block)
|
|
225
|
+
[:#{inverse}, args, kwargs, block] # [:drop_table, args, kwargs, block]
|
|
226
|
+
end # end
|
|
227
|
+
RUBY
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
include StraightReversions
|
|
233
|
+
|
|
234
|
+
def invert_add_reference_concurrently(args, kwargs)
|
|
235
|
+
[:remove_reference, args, kwargs]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def invert_swap_column_names(args, kwargs)
|
|
239
|
+
table_name, column1, column2 = args
|
|
240
|
+
[:swap_column_names, [table_name, column2, column1], kwargs]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def invert_add_column_with_default(args, kwargs)
|
|
244
|
+
table_name, column_name, = args
|
|
245
|
+
[:remove_column, [table_name, column_name], kwargs]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def invert_revert_initialize_column_rename(args, kwargs)
|
|
249
|
+
_table, column, new_column = args
|
|
250
|
+
if !column || !new_column
|
|
251
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
252
|
+
"revert_initialize_column_rename is only reversible if given a column and new_column."
|
|
253
|
+
end
|
|
254
|
+
[:initialize_column_rename, args, kwargs]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def invert_revert_initialize_columns_rename(args, kwargs)
|
|
258
|
+
_table, old_new_column_hash = args
|
|
259
|
+
if !old_new_column_hash
|
|
260
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
261
|
+
"revert_initialize_columns_rename is only reversible if given a hash of old and new columns."
|
|
262
|
+
end
|
|
263
|
+
[:initialize_columns_rename, args, kwargs]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def invert_finalize_table_rename(args, kwargs)
|
|
267
|
+
_table_name, new_name = args
|
|
268
|
+
if !new_name
|
|
269
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
270
|
+
"finalize_table_rename is only reversible if given a new_name."
|
|
271
|
+
end
|
|
272
|
+
[:revert_finalize_table_rename, args, kwargs]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def invert_revert_initialize_column_type_change(args, kwargs)
|
|
276
|
+
if !args[2]
|
|
277
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
278
|
+
"revert_initialize_column_type_change is only reversible if given a new_type."
|
|
279
|
+
end
|
|
280
|
+
super
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def invert_revert_initialize_columns_type_change(args, kwargs)
|
|
284
|
+
if args[1].empty?
|
|
285
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
286
|
+
"revert_initialize_columns_type_change is only reversible if given a columns_and_types."
|
|
287
|
+
end
|
|
288
|
+
super
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def invert_add_not_null_constraint(args, kwargs)
|
|
292
|
+
kwargs.delete(:validate)
|
|
293
|
+
[:remove_not_null_constraint, args, kwargs]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def invert_add_text_limit_constraint(args, kwargs)
|
|
297
|
+
kwargs.delete(:validate)
|
|
298
|
+
[:remove_text_limit_constraint, args, kwargs]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def invert_remove_text_limit_constraint(args, kwargs)
|
|
302
|
+
if !args[2]
|
|
303
|
+
raise ActiveRecord::IrreversibleMigration, "remove_text_limit_constraint is only reversible if given a limit."
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
[:add_text_limit_constraint, args, kwargs]
|
|
307
|
+
end
|
|
308
|
+
end
|
|
156
309
|
end
|
|
@@ -15,7 +15,13 @@ module OnlineMigrations
|
|
|
15
15
|
|
|
16
16
|
class WrappedConnection < SimpleDelegator
|
|
17
17
|
def columns(table_name)
|
|
18
|
-
super
|
|
18
|
+
columns = super
|
|
19
|
+
|
|
20
|
+
if Utils.ar_version >= 8.2 && columns.is_a?(Hash)
|
|
21
|
+
columns.transform_values { |v| v.sort_by(&:name) }
|
|
22
|
+
else
|
|
23
|
+
columns.sort_by(&:name)
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/online_migrations.rb
CHANGED
|
@@ -119,7 +119,12 @@ module OnlineMigrations
|
|
|
119
119
|
ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
|
|
120
120
|
|
|
121
121
|
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(OnlineMigrations::DatabaseTasks)
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
if Utils.ar_version >= 8.2
|
|
124
|
+
ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder82)
|
|
125
|
+
else
|
|
126
|
+
ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
|
|
127
|
+
end
|
|
123
128
|
|
|
124
129
|
if !ActiveRecord::Batches::BatchEnumerator.method_defined?(:use_ranges)
|
|
125
130
|
ActiveRecord::Batches::BatchEnumerator.include(OnlineMigrations::ActiveRecordBatchEnumerator)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: online_migrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.36.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fatkodima
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt
|
|
42
42
|
- lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt
|
|
43
43
|
- lib/generators/online_migrations/templates/background_data_migrations_add_iteration_pause.rb.tt
|
|
44
|
+
- lib/generators/online_migrations/templates/background_data_migrations_convert_cursor.rb.tt
|
|
44
45
|
- lib/generators/online_migrations/templates/background_data_migrations_remove_iteration_pause_default.rb.tt
|
|
45
46
|
- lib/generators/online_migrations/templates/background_migrations_change_status_default.rb.tt
|
|
46
47
|
- lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt
|
|
@@ -115,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
116
|
- !ruby/object:Gem::Version
|
|
116
117
|
version: '0'
|
|
117
118
|
requirements: []
|
|
118
|
-
rubygems_version: 4.0.
|
|
119
|
+
rubygems_version: 4.0.16
|
|
119
120
|
specification_version: 4
|
|
120
121
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|
|
121
122
|
production
|