online_migrations 0.17.1 → 0.18.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: 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