online_migrations 0.29.3 → 0.30.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: fb09b96dff81043dd551135fda5ec6a0aaf98cc1d5796961ddca4b7d20397f30
4
- data.tar.gz: eff3a9dc6c493c0ed5993fa6254ef095ac35e9d7e6f3d4b9fbae36266062b940
3
+ metadata.gz: 428fd65e363b915608f720370a2cd91ec14749cd17933ec4e26351bb4745084a
4
+ data.tar.gz: ae292bba4cd1b557694f78240a7da761c8d890aa691cc6b485ddd9f71fd339c5
5
5
  SHA512:
6
- metadata.gz: cd28c6d5c4288fd682a5b82e812a4ce9e2774ebc9ec459ebb4a087837dd05d6cbfc72b7ed57d940e9919fdc39fbba47fc0de667f6e1ff149336724aa6f1aa167
7
- data.tar.gz: 876c2219d85f80d3e1494194449c16ef32af3bf10d70b4e15e35f763d596b859ce80430e3f3fba559c6a79512cfc945a2b3fe672b4025a8681b3a707820ac393
6
+ metadata.gz: 59c0573c24d75488c849fa746d6346c31e439d66032725264ce724e97ab8ffbd16cebb03a57ad75071e2b29bf884be0b47696abdcadc72d974effaafd73fe8b4
7
+ data.tar.gz: 73de95254d001d4d285a48ad4d3adb60726ab6bf0bb86b085dde8466b6e1e85fbb668738f09121c38718504ad253615afb3da5e24f56bc48bd01ce9d369cdd4c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.30.0 (2025-10-17)
4
+
5
+ - Fix `remove_check_constraint` when using `:if_exists`
6
+ - Add schema statement to lock retrier
7
+
3
8
  ## 0.29.3 (2025-08-19)
4
9
 
5
10
  - Fix an edge case where a background index might not be added
data/docs/configuring.md CHANGED
@@ -113,6 +113,129 @@ When a statement within transaction fails - the whole transaction is retried. If
113
113
 
114
114
  **Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
115
115
 
116
+ ### Command-specific lock retry configuration
117
+
118
+ For migrations using `disable_ddl_transaction!`, you can implement command-specific lock retry behavior. This is useful when different DDL operations have different locking characteristics:
119
+
120
+ - `add_index` with `algorithm: :concurrently` uses `ShareUpdateExclusiveLock` (less restrictive), so can use longer timeouts
121
+ - `add_foreign_key` uses `AccessExclusiveLock` (blocks all access), so should use shorter timeouts to fail fast
122
+
123
+ **Note**: Command-specific configuration only works for migrations with `disable_ddl_transaction!`. For migrations running within transactions (the default), the lock retrier wraps the entire transaction and doesn't have visibility into individual DDL commands.
124
+
125
+ ```ruby
126
+ module OnlineMigrations
127
+ class CommandAwareLockRetrier < LockRetrier
128
+ # You can vary the number of attempts based on the command
129
+ def attempts(command = nil, arguments = [])
130
+ case command
131
+ when :add_index
132
+ # Concurrent index creation uses longer individual timeouts,
133
+ # so fewer attempts are needed to reach the same overall window
134
+ 10
135
+ when :add_foreign_key
136
+ # Foreign keys use shorter timeouts to fail fast,
137
+ # so more attempts are needed to reach the same overall window
138
+ 60
139
+ else
140
+ # Default attempts for other operations
141
+ 30
142
+ end
143
+ end
144
+
145
+ def lock_timeout(attempt, command = nil, arguments = [])
146
+ case command
147
+ when :add_index
148
+ # Concurrent index creation is less restrictive, use longer timeout
149
+ 30.seconds
150
+ when :add_foreign_key
151
+ # Foreign keys block all access, use shorter timeout to fail fast
152
+ 5.seconds
153
+ else
154
+ # Default timeout for other operations
155
+ 10.seconds
156
+ end
157
+ end
158
+
159
+ def delay(attempt, command = nil, arguments = [])
160
+ case command
161
+ when :add_index
162
+ # Longer delay for index operations since they take time anyway
163
+ 3.seconds
164
+ when :add_foreign_key
165
+ # Shorter delay to retry faster for quick FK operations
166
+ 1.second
167
+ else
168
+ # Default delay for other operations
169
+ 2.seconds
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ config.lock_retrier = OnlineMigrations::CommandAwareLockRetrier.new
176
+ ```
177
+
178
+ All three methods (`attempts`, `lock_timeout`, and `delay`) can receive command-specific parameters:
179
+ - `command` - the migration method being called (e.g., `:add_index`, `:add_column`, `:add_foreign_key`), or `nil` for transaction-wrapped migrations
180
+ - `arguments` - an array of arguments passed to the migration method
181
+
182
+ Additionally, `lock_timeout` and `delay` receive:
183
+ - `attempt` - the current retry attempt number (1-indexed)
184
+
185
+ This allows you to fine-tune the retry strategy for different commands. For example, to maintain roughly the same total timeout window:
186
+ - `add_index`: 10 attempts × (30s lock + 3s delay) = ~5.5 minute window
187
+ - `add_foreign_key`: 60 attempts × (5s lock + 1s delay) = ~6 minute window
188
+
189
+ #### Alternative: Configuration Hash Approach
190
+
191
+ For simpler use cases, you can use a configuration hash instead of case statements:
192
+
193
+ ```ruby
194
+ module OnlineMigrations
195
+ class ConfigurableLockRetrier < LockRetrier
196
+ COMMAND_CONFIGS = {
197
+ add_index: {
198
+ attempts: 10,
199
+ lock_timeout: 30.seconds,
200
+ delay: 3.seconds
201
+ },
202
+ add_foreign_key: {
203
+ attempts: 60,
204
+ lock_timeout: 5.seconds,
205
+ delay: 1.second
206
+ },
207
+ default: {
208
+ attempts: 30,
209
+ lock_timeout: 10.seconds,
210
+ delay: 2.seconds
211
+ }
212
+ }
213
+
214
+ def attempts(command = nil, arguments = [])
215
+ config_for(command)[:attempts]
216
+ end
217
+
218
+ def lock_timeout(attempt, command = nil, arguments = [])
219
+ config_for(command)[:lock_timeout]
220
+ end
221
+
222
+ def delay(attempt, command = nil, arguments = [])
223
+ config_for(command)[:delay]
224
+ end
225
+
226
+ private
227
+
228
+ def config_for(command)
229
+ COMMAND_CONFIGS[command] || COMMAND_CONFIGS[:default]
230
+ end
231
+ end
232
+ end
233
+
234
+ config.lock_retrier = OnlineMigrations::ConfigurableLockRetrier.new
235
+ ```
236
+
237
+ This approach is more concise and easier to maintain when you have simple static configurations per command. The case statement approach (shown above) is better when you need conditional logic or want to use the `attempt` parameter dynamically.
238
+
116
239
  To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable. This is useful when you are deploying a hotfix and do not want to wait too long while the lock retrier safely tries to acquire the lock, but try to acquire the lock immediately with the default configured lock timeout value.
117
240
 
118
241
  To permanently disable lock retries, you can set `lock_retrier` to `nil`.
@@ -36,7 +36,7 @@ module OnlineMigrations
36
36
  # TIMINGS.size
37
37
  # end
38
38
  #
39
- # def lock_timeout(attempt)
39
+ # def lock_timeout(attempt, command = nil, arguments = [])
40
40
  # TIMINGS[attempt - 1][0]
41
41
  # end
42
42
  #
@@ -48,21 +48,34 @@ module OnlineMigrations
48
48
  class LockRetrier
49
49
  # Returns the number of retrying attempts
50
50
  #
51
- def attempts
51
+ # @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
52
+ # Will be nil when called from a transaction-wrapped migration (the default).
53
+ # Only populated for migrations using `disable_ddl_transaction!`.
54
+ # @param _arguments [Array] the arguments passed to the migration method
55
+ #
56
+ def attempts(_command = nil, _arguments = [])
52
57
  raise NotImplementedError
53
58
  end
54
59
 
55
60
  # Returns database lock timeout value (in seconds) for specified attempt number
56
61
  #
57
62
  # @param _attempt [Integer] attempt number
63
+ # @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
64
+ # Will be nil when called from a transaction-wrapped migration (the default).
65
+ # Only populated for migrations using `disable_ddl_transaction!`.
66
+ # @param _arguments [Array] the arguments passed to the migration method
58
67
  #
59
- def lock_timeout(_attempt); end
68
+ def lock_timeout(_attempt, _command = nil, _arguments = []); end
60
69
 
61
70
  # Returns sleep time after unsuccessful lock attempt (in seconds)
62
71
  #
63
72
  # @param _attempt [Integer] attempt number
73
+ # @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
74
+ # Will be nil when called from a transaction-wrapped migration (the default).
75
+ # Only populated for migrations using `disable_ddl_transaction!`.
76
+ # @param _arguments [Array] the arguments passed to the migration method
64
77
  #
65
- def delay(_attempt)
78
+ def delay(_attempt, _command = nil, _arguments = [])
66
79
  raise NotImplementedError
67
80
  end
68
81
 
@@ -77,7 +90,7 @@ module OnlineMigrations
77
90
  # add_column(:users, :name, :string)
78
91
  # end
79
92
  #
80
- def with_lock_retries(connection, &block)
93
+ def with_lock_retries(connection, command = nil, *arguments, &block)
81
94
  return yield if lock_retries_disabled?
82
95
 
83
96
  current_attempt = 0
@@ -85,15 +98,15 @@ module OnlineMigrations
85
98
  begin
86
99
  current_attempt += 1
87
100
 
88
- current_lock_timeout = lock_timeout(current_attempt)
101
+ current_lock_timeout = lock_timeout(current_attempt, command, arguments)
89
102
  if current_lock_timeout
90
103
  with_lock_timeout(connection, current_lock_timeout.in_milliseconds, &block)
91
104
  else
92
105
  yield
93
106
  end
94
107
  rescue ActiveRecord::LockWaitTimeout, ActiveRecord::Deadlocked => e
95
- if current_attempt <= attempts
96
- current_delay = delay(current_attempt)
108
+ if current_attempt <= attempts(command, arguments)
109
+ current_delay = delay(current_attempt, command, arguments)
97
110
 
98
111
  problem = e.is_a?(ActiveRecord::Deadlocked) ? "Deadlock detected." : "Lock timeout."
99
112
  Utils.say("#{problem} Retrying in #{current_delay} seconds...")
@@ -130,13 +143,6 @@ module OnlineMigrations
130
143
  # config.retrier = OnlineMigrations::ConstantLockRetrier.new(attempts: 5, delay: 2.seconds, lock_timeout: 0.05.seconds)
131
144
  #
132
145
  class ConstantLockRetrier < LockRetrier
133
- # LockRetrier API implementation
134
- #
135
- # @return [Integer] Number of retrying attempts
136
- # @see LockRetrier#attempts
137
- #
138
- attr_reader :attempts
139
-
140
146
  # Create a new ConstantLockRetrier instance
141
147
  #
142
148
  # @param attempts [Integer] Maximum number of attempts
@@ -150,12 +156,21 @@ module OnlineMigrations
150
156
  @lock_timeout = lock_timeout
151
157
  end
152
158
 
159
+ # LockRetrier API implementation
160
+ #
161
+ # @return [Integer] Number of retrying attempts
162
+ # @see LockRetrier#attempts
163
+ #
164
+ def attempts(_command = nil, _arguments = [])
165
+ @attempts
166
+ end
167
+
153
168
  # LockRetrier API implementation
154
169
  #
155
170
  # @return [Numeric] Database lock timeout value (in seconds)
156
171
  # @see LockRetrier#lock_timeout
157
172
  #
158
- def lock_timeout(_attempt)
173
+ def lock_timeout(_attempt, _command = nil, _arguments = [])
159
174
  @lock_timeout
160
175
  end
161
176
 
@@ -164,7 +179,7 @@ module OnlineMigrations
164
179
  # @return [Numeric] Sleep time after unsuccessful lock attempt (in seconds)
165
180
  # @see LockRetrier#delay
166
181
  #
167
- def delay(_attempt)
182
+ def delay(_attempt, _command = nil, _arguments = [])
168
183
  @delay
169
184
  end
170
185
  end
@@ -180,13 +195,6 @@ module OnlineMigrations
180
195
  # base_delay: 0.01.seconds, max_delay: 1.minute, lock_timeout: 0.2.seconds)
181
196
  #
182
197
  class ExponentialLockRetrier < LockRetrier
183
- # LockRetrier API implementation
184
- #
185
- # @return [Integer] Number of retrying attempts
186
- # @see LockRetrier#attempts
187
- #
188
- attr_reader :attempts
189
-
190
198
  # Create a new ExponentialLockRetrier instance
191
199
  #
192
200
  # @param attempts [Integer] Maximum number of attempts
@@ -202,12 +210,21 @@ module OnlineMigrations
202
210
  @lock_timeout = lock_timeout
203
211
  end
204
212
 
213
+ # LockRetrier API implementation
214
+ #
215
+ # @return [Integer] Number of retrying attempts
216
+ # @see LockRetrier#attempts
217
+ #
218
+ def attempts(_command = nil, _arguments = [])
219
+ @attempts
220
+ end
221
+
205
222
  # LockRetrier API implementation
206
223
  #
207
224
  # @return [Numeric] Database lock timeout value (in seconds)
208
225
  # @see LockRetrier#lock_timeout
209
226
  #
210
- def lock_timeout(_attempt)
227
+ def lock_timeout(_attempt, _command = nil, _arguments = [])
211
228
  @lock_timeout
212
229
  end
213
230
 
@@ -217,21 +234,21 @@ module OnlineMigrations
217
234
  # @see LockRetrier#delay
218
235
  # @see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
219
236
  #
220
- def delay(attempt)
237
+ def delay(attempt, _command = nil, _arguments = [])
221
238
  (rand * [@max_delay, @base_delay * (2**(attempt - 1))].min).ceil
222
239
  end
223
240
  end
224
241
 
225
242
  # @private
226
243
  class NullLockRetrier < LockRetrier
227
- def attempts(*)
244
+ def attempts(_command = nil, _arguments = [])
228
245
  0
229
246
  end
230
247
 
231
248
  def lock_timeout(*)
232
249
  end
233
250
 
234
- def delay(*)
251
+ def delay(_attempt, _command = nil, _arguments = [])
235
252
  end
236
253
 
237
254
  def with_lock_retries(_connection)
@@ -23,9 +23,9 @@ module OnlineMigrations
23
23
  if in_transaction?
24
24
  super
25
25
  elsif method == :with_lock_retries
26
- connection.with_lock_retries(*args, &block)
26
+ connection.with_lock_retries(method, *args, &block)
27
27
  else
28
- connection.with_lock_retries { super }
28
+ connection.with_lock_retries(method, *args) { super }
29
29
  end
30
30
  end
31
31
  end
@@ -12,6 +12,11 @@ module OnlineMigrations
12
12
  end
13
13
 
14
14
  if use_transaction?(migration)
15
+ # Wrap the entire transaction with lock retries so that if the transaction
16
+ # fails to acquire any locks, the whole migration is retried.
17
+ # Note: at this point we don't have visibility into individual DDL commands,
18
+ # so command and arguments will be nil when lock_timeout is called.
19
+ # For command-specific retry behavior, migrations must use `disable_ddl_transaction!`
15
20
  migration.connection.with_lock_retries do
16
21
  super
17
22
  end
@@ -801,7 +801,7 @@ module OnlineMigrations
801
801
  #
802
802
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
803
803
  #
804
- def add_check_constraint(table_name, expression, **options)
804
+ def add_check_constraint(table_name, expression, if_not_exists: false, **options)
805
805
  if check_constraint_exists?(table_name, expression: expression, **options)
806
806
  Utils.say(<<~MSG.squish)
807
807
  Check constraint was not created because it already exists (this may be due to an aborted migration or similar).
@@ -816,7 +816,7 @@ module OnlineMigrations
816
816
  #
817
817
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
818
818
  #
819
- def remove_check_constraint(table_name, expression = nil, **options)
819
+ def remove_check_constraint(table_name, expression = nil, if_exists: false, **options)
820
820
  if check_constraint_exists?(table_name, expression: expression, **options)
821
821
  super
822
822
  else
@@ -862,11 +862,11 @@ module OnlineMigrations
862
862
  # Executes the block with a retry mechanism that alters the `lock_timeout`
863
863
  # and sleep time between attempts.
864
864
  #
865
- def with_lock_retries(&block)
865
+ def with_lock_retries(command = nil, *args, &block)
866
866
  __ensure_not_in_transaction!
867
867
 
868
868
  retrier = OnlineMigrations.config.lock_retrier
869
- retrier.with_lock_retries(self, &block)
869
+ retrier.with_lock_retries(self, command, *args, &block)
870
870
  end
871
871
 
872
872
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.29.3"
4
+ VERSION = "0.30.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.3
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-08-18 00:00:00.000000000 Z
10
+ date: 2025-10-17 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '7.1'
27
- description:
28
26
  email:
29
27
  - fatkodima123@gmail.com
30
28
  executables: []
@@ -101,7 +99,6 @@ metadata:
101
99
  homepage_uri: https://github.com/fatkodima/online_migrations
102
100
  source_code_uri: https://github.com/fatkodima/online_migrations
103
101
  changelog_uri: https://github.com/fatkodima/online_migrations/blob/master/CHANGELOG.md
104
- post_install_message:
105
102
  rdoc_options: []
106
103
  require_paths:
107
104
  - lib
@@ -116,8 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
113
  - !ruby/object:Gem::Version
117
114
  version: '0'
118
115
  requirements: []
119
- rubygems_version: 3.4.19
120
- signing_key:
116
+ rubygems_version: 3.6.2
121
117
  specification_version: 4
122
118
  summary: Catch unsafe PostgreSQL migrations in development and run them easier in
123
119
  production