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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ceda570f99a1712496ac5c9549859c7dc2cad7e3cc4ab59c97a22ba17f3bfd0
4
- data.tar.gz: dfc5a83147a92bf5e04a532210e72b6f32f561f032b2a1b746ff142bf4e16635
3
+ metadata.gz: a5db4da5657a6887113af46baa5c5ec9a6906ba4f8e3d33afc3d81f378f9b5ae
4
+ data.tar.gz: 0acf2706e6b453055d41f338908323a0672066b6e742724b8a6d588e969ca05f
5
5
  SHA512:
6
- metadata.gz: 0ff6cce820134ef6b893f9405b71a0d8f8cd42dd79616123aaa492983671ebcff98dbff21477afa78b29342d40098822da4b8de0306489faf956b21ee44e2113
7
- data.tar.gz: 42dcf132290c9affe812ef7489397d4c05d48f33069f3ee09710fced6aaf8ac928fee63c1498333657a824f98885e3af5e706e4dcc4a089b9cd62284ad0223d1
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
- Add timeouts to `config/database.yml`:
64
+ You can configure a statement timeout for migrations via:
64
65
 
65
- ```yml
66
- production:
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 = '15s';
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 environment. You can override this setting via `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
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 to namespace background migrations in
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
- progresses = children.map(&:progress).compact
106
- if progresses.any?
107
- (progresses.sum / progresses.size).round(2)
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
- # rubocop:disable Lint/UnreachableLoop
185
- iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
186
- min = relation.arel_table[batch_column_name].minimum
187
- max = relation.arel_table[batch_column_name].maximum
188
- batch_range = relation.pick(min, max)
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
- break
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
- migration_name = migration_name.name if migration_name.is_a?(Class)
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
- # For convenience in dev/test environments
373
- if Utils.developer_env?
374
- runner = MigrationRunner.new(migration)
375
- runner.run_all_migration_jobs
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
- migration.running!
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 environment.
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 (this may be due to an aborted migration " \
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
- disable_statement_timeout do
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
- algorithm = options[:algorithm]
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
- column_names = index_column_names(column_name || options[:column])
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
- super(table_name, **options.merge(column: column_names))
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 (this may be due to an aborted migration " \
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
- disable_statement_timeout do
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
- disable_statement_timeout do
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
- # Disables statement timeout while executing &block
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
- prev_value = select_value("SHOW statement_timeout")
875
- execute("SET statement_timeout TO 0")
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
- execute("SET statement_timeout TO #{quote(prev_value)}")
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
- defined?(Rails.env) && (Rails.env.development? || Rails.env.test?)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
@@ -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.11.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-08 00:00:00.000000000 Z
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.10
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