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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a84bdeb495a3d25432c6ae97d4c46777517ae3bbcf01359020d400ad8f16f30
4
- data.tar.gz: 935d3b38b096fcdb222a89065d7c10023a8b85a6009065d73a1df9bb92c2b0f8
3
+ metadata.gz: c3d28d3f9893b7c26240f94069032fa65ede6b14a1fb79b8c205111b8eedd934
4
+ data.tar.gz: da807a14d87705650c05bb7d3151129c534a3c50344b60d0e8e865cdecaa04b5
5
5
  SHA512:
6
- metadata.gz: f2c578a25081bb86cafc2851cb818c40e819363d40bdac2a6f4cde634f71a8dc3a90909e4dbf10bf0fb6df61e1d979f4d581982650eec3834715baa8e5888832
7
- data.tar.gz: 01377b6bcdbe0b601a45b53a52a99746e443d60c5ca513e2312322c81718e4a995fde32ee78a45554fe155878391f6a885fdef50bdea4d4619a4e10799bab295
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
@@ -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
@@ -57,6 +57,8 @@ module OnlineMigrations
57
57
  migrations << "background_migrations_change_status_default"
58
58
  end
59
59
 
60
+ migrations << "background_data_migrations_convert_cursor"
61
+
60
62
  migrations
61
63
  end
62
64
 
@@ -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
- # running, cancelling or pausing, and not having been updated in the last 5 minutes.
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
- (running? || cancelling? || pausing?) && updated_at <= stuck_timeout.ago
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!
@@ -72,6 +72,7 @@ module OnlineMigrations
72
72
  # Job was manually cancelled.
73
73
  @migration.cancel if cancelled?
74
74
 
75
+ @migration.update!(jid: nil)
75
76
  @migration.complete
76
77
  end
77
78
 
@@ -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
- stuck_migrations, active_migrations = relation.running.partition(&:stuck?)
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 - active_migrations.count
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 (is running for some configured time).
84
+ # Whether the migration is considered stuck.
84
85
  #
85
86
  def stuck?
86
- if index_addition?
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 either be safely
48
- # manually retried or will be picked up in the future by scheduler when it decides that
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
- stuck_migrations, active_migrations = Migration.running.partition(&:stuck?)
39
- runnable_migrations = (Migration.pending + Migration.errored + stuck_migrations).sort_by(&:created_at)
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.sort_by(&:name)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.35.0"
4
+ VERSION = "0.36.1"
5
5
  end
@@ -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
- ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
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.35.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.14
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