online_migrations 0.24.0 → 0.25.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99170e244f0f008e8d097222c88a96555692aa5afc136087d03341866ed8412c
4
- data.tar.gz: 6068a722274c73d8a0ab3c1bed4d1f2673cd99d15ca77c24f7fff5af75e77129
3
+ metadata.gz: f6b7685527d71319ba50e3a5c29744d9ef28103c1cacfdc650cba12c276782d8
4
+ data.tar.gz: e10e0d1dfb1c807b05927189f682633a7ab17c53f37974c5df5a2b452d78ab92
5
5
  SHA512:
6
- metadata.gz: 9d2a127aee978d36676d8bdd85352487f66a14289cca28173eb1126aee65ceeea5dda067171ffb320b7c0cb2be903712ab158af12312452180b8e56da1e7b641
7
- data.tar.gz: 305f89162f87a3746d91bfdfacae00b7d0f2a374f27755c846cd4386d65694442b70fc6963a7dec8845c192eca2cc625bb020c7c4aec664320a395b124bb3e71
6
+ metadata.gz: d7c62d15765a2837cf0dcba5e9a0c63c89ecc73f33fbd5500284184a78027f80a4ad12d980b5ac5de8235550384bd123708a075adab0243a06202154348268a3
7
+ data.tar.gz: 24449c58e1bec9b3a31e925a27df17c56c7cd923f2cdd33a5d6781f1f253204776901e92fd4024d3340e4eeb533b043a71a1afeaacb046bcc6ea62d5e5444668
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.25.0 (2025-02-03)
4
+
5
+ - Track start/finish time of background data migrations
6
+
7
+ Note: Make sure to run `bin/rails generate online_migrations:upgrade` if using background migrations.
8
+
9
+ - Add new state for errored background migrations
10
+
11
+ * **errored** - migration raised an error during last run
12
+ * **failed** - migration raises an error when running and retry attempts exceeded
13
+
14
+ - Fix thread safety issue for lock retrier
15
+
16
+ Note: Lock retrier changed its API (`LockRetrier#connection` accessor was removed and
17
+ `LockRetrier#with_lock_retries` now accepts a connection argument). This change might be of interest
18
+ if you implemented a custom lock retrier class.
19
+
3
20
  ## 0.24.0 (2025-01-20)
4
21
 
5
22
  - Add ability to run a separate background migrations scheduler per shard
@@ -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
 
@@ -0,0 +1,31 @@
1
+ class AddTimestampsToBackgroundMigrations < <%= migration_parent %>
2
+ def change
3
+ safety_assured 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
@@ -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
@@ -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"],
@@ -46,10 +46,6 @@ module OnlineMigrations
46
46
  # end
47
47
  #
48
48
  class LockRetrier
49
- # Database connection on which retries are run
50
- #
51
- attr_accessor :connection
52
-
53
49
  # Returns the number of retrying attempts
54
50
  #
55
51
  def attempts
@@ -73,14 +69,15 @@ module OnlineMigrations
73
69
  # Executes the block with a retry mechanism that alters the `lock_timeout`
74
70
  # and sleep time between attempts.
75
71
  #
72
+ # @param connection The connection on which to retry lock timeouts
76
73
  # @return [void]
77
74
  #
78
75
  # @example
79
- # retrier.with_lock_retries do
76
+ # retrier.with_lock_retries(connection) do
80
77
  # add_column(:users, :name, :string)
81
78
  # end
82
79
  #
83
- def with_lock_retries(&block)
80
+ def with_lock_retries(connection, &block)
84
81
  return yield if lock_retries_disabled?
85
82
 
86
83
  current_attempt = 0
@@ -90,7 +87,7 @@ module OnlineMigrations
90
87
 
91
88
  current_lock_timeout = lock_timeout(current_attempt)
92
89
  if current_lock_timeout
93
- with_lock_timeout(current_lock_timeout.in_milliseconds, &block)
90
+ with_lock_timeout(connection, current_lock_timeout.in_milliseconds, &block)
94
91
  else
95
92
  yield
96
93
  end
@@ -110,7 +107,7 @@ module OnlineMigrations
110
107
  Utils.to_bool(ENV["DISABLE_LOCK_RETRIES"])
111
108
  end
112
109
 
113
- def with_lock_timeout(value)
110
+ def with_lock_timeout(connection, value)
114
111
  value = value.ceil.to_i
115
112
  prev_value = connection.select_value("SHOW lock_timeout")
116
113
  connection.execute("SET lock_timeout TO #{connection.quote("#{value}ms")}")
@@ -234,7 +231,7 @@ module OnlineMigrations
234
231
  def delay(*)
235
232
  end
236
233
 
237
- def with_lock_retries
234
+ def with_lock_retries(_connection)
238
235
  yield
239
236
  end
240
237
  end
@@ -892,14 +892,10 @@ module OnlineMigrations
892
892
  views = self.views
893
893
 
894
894
  table_renames = OnlineMigrations.config.table_renames
895
- renamed_tables = table_renames.select do |old_name, _|
896
- views.include?(old_name)
897
- end
895
+ renamed_tables = table_renames.slice(*views)
898
896
 
899
897
  column_renames = OnlineMigrations.config.column_renames
900
- renamed_columns = column_renames.select do |table_name, _|
901
- views.include?(table_name)
902
- end
898
+ renamed_columns = column_renames.slice(*views)
903
899
 
904
900
  if renamed_tables.key?(table)
905
901
  super(renamed_tables[table])
@@ -919,8 +915,7 @@ module OnlineMigrations
919
915
  __ensure_not_in_transaction!
920
916
 
921
917
  retrier = OnlineMigrations.config.lock_retrier
922
- retrier.connection = self
923
- retrier.with_lock_retries(&block)
918
+ retrier.with_lock_retries(self, &block)
924
919
  end
925
920
 
926
921
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.24.0"
4
+ VERSION = "0.25.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-20 00:00:00.000000000 Z
11
+ date: 2025-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -40,6 +40,7 @@ files:
40
40
  - lib/generators/online_migrations/background_migration_generator.rb
41
41
  - lib/generators/online_migrations/install_generator.rb
42
42
  - lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt
43
+ - lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt
43
44
  - lib/generators/online_migrations/templates/background_data_migration.rb.tt
44
45
  - lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt
45
46
  - lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt