online_migrations 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
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