online_migrations 0.11.0 → 0.12.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: 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