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 +4 -4
- data/CHANGELOG.md +7 -0
- data/docs/background_data_migrations.md +11 -0
- data/docs/background_schema_migrations.md +11 -0
- data/lib/online_migrations/background_migrations/migration.rb +32 -26
- data/lib/online_migrations/background_migrations/migration_job.rb +18 -10
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +2 -1
- data/lib/online_migrations/background_schema_migrations/migration.rb +11 -3
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +10 -3
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +16 -2
- data/lib/online_migrations/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31bbf9d9c3a619e7a878717b8d4bcffae2e71e819bd4d2ea140495fdbb951452
|
4
|
+
data.tar.gz: f9a92ef118577b4bd2e837c95687d19a6d69fba988e61625f51bf2807a855e62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
160
|
+
last_job = migration_jobs.order(:updated_at).last
|
161
|
+
return true if last_job.nil?
|
158
162
|
|
159
|
-
|
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
|
-
#
|
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
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
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
|
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 =
|
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
|
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.
|
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-
|
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.
|
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
|