online_migrations 0.17.1 → 0.18.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: e9822bc892c205f69f4dba067e5b3b096c1a2950e36a40800411aa30e23cdf95
4
- data.tar.gz: 3756a3c7f274370411372ce08ffd37fde5ef7e930b0c98c57d1185e853e400af
3
+ metadata.gz: 31bbf9d9c3a619e7a878717b8d4bcffae2e71e819bd4d2ea140495fdbb951452
4
+ data.tar.gz: f9a92ef118577b4bd2e837c95687d19a6d69fba988e61625f51bf2807a855e62
5
5
  SHA512:
6
- metadata.gz: bce2d08b3126bfe68cfe85535f6d0efcdbdeea37843bdab281eb8330a5279a60df7dc63b904aa58d26c7a6c6f617103896a1478d4197127511632c151f0ed335
7
- data.tar.gz: ffd7307d6e042188199991abb61efa93d53e72f8f1653349863167f6e8cd7df4d90fafe2113c76ac032bb9b32274b5f512ccda36840289c99b10ef3bab80df9a
6
+ metadata.gz: b786ffdeb3d57f72d2582a222915b21c2c09f2755b049d99b9faf68fd1fe5a0655a577b37bb427473af9e24c564b5db825051da86a87e82787c29122c7e5053a
7
+ data.tar.gz: 33ec438be94ff7b69dee15770a730e6d946b2e1ccf0fb71a3be8819690701b7057d4ba4d5f4b297810265be83225f78b5b7e990b91e5affc35d5961a807ffc93
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.18.0 (2024-05-07)
4
+
5
+ - Fix setting `started_at`/`finished_at` for parents of sharded background schema migrations
6
+ - Improve retrying of failed sharded background migrations
7
+ - Fix a bug when retried background data migration can not start
8
+ - Do not run multiple background schema migrations on the same table at the same time
9
+
3
10
  ## 0.17.1 (2024-04-28)
4
11
 
5
12
  - Fix raising in development when using sharding and background index creation/removal was not enqueued
@@ -247,6 +247,17 @@ migration.progress # value from 0 to 100.0
247
247
 
248
248
  **Note**: It will be easier to work with background migrations through some kind of Web UI, but until it is implemented, we can work with them only manually.
249
249
 
250
+ ## Retrying a failed migration
251
+
252
+ To retry a failed migration, run:
253
+
254
+ ```ruby
255
+ migration = OnlineMigrations::BackgroundMigrations::Migration.find(id)
256
+ migration.retry # => `true` if scheduled to be retried, `false` - if not
257
+ ```
258
+
259
+ The migration will be retried on the next Scheduler run.
260
+
250
261
  ## Configuring
251
262
 
252
263
  There are a few configurable options for the Background Migrations. Custom configurations should be placed in a `online_migrations.rb` initializer.
@@ -66,6 +66,17 @@ end
66
66
 
67
67
  You shouldn't depend on the schema until the background schema migration is finished. If having the schema migrated is a requirement, then the `ensure_background_schema_migration_succeeded` helper can be used to guarantee that the migration succeeded and the schema change applied.
68
68
 
69
+ ## Retrying a failed migration
70
+
71
+ To retry a failed migration, run:
72
+
73
+ ```ruby
74
+ migration = OnlineMigrations::BackgroundSchemaMigrations::Migration.find(id)
75
+ migration.retry # => `true` if scheduled to be retried, `false` - if not
76
+ ```
77
+
78
+ The migration will be retried on the next Scheduler run.
79
+
69
80
  ## Instrumentation
70
81
 
71
82
  Background schema migrations use the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API.
@@ -38,9 +38,9 @@ module OnlineMigrations
38
38
  enum status: STATUSES.index_with(&:to_s)
39
39
  end
40
40
 
41
- belongs_to :parent, class_name: name, optional: true
42
- has_many :children, class_name: name, foreign_key: :parent_id, dependent: :delete_all
43
- has_many :migration_jobs, dependent: :delete_all
41
+ belongs_to :parent, class_name: name, optional: true, inverse_of: :children
42
+ has_many :children, class_name: name, foreign_key: :parent_id, dependent: :delete_all, inverse_of: :parent
43
+ has_many :migration_jobs, dependent: :delete_all, inverse_of: :migration
44
44
 
45
45
  validates :migration_name, :batch_column_name, presence: true
46
46
 
@@ -102,10 +102,6 @@ module OnlineMigrations
102
102
  migration_jobs.order(:max_value).last
103
103
  end
104
104
 
105
- def last_completed_job
106
- migration_jobs.completed.order(:finished_at).last
107
- end
108
-
109
105
  # Returns the progress of the background migration.
110
106
  #
111
107
  # @return [Float, nil]
@@ -115,10 +111,13 @@ module OnlineMigrations
115
111
  def progress
116
112
  if succeeded?
117
113
  100.0
114
+ elsif enqueued?
115
+ 0.0
118
116
  elsif composite?
119
117
  rows_counts = children.to_a.pluck(:rows_count)
120
118
  if rows_counts.none?(nil)
121
119
  total_rows_count = rows_counts.sum
120
+ return 100.0 if total_rows_count == 0
122
121
 
123
122
  progresses = children.map do |child|
124
123
  child.progress * child.rows_count / total_rows_count # weighted progress
@@ -126,11 +125,15 @@ module OnlineMigrations
126
125
 
127
126
  progresses.sum.round(2)
128
127
  end
129
- elsif rows_count && rows_count > 0
130
- jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
131
- # The last migration job may need to process the amount of rows
132
- # less than the batch size, so we can get a value > 1.0.
133
- ([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
128
+ elsif rows_count
129
+ if rows_count > 0
130
+ jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
131
+ # The last migration job may need to process the amount of rows
132
+ # less than the batch size, so we can get a value > 1.0.
133
+ ([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
134
+ else
135
+ 0.0
136
+ end
134
137
  end
135
138
  end
136
139
 
@@ -154,31 +157,34 @@ module OnlineMigrations
154
157
  # @return [Boolean]
155
158
  #
156
159
  def interval_elapsed?
157
- last_active_job = migration_jobs.active.order(:updated_at).last
160
+ last_job = migration_jobs.order(:updated_at).last
161
+ return true if last_job.nil?
158
162
 
159
- if last_active_job && !last_active_job.stuck?
160
- false
161
- elsif batch_pause > 0 && (job = last_completed_job)
162
- job.finished_at + batch_pause <= Time.current
163
- else
164
- true
165
- end
163
+ last_job.enqueued? || (last_job.updated_at + batch_pause <= Time.current)
166
164
  end
167
165
 
168
- # Manually retry failed jobs.
166
+ # Mark this migration as ready to be processed again.
169
167
  #
170
168
  # This method marks failed jobs as ready to be processed again, and
171
169
  # they will be picked up on the next Scheduler run.
172
170
  #
173
- def retry_failed_jobs
174
- iterator = BatchIterator.new(migration_jobs.failed)
175
- iterator.each_batch(of: 100) do |batch|
176
- transaction do
171
+ def retry
172
+ if composite? && failed?
173
+ children.failed.each(&:retry)
174
+ running!
175
+ true
176
+ elsif failed?
177
+ iterator = BatchIterator.new(migration_jobs.failed)
178
+ iterator.each_batch(of: 100) do |batch|
177
179
  batch.each(&:retry)
178
- enqueued!
179
180
  end
181
+ running!
182
+ true
183
+ else
184
+ false
180
185
  end
181
186
  end
187
+ alias retry_failed_jobs retry
182
188
 
183
189
  # Returns the time this migration started running.
184
190
  def started_at
@@ -47,7 +47,7 @@ module OnlineMigrations
47
47
  delegate :migration_name, :migration_class, :migration_object, :migration_relation, :batch_column_name,
48
48
  :arguments, :batch_pause, to: :migration
49
49
 
50
- belongs_to :migration
50
+ belongs_to :migration, inverse_of: :migration_jobs
51
51
 
52
52
  validates :min_value, :max_value, presence: true, numericality: { greater_than: 0 }
53
53
  validate :values_in_migration_range, if: :min_value?
@@ -69,15 +69,23 @@ module OnlineMigrations
69
69
  # This is used when retrying failed jobs.
70
70
  #
71
71
  def retry
72
- update!(
73
- status: self.class.statuses[:enqueued],
74
- attempts: 0,
75
- started_at: nil,
76
- finished_at: nil,
77
- error_class: nil,
78
- error_message: nil,
79
- backtrace: nil
80
- )
72
+ if failed?
73
+ transaction do
74
+ update!(
75
+ status: self.class.statuses[:enqueued],
76
+ attempts: 0,
77
+ started_at: nil,
78
+ finished_at: nil,
79
+ error_class: nil,
80
+ error_message: nil,
81
+ backtrace: nil
82
+ )
83
+ migration.running! if migration.failed?
84
+ end
85
+ true
86
+ else
87
+ false
88
+ end
81
89
  end
82
90
 
83
91
  private
@@ -25,7 +25,8 @@ module OnlineMigrations
25
25
  # paused -> running occurs when the migration is resumed after being paused.
26
26
  "paused" => ["running"],
27
27
  # failed -> enqueued occurs when the failed migration jobs are retried after being failed.
28
- "failed" => ["enqueued"],
28
+ # failed -> running occurs when the failed migration is retried.
29
+ "failed" => ["enqueued", "running"],
29
30
  }
30
31
 
31
32
  def validate(record)
@@ -55,8 +55,8 @@ module OnlineMigrations
55
55
  enum status: STATUSES.index_with(&:to_s)
56
56
  end
57
57
 
58
- belongs_to :parent, class_name: name, optional: true
59
- has_many :children, class_name: name, foreign_key: :parent_id
58
+ belongs_to :parent, class_name: name, optional: true, inverse_of: :children
59
+ has_many :children, class_name: name, foreign_key: :parent_id, inverse_of: :parent
60
60
 
61
61
  validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
62
62
  validates :definition, presence: true
@@ -109,8 +109,13 @@ module OnlineMigrations
109
109
  # This is used to manually retrying failed migrations.
110
110
  #
111
111
  def retry
112
- if composite?
112
+ if composite? && failed?
113
113
  children.failed.each(&:retry)
114
+ update!(
115
+ status: self.class.statuses[:running],
116
+ finished_at: nil
117
+ )
118
+ true
114
119
  elsif failed?
115
120
  update!(
116
121
  status: self.class.statuses[:enqueued],
@@ -121,6 +126,9 @@ module OnlineMigrations
121
126
  error_message: nil,
122
127
  backtrace: nil
123
128
  )
129
+ true
130
+ else
131
+ false
124
132
  end
125
133
  end
126
134
 
@@ -27,7 +27,14 @@ module OnlineMigrations
27
27
  def mark_as_running
28
28
  Migration.transaction do
29
29
  migration.running!
30
- migration.parent.running! if migration.parent
30
+
31
+ if (parent = migration.parent)
32
+ if parent.started_at
33
+ parent.update!(status: :running, finished_at: nil)
34
+ else
35
+ parent.update!(status: :running, started_at: Time.current, finished_at: nil)
36
+ end
37
+ end
31
38
  end
32
39
  end
33
40
 
@@ -90,10 +97,10 @@ module OnlineMigrations
90
97
  parent.with_lock do
91
98
  children = parent.children.select(:status)
92
99
  if children.all?(&:succeeded?)
93
- parent.succeeded!
100
+ parent.update!(status: :succeeded, finished_at: Time.current)
94
101
  completed = true
95
102
  elsif children.any?(&:failed?)
96
- parent.failed!
103
+ parent.update!(status: :failed, finished_at: Time.current)
97
104
  completed = true
98
105
  end
99
106
  end
@@ -3,7 +3,8 @@
3
3
  module OnlineMigrations
4
4
  module BackgroundSchemaMigrations
5
5
  # Class responsible for scheduling background schema migrations.
6
- # It selects a single migration and runs it if there is no currently running migration.
6
+ # It selects a single migration and runs it if there is no currently
7
+ # running migration on the same table.
7
8
  #
8
9
  # Scheduler should be configured to run periodically, for example, via cron.
9
10
  # @example Run via whenever
@@ -19,12 +20,25 @@ module OnlineMigrations
19
20
 
20
21
  # Runs Scheduler
21
22
  def run
22
- migration = Migration.runnable.enqueued.queue_order.first || Migration.retriable.queue_order.first
23
+ migration = find_migration
23
24
  if migration
24
25
  runner = MigrationRunner.new(migration)
25
26
  runner.run
26
27
  end
27
28
  end
29
+
30
+ private
31
+ def find_migration
32
+ active_migrations = Migration.running.select(:table_name, :shard).to_a
33
+ runnable_migrations = Migration.runnable.enqueued.queue_order.to_a + Migration.retriable.queue_order.to_a
34
+
35
+ runnable_migrations.find do |runnable_migration|
36
+ active_migrations.none? do |active_migration|
37
+ active_migration.shard == runnable_migration.shard &&
38
+ active_migration.table_name == runnable_migration.table_name
39
+ end
40
+ end
41
+ end
28
42
  end
29
43
  end
30
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.17.1"
4
+ VERSION = "0.18.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.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-28 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -113,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
113
  - !ruby/object:Gem::Version
114
114
  version: '0'
115
115
  requirements: []
116
- rubygems_version: 3.5.4
116
+ rubygems_version: 3.4.19
117
117
  signing_key:
118
118
  specification_version: 4
119
119
  summary: Catch unsafe PostgreSQL migrations in development and run them easier in