online_migrations 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/docs/configuring.md +21 -10
- data/lib/generators/online_migrations/templates/initializer.rb.tt +9 -2
- data/lib/online_migrations/background_migrations/config.rb +1 -1
- data/lib/online_migrations/background_migrations/migration.rb +27 -35
- data/lib/online_migrations/background_migrations/migration_helpers.rb +37 -7
- data/lib/online_migrations/background_migrations/migration_runner.rb +9 -5
- data/lib/online_migrations/command_checker.rb +11 -0
- data/lib/online_migrations/config.rb +7 -1
- data/lib/online_migrations/lock_retrier.rb +4 -6
- data/lib/online_migrations/schema_statements.rb +72 -33
- data/lib/online_migrations/utils.rb +15 -1
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +21 -11
- 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: a5db4da5657a6887113af46baa5c5ec9a6906ba4f8e3d33afc3d81f378f9b5ae
|
4
|
+
data.tar.gz: 0acf2706e6b453055d41f338908323a0672066b6e742724b8a6d588e969ca05f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a1b97dceb858d5c6ebd6ddccab4ea0b3ed8d257074f1e7f6562f101b3ae002ac19576053a44fe765624350c40d6e269d753fd24be5b5419474036a308b5cce5
|
7
|
+
data.tar.gz: 9b6a3ece26c9e3e679c5d8d0f827eda7aa10b591242440a18d2a0fb18aa8f16b16935db6654859aab9b6f5008160e117c2eb32e98571261127d1a6ac1e936f2c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.12.0 (2024-01-18)
|
4
|
+
|
5
|
+
- Require passing model name for background migration helpers when using multiple databases
|
6
|
+
- Add `statement_timeout` configuration option
|
7
|
+
|
8
|
+
- Make `lock_timeout` argument optional for `config.lock_retrier`
|
9
|
+
|
10
|
+
This way, a default lock timeout value will be used (configured in `database.yml` or for the database user).
|
11
|
+
|
12
|
+
- Fix a bug that can lead to unfinished children of a sharded background migration
|
13
|
+
|
14
|
+
## 0.11.1 (2024-01-11)
|
15
|
+
|
16
|
+
- Fix calculation of batch ranges for sharded background migrations
|
17
|
+
|
3
18
|
## 0.11.0 (2024-01-09)
|
4
19
|
|
5
20
|
- Support sharding for background migrations
|
data/docs/configuring.md
CHANGED
@@ -59,22 +59,33 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
|
|
59
59
|
## Migration Timeouts
|
60
60
|
|
61
61
|
It’s extremely important to set a short lock timeout for migrations. This way, if a migration can't acquire a lock in a timely manner, other statements won't be stuck behind it.
|
62
|
+
We also recommend setting a long statement timeout so migrations can run for a while.
|
62
63
|
|
63
|
-
|
64
|
+
You can configure a statement timeout for migrations via:
|
64
65
|
|
65
|
-
```
|
66
|
-
|
67
|
-
connect_timeout: 5
|
68
|
-
variables:
|
69
|
-
lock_timeout: 10s
|
70
|
-
statement_timeout: 15s
|
66
|
+
```ruby
|
67
|
+
config.statement_timeout = 1.hour
|
71
68
|
```
|
72
69
|
|
70
|
+
and a lock timeout for migrations can be configured via the `lock_retrier`.
|
71
|
+
|
73
72
|
Or set the timeouts directly on the database user that runs migrations:
|
74
73
|
|
75
74
|
```sql
|
76
75
|
ALTER ROLE myuser SET lock_timeout = '10s';
|
77
|
-
ALTER ROLE myuser SET statement_timeout = '
|
76
|
+
ALTER ROLE myuser SET statement_timeout = '1h';
|
77
|
+
```
|
78
|
+
|
79
|
+
## App Timeouts
|
80
|
+
|
81
|
+
We recommend adding timeouts to `config/database.yml` to prevent connections from hanging and individual queries from taking up too many resources in controllers, jobs, the Rails console, and other places.
|
82
|
+
|
83
|
+
```yml
|
84
|
+
production:
|
85
|
+
connect_timeout: 5
|
86
|
+
variables:
|
87
|
+
lock_timeout: 10s
|
88
|
+
statement_timeout: 15s
|
78
89
|
```
|
79
90
|
|
80
91
|
## Lock Timeout Retries
|
@@ -86,7 +97,7 @@ config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
|
|
86
97
|
attempts: 30, # attempt 30 retries
|
87
98
|
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
88
99
|
max_delay: 1.minute, # maximum delay is 1 minute
|
89
|
-
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
|
100
|
+
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try. Remove this line to use a default lock timeout.
|
90
101
|
)
|
91
102
|
```
|
92
103
|
|
@@ -192,7 +203,7 @@ To enable verbose sql logs:
|
|
192
203
|
config.verbose_sql_logs = true
|
193
204
|
```
|
194
205
|
|
195
|
-
This feature is enabled by default in a production Rails
|
206
|
+
This feature is enabled by default in a staging and production Rails environments. You can override this setting via `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
196
207
|
|
197
208
|
## Analyze Tables
|
198
209
|
|
@@ -4,6 +4,9 @@ OnlineMigrations.configure do |config|
|
|
4
4
|
# Configure the migration version starting after which checks are performed.
|
5
5
|
# config.start_after = <%= start_after %>
|
6
6
|
|
7
|
+
# Configure statement timeout used for migrations.
|
8
|
+
config.statement_timeout = 1.hour
|
9
|
+
|
7
10
|
# Set the version of the production database so the right checks are run in development.
|
8
11
|
# config.target_version = 10
|
9
12
|
|
@@ -48,7 +51,7 @@ OnlineMigrations.configure do |config|
|
|
48
51
|
# a better grasp of what is going on for high-level statements like add_column_with_default.
|
49
52
|
#
|
50
53
|
# Note: It can be overridden by `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
51
|
-
config.verbose_sql_logs = defined?(Rails) && Rails.env.production?
|
54
|
+
config.verbose_sql_logs = defined?(Rails.env) && (Rails.env.production? || Rails.env.staging?)
|
52
55
|
|
53
56
|
# Lock retries.
|
54
57
|
# Configure your custom lock retrier (see LockRetrier).
|
@@ -57,7 +60,7 @@ OnlineMigrations.configure do |config|
|
|
57
60
|
attempts: 30, # attempt 30 retries
|
58
61
|
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
59
62
|
max_delay: 1.minute, # up to the maximum delay of 1 minute
|
60
|
-
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
|
63
|
+
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try. Remove this line to use a default lock timeout.
|
61
64
|
)
|
62
65
|
|
63
66
|
# Configure tables that are in the process of being renamed.
|
@@ -75,6 +78,10 @@ OnlineMigrations.configure do |config|
|
|
75
78
|
# end
|
76
79
|
|
77
80
|
# ==> Background migrations configuration
|
81
|
+
|
82
|
+
# The module in which background migrations will be placed.
|
83
|
+
# config.background_migrations.migrations_module = "OnlineMigrations::BackgroundMigrations"
|
84
|
+
|
78
85
|
# The number of rows to process in a single background migration run.
|
79
86
|
# config.background_migrations.batch_size = 20_000
|
80
87
|
|
@@ -4,7 +4,7 @@ module OnlineMigrations
|
|
4
4
|
module BackgroundMigrations
|
5
5
|
# Class representing configuration options for background migrations.
|
6
6
|
class Config
|
7
|
-
# The module
|
7
|
+
# The module in which background migrations will be placed
|
8
8
|
# @return [String] defaults to "OnlineMigrations::BackgroundMigrations"
|
9
9
|
attr_accessor :migrations_module
|
10
10
|
|
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
module OnlineMigrations
|
4
4
|
module BackgroundMigrations
|
5
|
+
# Class representing background data migration.
|
6
|
+
#
|
7
|
+
# @note The records of this class should not be created manually, but via
|
8
|
+
# `enqueue_background_migration` helper inside migrations.
|
9
|
+
#
|
5
10
|
class Migration < ApplicationRecord
|
6
11
|
STATUSES = [
|
7
12
|
:enqueued, # The migration has been enqueued by the user.
|
@@ -26,8 +31,8 @@ module OnlineMigrations
|
|
26
31
|
enum status: STATUSES.index_with(&:to_s)
|
27
32
|
|
28
33
|
belongs_to :parent, class_name: name, optional: true
|
29
|
-
has_many :children, class_name: name, foreign_key: :parent_id
|
30
|
-
has_many :migration_jobs
|
34
|
+
has_many :children, class_name: name, foreign_key: :parent_id, dependent: :delete_all
|
35
|
+
has_many :migration_jobs, dependent: :delete_all
|
31
36
|
|
32
37
|
validates :migration_name, :batch_column_name, presence: true
|
33
38
|
|
@@ -47,7 +52,6 @@ module OnlineMigrations
|
|
47
52
|
validates_with MigrationStatusValidator, on: :update
|
48
53
|
|
49
54
|
before_validation :set_defaults
|
50
|
-
before_create :create_child_migrations, if: :composite?
|
51
55
|
before_update :copy_attributes_to_children, if: :composite?
|
52
56
|
|
53
57
|
# @private
|
@@ -57,6 +61,7 @@ module OnlineMigrations
|
|
57
61
|
end
|
58
62
|
|
59
63
|
def migration_name=(class_name)
|
64
|
+
class_name = class_name.name if class_name.is_a?(Class)
|
60
65
|
write_attribute(:migration_name, self.class.normalize_migration_name(class_name))
|
61
66
|
end
|
62
67
|
|
@@ -102,9 +107,15 @@ module OnlineMigrations
|
|
102
107
|
if succeeded?
|
103
108
|
100.0
|
104
109
|
elsif composite?
|
105
|
-
|
106
|
-
if
|
107
|
-
|
110
|
+
rows_counts = children.to_a.pluck(:rows_count)
|
111
|
+
if rows_counts.none?(nil)
|
112
|
+
total_rows_count = rows_counts.sum
|
113
|
+
|
114
|
+
progresses = children.map do |child|
|
115
|
+
child.progress * child.rows_count / total_rows_count # weighted progress
|
116
|
+
end
|
117
|
+
|
118
|
+
progresses.sum.round(2)
|
108
119
|
end
|
109
120
|
elsif rows_count
|
110
121
|
jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
|
@@ -181,15 +192,17 @@ module OnlineMigrations
|
|
181
192
|
iterator = BatchIterator.new(migration_relation)
|
182
193
|
batch_range = nil
|
183
194
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
195
|
+
on_shard do
|
196
|
+
# rubocop:disable Lint/UnreachableLoop
|
197
|
+
iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
|
198
|
+
min = relation.arel_table[batch_column_name].minimum
|
199
|
+
max = relation.arel_table[batch_column_name].maximum
|
200
|
+
batch_range = relation.pick(min, max)
|
189
201
|
|
190
|
-
|
202
|
+
break
|
203
|
+
end
|
204
|
+
# rubocop:enable Lint/UnreachableLoop
|
191
205
|
end
|
192
|
-
# rubocop:enable Lint/UnreachableLoop
|
193
206
|
|
194
207
|
return if batch_range.nil?
|
195
208
|
|
@@ -201,10 +214,6 @@ module OnlineMigrations
|
|
201
214
|
[min_value, max_value]
|
202
215
|
end
|
203
216
|
|
204
|
-
protected
|
205
|
-
attr_accessor :child
|
206
|
-
alias child? child
|
207
|
-
|
208
217
|
private
|
209
218
|
def validate_batch_column_values
|
210
219
|
if max_value.to_i < min_value.to_i
|
@@ -234,11 +243,6 @@ module OnlineMigrations
|
|
234
243
|
|
235
244
|
def set_defaults
|
236
245
|
if migration_relation.is_a?(ActiveRecord::Relation)
|
237
|
-
if !child?
|
238
|
-
shards = Utils.shard_names(migration_model)
|
239
|
-
self.composite = shards.size > 1
|
240
|
-
end
|
241
|
-
|
242
246
|
self.batch_column_name ||= migration_relation.primary_key
|
243
247
|
|
244
248
|
if composite?
|
@@ -268,25 +272,13 @@ module OnlineMigrations
|
|
268
272
|
self.batch_max_attempts ||= config.batch_max_attempts
|
269
273
|
end
|
270
274
|
|
271
|
-
def create_child_migrations
|
272
|
-
shards = Utils.shard_names(migration_model)
|
273
|
-
|
274
|
-
children = shards.map do |shard|
|
275
|
-
child = Migration.new(migration_name: migration_name, arguments: arguments, shard: shard)
|
276
|
-
child.child = true
|
277
|
-
child
|
278
|
-
end
|
279
|
-
|
280
|
-
self.children = children
|
281
|
-
end
|
282
|
-
|
283
275
|
def copy_attributes_to_children
|
284
276
|
attributes = [:batch_size, :sub_batch_size, :batch_pause, :sub_batch_pause_ms, :batch_max_attempts]
|
285
277
|
updates = {}
|
286
278
|
attributes.each do |attribute|
|
287
279
|
updates[attribute] = read_attribute(attribute) if attribute_changed?(attribute)
|
288
280
|
end
|
289
|
-
children.update_all(updates) if updates.any?
|
281
|
+
children.active.update_all(updates) if updates.any?
|
290
282
|
end
|
291
283
|
|
292
284
|
def next_min_value
|
@@ -42,6 +42,10 @@ module OnlineMigrations
|
|
42
42
|
# @see #backfill_column_in_background
|
43
43
|
#
|
44
44
|
def backfill_columns_in_background(table_name, updates, model_name: nil, **options)
|
45
|
+
if model_name.nil? && Utils.multiple_databases?
|
46
|
+
raise ArgumentError, "You must pass a :model_name when using multiple databases."
|
47
|
+
end
|
48
|
+
|
45
49
|
model_name = model_name.name if model_name.is_a?(Class)
|
46
50
|
|
47
51
|
enqueue_background_migration(
|
@@ -99,6 +103,10 @@ module OnlineMigrations
|
|
99
103
|
#
|
100
104
|
def backfill_columns_for_type_change_in_background(table_name, *column_names, model_name: nil,
|
101
105
|
type_cast_functions: {}, **options)
|
106
|
+
if model_name.nil? && Utils.multiple_databases?
|
107
|
+
raise ArgumentError, "You must pass a :model_name when using multiple databases."
|
108
|
+
end
|
109
|
+
|
102
110
|
tmp_columns = column_names.map { |column_name| "#{column_name}_for_type_change" }
|
103
111
|
model_name = model_name.name if model_name.is_a?(Class)
|
104
112
|
|
@@ -153,6 +161,10 @@ module OnlineMigrations
|
|
153
161
|
# @see #copy_column_in_background
|
154
162
|
#
|
155
163
|
def copy_columns_in_background(table_name, copy_from, copy_to, model_name: nil, type_cast_functions: {}, **options)
|
164
|
+
if model_name.nil? && Utils.multiple_databases?
|
165
|
+
raise ArgumentError, "You must pass a :model_name when using multiple databases."
|
166
|
+
end
|
167
|
+
|
156
168
|
model_name = model_name.name if model_name.is_a?(Class)
|
157
169
|
|
158
170
|
enqueue_background_migration(
|
@@ -358,23 +370,41 @@ module OnlineMigrations
|
|
358
370
|
# in development and test environments
|
359
371
|
#
|
360
372
|
def enqueue_background_migration(migration_name, *arguments, **options)
|
373
|
+
migration = create_background_migration(migration_name, *arguments, **options)
|
374
|
+
|
375
|
+
# For convenience in dev/test environments
|
376
|
+
if Utils.developer_env?
|
377
|
+
runner = MigrationRunner.new(migration)
|
378
|
+
runner.run_all_migration_jobs
|
379
|
+
end
|
380
|
+
|
381
|
+
migration
|
382
|
+
end
|
383
|
+
|
384
|
+
# @private
|
385
|
+
def create_background_migration(migration_name, *arguments, **options)
|
361
386
|
options.assert_valid_keys(:batch_column_name, :min_value, :max_value, :batch_size, :sub_batch_size,
|
362
387
|
:batch_pause, :sub_batch_pause_ms, :batch_max_attempts)
|
363
388
|
|
364
|
-
|
365
|
-
|
366
|
-
migration = Migration.create!(
|
389
|
+
migration = Migration.new(
|
367
390
|
migration_name: migration_name,
|
368
391
|
arguments: arguments,
|
369
392
|
**options
|
370
393
|
)
|
371
394
|
|
372
|
-
|
373
|
-
if
|
374
|
-
|
375
|
-
|
395
|
+
shards = Utils.shard_names(migration.migration_model)
|
396
|
+
if shards.size > 1
|
397
|
+
migration.children = shards.map do |shard|
|
398
|
+
child = migration.dup
|
399
|
+
child.shard = shard
|
400
|
+
child
|
401
|
+
end
|
402
|
+
|
403
|
+
migration.composite = true
|
376
404
|
end
|
377
405
|
|
406
|
+
# This will save all the records using a transaction.
|
407
|
+
migration.save!
|
378
408
|
migration
|
379
409
|
end
|
380
410
|
end
|
@@ -14,10 +14,7 @@ module OnlineMigrations
|
|
14
14
|
def run_migration_job
|
15
15
|
raise "Should not be called on a composite (with sharding) migration" if migration.composite?
|
16
16
|
|
17
|
-
if migration.enqueued?
|
18
|
-
migration.running!
|
19
|
-
migration.parent.running! if migration.parent && migration.parent.enqueued?
|
20
|
-
end
|
17
|
+
mark_as_running if migration.enqueued?
|
21
18
|
migration_payload = notifications_payload(migration)
|
22
19
|
|
23
20
|
if !migration.migration_jobs.exists?
|
@@ -57,7 +54,7 @@ module OnlineMigrations
|
|
57
54
|
raise "This method is not intended for use in production environments" if !Utils.developer_env?
|
58
55
|
return if migration.completed?
|
59
56
|
|
60
|
-
|
57
|
+
mark_as_running
|
61
58
|
|
62
59
|
if migration.composite?
|
63
60
|
migration.children.each do |child_migration|
|
@@ -96,6 +93,13 @@ module OnlineMigrations
|
|
96
93
|
end
|
97
94
|
|
98
95
|
private
|
96
|
+
def mark_as_running
|
97
|
+
Migration.transaction do
|
98
|
+
migration.running!
|
99
|
+
migration.parent.running! if migration.parent && migration.parent.enqueued?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
99
103
|
def should_throttle?
|
100
104
|
::OnlineMigrations.config.background_migrations.throttler.call
|
101
105
|
end
|
@@ -31,6 +31,7 @@ module OnlineMigrations
|
|
31
31
|
|
32
32
|
def check(command, *args, &block)
|
33
33
|
check_database_version
|
34
|
+
set_statement_timeout
|
34
35
|
check_lock_timeout
|
35
36
|
|
36
37
|
if !safe?
|
@@ -98,6 +99,16 @@ module OnlineMigrations
|
|
98
99
|
@database_version_checked = true
|
99
100
|
end
|
100
101
|
|
102
|
+
def set_statement_timeout
|
103
|
+
if !@statement_timeout_set
|
104
|
+
if (statement_timeout = OnlineMigrations.config.statement_timeout)
|
105
|
+
# TODO: inline this method call after deprecated `disable_statement_timeout` method removal.
|
106
|
+
connection.__set_statement_timeout(statement_timeout)
|
107
|
+
end
|
108
|
+
@statement_timeout_set = true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
101
112
|
def check_lock_timeout
|
102
113
|
limit = OnlineMigrations.config.lock_timeout_limit
|
103
114
|
|
@@ -35,6 +35,12 @@ module OnlineMigrations
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
# Statement timeout used for migrations (in seconds)
|
39
|
+
#
|
40
|
+
# @return [Numeric]
|
41
|
+
#
|
42
|
+
attr_accessor :statement_timeout
|
43
|
+
|
38
44
|
# Set the database version against which the checks will be performed
|
39
45
|
#
|
40
46
|
# If your development database version is different from production, you can specify
|
@@ -158,7 +164,7 @@ module OnlineMigrations
|
|
158
164
|
# migration failure in production. This is also useful in development to get
|
159
165
|
# a better grasp of what is going on for high-level statements like add_column_with_default.
|
160
166
|
#
|
161
|
-
# This feature is enabled by default in a production Rails
|
167
|
+
# This feature is enabled by default in a staging and production Rails environments.
|
162
168
|
# @return [Boolean]
|
163
169
|
#
|
164
170
|
# @note: It can be overridden by `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
@@ -60,9 +60,7 @@ module OnlineMigrations
|
|
60
60
|
#
|
61
61
|
# @param _attempt [Integer] attempt number
|
62
62
|
#
|
63
|
-
def lock_timeout(_attempt)
|
64
|
-
raise NotImplementedError
|
65
|
-
end
|
63
|
+
def lock_timeout(_attempt); end
|
66
64
|
|
67
65
|
# Returns sleep time after unsuccessful lock attempt (in seconds)
|
68
66
|
#
|
@@ -143,9 +141,9 @@ module OnlineMigrations
|
|
143
141
|
#
|
144
142
|
# @param attempts [Integer] Maximum number of attempts
|
145
143
|
# @param delay [Numeric] Sleep time after unsuccessful lock attempt (in seconds)
|
146
|
-
# @param lock_timeout [Numeric] Database lock timeout value (in seconds)
|
144
|
+
# @param lock_timeout [Numeric, nil] Database lock timeout value (in seconds)
|
147
145
|
#
|
148
|
-
def initialize(attempts:, delay:, lock_timeout:)
|
146
|
+
def initialize(attempts:, delay:, lock_timeout: nil)
|
149
147
|
super()
|
150
148
|
@attempts = attempts
|
151
149
|
@delay = delay
|
@@ -196,7 +194,7 @@ module OnlineMigrations
|
|
196
194
|
# @param max_delay [Numeric] Maximum sleep time after unsuccessful lock attempt (in seconds)
|
197
195
|
# @param lock_timeout [Numeric] Database lock timeout value (in seconds)
|
198
196
|
#
|
199
|
-
def initialize(attempts:, base_delay:, max_delay:, lock_timeout:)
|
197
|
+
def initialize(attempts:, base_delay:, max_delay:, lock_timeout: nil)
|
200
198
|
super()
|
201
199
|
@attempts = attempts
|
202
200
|
@base_delay = base_delay
|
@@ -680,7 +680,7 @@ module OnlineMigrations
|
|
680
680
|
#
|
681
681
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index
|
682
682
|
#
|
683
|
-
def add_index(table_name, column_name, options
|
683
|
+
def add_index(table_name, column_name, **options)
|
684
684
|
algorithm = options[:algorithm]
|
685
685
|
|
686
686
|
__ensure_not_in_transaction! if algorithm == :concurrently
|
@@ -694,8 +694,7 @@ module OnlineMigrations
|
|
694
694
|
schema = __schema_for_table(table_name)
|
695
695
|
|
696
696
|
if __index_valid?(index_name, schema: schema)
|
697
|
-
Utils.say("Index was not created because it already exists
|
698
|
-
"or similar): table_name: #{table_name}, column_name: #{column_name}")
|
697
|
+
Utils.say("Index was not created because it already exists.")
|
699
698
|
return
|
700
699
|
else
|
701
700
|
Utils.say("Recreating invalid index: table_name: #{table_name}, column_name: #{column_name}")
|
@@ -703,11 +702,22 @@ module OnlineMigrations
|
|
703
702
|
end
|
704
703
|
end
|
705
704
|
|
706
|
-
|
705
|
+
if OnlineMigrations.config.statement_timeout
|
707
706
|
# "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
708
707
|
# It only conflicts with constraint validations, creating/removing indexes,
|
709
708
|
# and some other "ALTER TABLE"s.
|
710
709
|
super(table_name, column_name, **options.merge(name: index_name))
|
710
|
+
else
|
711
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
712
|
+
Running `add_index` without a statement timeout is deprecated.
|
713
|
+
Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
|
714
|
+
or the default database statement timeout will be used.
|
715
|
+
Example, `config.statement_timeout = 1.hour`.
|
716
|
+
MSG
|
717
|
+
|
718
|
+
disable_statement_timeout do
|
719
|
+
super(table_name, column_name, **options.merge(name: index_name))
|
720
|
+
end
|
711
721
|
end
|
712
722
|
end
|
713
723
|
|
@@ -716,23 +726,32 @@ module OnlineMigrations
|
|
716
726
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_index
|
717
727
|
#
|
718
728
|
def remove_index(table_name, column_name = nil, **options)
|
719
|
-
|
729
|
+
if column_name.blank? && options[:column].blank? && options[:name].blank?
|
730
|
+
raise ArgumentError, "No name or columns specified"
|
731
|
+
end
|
720
732
|
|
721
|
-
__ensure_not_in_transaction! if algorithm == :concurrently
|
733
|
+
__ensure_not_in_transaction! if options[:algorithm] == :concurrently
|
722
734
|
|
723
|
-
|
724
|
-
|
725
|
-
if index_exists?(table_name, column_names, **options)
|
726
|
-
disable_statement_timeout do
|
735
|
+
if index_exists?(table_name, column_name, **options)
|
736
|
+
if OnlineMigrations.config.statement_timeout
|
727
737
|
# "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
728
738
|
# It only conflicts with constraint validations, other creating/removing indexes,
|
729
739
|
# and some "ALTER TABLE"s.
|
740
|
+
super(table_name, column_name, **options)
|
741
|
+
else
|
742
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
743
|
+
Running `remove_index` without a statement timeout is deprecated.
|
744
|
+
Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
|
745
|
+
or the default database statement timeout will be used.
|
746
|
+
Example, `config.statement_timeout = 1.hour`.
|
747
|
+
MSG
|
730
748
|
|
731
|
-
|
749
|
+
disable_statement_timeout do
|
750
|
+
super(table_name, column_name, **options)
|
751
|
+
end
|
732
752
|
end
|
733
753
|
else
|
734
|
-
Utils.say("Index was not removed because it does not exist
|
735
|
-
"or similar): table_name: #{table_name}, column_name: #{column_names}")
|
754
|
+
Utils.say("Index was not removed because it does not exist.")
|
736
755
|
end
|
737
756
|
end
|
738
757
|
|
@@ -778,11 +797,22 @@ module OnlineMigrations
|
|
778
797
|
# Skip costly operation if already validated.
|
779
798
|
return if foreign_key.validated?
|
780
799
|
|
781
|
-
|
800
|
+
if OnlineMigrations.config.statement_timeout
|
782
801
|
# "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
783
802
|
# It only conflicts with other validations, creating/removing indexes,
|
784
803
|
# and some other "ALTER TABLE"s.
|
785
804
|
super
|
805
|
+
else
|
806
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
807
|
+
Running `validate_foreign_key` without a statement timeout is deprecated.
|
808
|
+
Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
|
809
|
+
or the default database statement timeout will be used.
|
810
|
+
Example, `config.statement_timeout = 1.hour`.
|
811
|
+
MSG
|
812
|
+
|
813
|
+
disable_statement_timeout do
|
814
|
+
super
|
815
|
+
end
|
786
816
|
end
|
787
817
|
end
|
788
818
|
|
@@ -811,11 +841,22 @@ module OnlineMigrations
|
|
811
841
|
# Skip costly operation if already validated.
|
812
842
|
return if check_constraint.validated?
|
813
843
|
|
814
|
-
|
844
|
+
if OnlineMigrations.config.statement_timeout
|
815
845
|
# "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
816
846
|
# It only conflicts with other validations, creating/removing indexes,
|
817
847
|
# and some other "ALTER TABLE"s.
|
818
848
|
super
|
849
|
+
else
|
850
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
851
|
+
Running `validate_check_constraint` without a statement timeout is deprecated.
|
852
|
+
Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
|
853
|
+
or the default database statement timeout will be used.
|
854
|
+
Example, `config.statement_timeout = 1.hour`.
|
855
|
+
MSG
|
856
|
+
|
857
|
+
disable_statement_timeout do
|
858
|
+
super
|
859
|
+
end
|
819
860
|
end
|
820
861
|
end
|
821
862
|
|
@@ -855,28 +896,26 @@ module OnlineMigrations
|
|
855
896
|
end
|
856
897
|
end
|
857
898
|
|
858
|
-
#
|
859
|
-
#
|
860
|
-
# Long-running migrations may take more than the timeout allowed by the database.
|
861
|
-
# Disable the session's statement timeout to ensure migrations don't get killed prematurely.
|
862
|
-
#
|
863
|
-
# Statement timeouts are already disabled in `add_index`, `remove_index`,
|
864
|
-
# `validate_foreign_key`, and `validate_check_constraint` helpers.
|
865
|
-
#
|
866
|
-
# @return [void]
|
867
|
-
#
|
868
|
-
# @example
|
869
|
-
# disable_statement_timeout do
|
870
|
-
# add_index(:users, :email, unique: true, algorithm: :concurrently)
|
871
|
-
# end
|
872
|
-
#
|
899
|
+
# @private
|
873
900
|
def disable_statement_timeout
|
874
|
-
|
875
|
-
|
901
|
+
OnlineMigrations.deprecator.warn(<<~MSG)
|
902
|
+
`disable_statement_timeout` is deprecated and will be removed. Configure an explicit
|
903
|
+
statement timeout in the initializer file via `config.statement_timeout` or the default
|
904
|
+
database statement timeout will be used. Example, `config.statement_timeout = 1.hour`.
|
905
|
+
MSG
|
876
906
|
|
907
|
+
prev_value = select_value("SHOW statement_timeout")
|
908
|
+
__set_statement_timeout(0)
|
877
909
|
yield
|
878
910
|
ensure
|
879
|
-
|
911
|
+
__set_statement_timeout(prev_value)
|
912
|
+
end
|
913
|
+
|
914
|
+
# @private
|
915
|
+
def __set_statement_timeout(timeout)
|
916
|
+
# use ceil to prevent no timeout for values under 1 ms
|
917
|
+
timeout = (timeout.to_f * 1000).ceil if !timeout.is_a?(String)
|
918
|
+
execute("SET statement_timeout TO #{quote(timeout)}")
|
880
919
|
end
|
881
920
|
|
882
921
|
# @private
|
@@ -10,8 +10,17 @@ module OnlineMigrations
|
|
10
10
|
ActiveRecord.version.to_s.to_f
|
11
11
|
end
|
12
12
|
|
13
|
+
def env
|
14
|
+
if defined?(Rails.env)
|
15
|
+
Rails.env
|
16
|
+
else
|
17
|
+
# default to production for safety
|
18
|
+
ENV["RACK_ENV"] || "production"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
13
22
|
def developer_env?
|
14
|
-
|
23
|
+
env == "development" || env == "test"
|
15
24
|
end
|
16
25
|
|
17
26
|
def say(message)
|
@@ -137,6 +146,11 @@ module OnlineMigrations
|
|
137
146
|
return pool_manager.shard_names.uniq if pool_manager
|
138
147
|
end
|
139
148
|
end
|
149
|
+
|
150
|
+
def multiple_databases?
|
151
|
+
db_config = ActiveRecord::Base.configurations.configs_for(env_name: env)
|
152
|
+
db_config.reject(&:replica?).size > 1
|
153
|
+
end
|
140
154
|
end
|
141
155
|
end
|
142
156
|
end
|
data/lib/online_migrations.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_record"
|
4
|
+
|
4
5
|
require "online_migrations/version"
|
6
|
+
require "online_migrations/utils"
|
7
|
+
require "online_migrations/change_column_type_helpers"
|
8
|
+
require "online_migrations/background_migrations/migration_helpers"
|
9
|
+
require "online_migrations/schema_statements"
|
10
|
+
require "online_migrations/migration"
|
11
|
+
require "online_migrations/migrator"
|
12
|
+
require "online_migrations/schema_dumper"
|
13
|
+
require "online_migrations/database_tasks"
|
14
|
+
require "online_migrations/command_recorder"
|
15
|
+
require "online_migrations/error_messages"
|
16
|
+
require "online_migrations/config"
|
5
17
|
|
6
18
|
module OnlineMigrations
|
7
19
|
class Error < StandardError; end
|
@@ -9,15 +21,8 @@ module OnlineMigrations
|
|
9
21
|
|
10
22
|
extend ActiveSupport::Autoload
|
11
23
|
|
12
|
-
autoload :Utils
|
13
|
-
autoload :ErrorMessages
|
14
|
-
autoload :Config
|
15
24
|
autoload :BatchIterator
|
16
25
|
autoload :VerboseSqlLogs
|
17
|
-
autoload :Migration
|
18
|
-
autoload :Migrator
|
19
|
-
autoload :SchemaDumper
|
20
|
-
autoload :DatabaseTasks
|
21
26
|
autoload :ForeignKeysCollector
|
22
27
|
autoload :IndexDefinition
|
23
28
|
autoload :IndexesCollector
|
@@ -36,10 +41,7 @@ module OnlineMigrations
|
|
36
41
|
autoload :NullLockRetrier
|
37
42
|
end
|
38
43
|
|
39
|
-
autoload :CommandRecorder
|
40
44
|
autoload :CopyTrigger
|
41
|
-
autoload :ChangeColumnTypeHelpers
|
42
|
-
autoload :SchemaStatements
|
43
45
|
|
44
46
|
module BackgroundMigrations
|
45
47
|
extend ActiveSupport::Autoload
|
@@ -59,7 +61,6 @@ module OnlineMigrations
|
|
59
61
|
autoload :Migration
|
60
62
|
autoload :MigrationJobRunner
|
61
63
|
autoload :MigrationRunner
|
62
|
-
autoload :MigrationHelpers
|
63
64
|
autoload :Scheduler
|
64
65
|
end
|
65
66
|
|
@@ -80,6 +81,15 @@ module OnlineMigrations
|
|
80
81
|
BackgroundMigrations::Scheduler.run
|
81
82
|
end
|
82
83
|
|
84
|
+
def deprecator
|
85
|
+
@deprecator ||=
|
86
|
+
if Utils.ar_version >= 7.1
|
87
|
+
ActiveSupport::Deprecation.new(nil, "online_migrations")
|
88
|
+
else
|
89
|
+
ActiveSupport::Deprecation
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
83
93
|
# @private
|
84
94
|
def load
|
85
95
|
require "active_record/connection_adapters/postgresql_adapter"
|
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.12.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-01-
|
11
|
+
date: 2024-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
104
|
- !ruby/object:Gem::Version
|
105
105
|
version: '0'
|
106
106
|
requirements: []
|
107
|
-
rubygems_version: 3.4
|
107
|
+
rubygems_version: 3.5.4
|
108
108
|
signing_key:
|
109
109
|
specification_version: 4
|
110
110
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|