active-record-deadlock-handler 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +111 -0
- data/lib/active_record_deadlock_handler/backoff.rb +19 -0
- data/lib/active_record_deadlock_handler/callbacks.rb +38 -0
- data/lib/active_record_deadlock_handler/configuration.rb +32 -0
- data/lib/active_record_deadlock_handler/connection_adapter_patch.rb +23 -0
- data/lib/active_record_deadlock_handler/logging.rb +21 -0
- data/lib/active_record_deadlock_handler/railtie.rb +18 -0
- data/lib/active_record_deadlock_handler/retry_executor.rb +40 -0
- data/lib/active_record_deadlock_handler/version.rb +5 -0
- data/lib/active_record_deadlock_handler.rb +64 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5eb27e3dc7a6c988d2878216e63b5de755cc42233e9630a13a5817d999ad10bb
|
|
4
|
+
data.tar.gz: 7b6a3266f459b1ceb434071c782d54f17262c270781ffb3b562766d361988662
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 33a0a288d9bc1f644c07bf798b4939ecc2871a5b4458c8450364301e1e47b0d9aad9174add38141296c32adeabef7fa81207beeb0bea8f3aa7d2b7d477332ec8
|
|
7
|
+
data.tar.gz: 24e7b8b19e9a7956c11cb57f6decf690290b6ce24c43826e79dacc359c84ab0a26943919c7de4be7616934ac2c605765f4e21d858d25b1f8c687aada64df9b8c
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 afshmini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# ActiveRecordDeadlockHandler
|
|
2
|
+
|
|
3
|
+
A Ruby gem that automatically retries ActiveRecord transactions on deadlock errors with exponential backoff and jitter.
|
|
4
|
+
|
|
5
|
+
Handles:
|
|
6
|
+
- `ActiveRecord::Deadlocked`
|
|
7
|
+
- `PG::TRDeadlockDetected`
|
|
8
|
+
- Any database deadlock that raises through ActiveRecord's exception hierarchy
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to your `Gemfile`:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "active_record_deadlock_handler"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Rails (automatic)
|
|
27
|
+
|
|
28
|
+
The gem hooks into Rails via a Railtie and automatically patches `ActiveRecord::Base.transaction`. No code changes required — all transactions are retried on deadlock by default.
|
|
29
|
+
|
|
30
|
+
Create an initializer to customize behavior:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# config/initializers/active_record_deadlock_handler.rb
|
|
34
|
+
ActiveRecordDeadlockHandler.configure do |config|
|
|
35
|
+
config.max_retries = 3 # number of retries after the first attempt
|
|
36
|
+
config.base_delay = 0.1 # seconds before first retry
|
|
37
|
+
config.max_delay = 5.0 # upper cap on delay
|
|
38
|
+
config.jitter_factor = 0.25 # randomness fraction to avoid thundering herd
|
|
39
|
+
config.log_level = :warn # :debug, :info, :warn, :error
|
|
40
|
+
config.auto_patch = true # set false to disable automatic patching
|
|
41
|
+
config.reraise_after_exhaustion = true # re-raise after all retries fail
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Manual wrapper (without Rails or auto-patch)
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
ActiveRecordDeadlockHandler.with_retry do
|
|
49
|
+
User.find_or_create_by!(email: params[:email])
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Callbacks (e.g. error tracking)
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
ActiveRecordDeadlockHandler.on_deadlock do |exception:, attempt:, **|
|
|
57
|
+
Sentry.capture_exception(exception, extra: { retry_attempt: attempt })
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Multiple callbacks can be registered and are all invoked on each deadlock detection.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
| Option | Default | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `max_retries` | `3` | Number of retries after the initial attempt |
|
|
68
|
+
| `base_delay` | `0.1` | Base sleep duration in seconds before first retry |
|
|
69
|
+
| `max_delay` | `5.0` | Maximum sleep duration in seconds |
|
|
70
|
+
| `jitter_factor` | `0.25` | Random jitter as a fraction of the computed delay (0–1) |
|
|
71
|
+
| `backoff_multiplier` | `2.0` | Exponential growth factor per retry |
|
|
72
|
+
| `logger` | `nil` | Custom logger; falls back to `ActiveRecord::Base.logger` |
|
|
73
|
+
| `log_level` | `:warn` | Log level for deadlock messages |
|
|
74
|
+
| `auto_patch` | `true` | Automatically patch `DatabaseStatements#transaction` |
|
|
75
|
+
| `reraise_after_exhaustion` | `true` | Re-raise the error after all retries are exhausted |
|
|
76
|
+
|
|
77
|
+
### Backoff formula
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
delay = min(base_delay * backoff_multiplier^(attempt - 1), max_delay)
|
|
81
|
+
+ delay * jitter_factor * rand
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Example with defaults — sleep durations before each retry:
|
|
85
|
+
|
|
86
|
+
| Retry | Base delay | With jitter (approx) |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| 1st | 0.1s | 0.10–0.125s |
|
|
89
|
+
| 2nd | 0.2s | 0.20–0.250s |
|
|
90
|
+
| 3rd | 0.4s | 0.40–0.500s |
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
The gem `prepend`s a module into `ActiveRecord::ConnectionAdapters::DatabaseStatements`, wrapping the `#transaction` method with retry logic. Only the **outermost** transaction is wrapped — nested transactions (savepoints) pass through untouched, because a deadlock always invalidates the entire outer transaction. Retrying a savepoint inside a dead transaction would corrupt state.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
User.transaction do # ← retry loop installed here
|
|
98
|
+
account.transaction do # ← savepoint, passes through
|
|
99
|
+
account.update!(balance: 0)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# If deadlock fires: entire outer block is re-executed
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT — see [LICENSE](LICENSE).
|
|
108
|
+
|
|
109
|
+
## Contributing
|
|
110
|
+
|
|
111
|
+
Bug reports and pull requests are welcome at https://github.com/afshmini/deadlock-handler.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
module Backoff
|
|
5
|
+
# Computes sleep duration for a given retry attempt using exponential backoff with jitter.
|
|
6
|
+
#
|
|
7
|
+
# Formula: min(base_delay * multiplier^(attempt-1), max_delay) + jitter
|
|
8
|
+
# Jitter is a random fraction of the computed exponential delay to reduce thundering herd.
|
|
9
|
+
#
|
|
10
|
+
# @param attempt [Integer] 1-indexed retry attempt number
|
|
11
|
+
# @param config [Configuration]
|
|
12
|
+
# @return [Float] seconds to sleep
|
|
13
|
+
def self.compute(attempt:, config:)
|
|
14
|
+
exponential = [config.base_delay * (config.backoff_multiplier**(attempt - 1)), config.max_delay].min
|
|
15
|
+
jitter = exponential * config.jitter_factor * rand
|
|
16
|
+
exponential + jitter
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
module Callbacks
|
|
5
|
+
LOCK = Mutex.new
|
|
6
|
+
private_constant :LOCK
|
|
7
|
+
|
|
8
|
+
@handlers = []
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Register a callback to be invoked on each deadlock detection.
|
|
12
|
+
#
|
|
13
|
+
# @yieldparam exception [ActiveRecord::Deadlocked] the deadlock error
|
|
14
|
+
# @yieldparam attempt [Integer] which retry attempt triggered this (1-indexed)
|
|
15
|
+
# @yieldparam config [Configuration]
|
|
16
|
+
def register(&block)
|
|
17
|
+
raise ArgumentError, "block required" unless block
|
|
18
|
+
|
|
19
|
+
LOCK.synchronize { @handlers << block }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear
|
|
23
|
+
LOCK.synchronize { @handlers.clear }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Invokes all registered callbacks with deadlock context.
|
|
27
|
+
# Runs outside the lock so slow callbacks don't block registration.
|
|
28
|
+
def run(exception:, attempt:, config:)
|
|
29
|
+
handlers = LOCK.synchronize { @handlers.dup }
|
|
30
|
+
handlers.each do |handler|
|
|
31
|
+
handler.call(exception: exception, attempt: attempt, config: config)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
warn "[ActiveRecordDeadlockHandler] Callback raised an error: #{e.class}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULTS = {
|
|
6
|
+
max_retries: 3,
|
|
7
|
+
base_delay: 0.1,
|
|
8
|
+
max_delay: 5.0,
|
|
9
|
+
jitter_factor: 0.25,
|
|
10
|
+
backoff_multiplier: 2.0,
|
|
11
|
+
logger: nil,
|
|
12
|
+
log_level: :warn,
|
|
13
|
+
auto_patch: true,
|
|
14
|
+
reraise_after_exhaustion: true
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_accessor(*DEFAULTS.keys)
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
DEFAULTS.each { |k, v| public_send(:"#{k}=", v) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate!
|
|
24
|
+
raise ArgumentError, "max_retries must be >= 0" unless max_retries >= 0
|
|
25
|
+
raise ArgumentError, "base_delay must be > 0" unless base_delay > 0
|
|
26
|
+
raise ArgumentError, "max_delay must be >= base_delay" unless max_delay >= base_delay
|
|
27
|
+
raise ArgumentError, "jitter_factor must be between 0 and 1" unless (0.0..1.0).cover?(jitter_factor)
|
|
28
|
+
raise ArgumentError, "backoff_multiplier must be >= 1" unless backoff_multiplier >= 1
|
|
29
|
+
raise ArgumentError, "log_level must be a symbol" unless log_level.is_a?(Symbol)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
# Prepended into ActiveRecord::ConnectionAdapters::DatabaseStatements to wrap
|
|
5
|
+
# every top-level transaction with deadlock retry logic.
|
|
6
|
+
module ConnectionAdapterPatch
|
|
7
|
+
# Wraps transaction with retry logic only at the outermost transaction level.
|
|
8
|
+
#
|
|
9
|
+
# Nested transactions (savepoints) are NOT wrapped individually — when a deadlock
|
|
10
|
+
# kills a transaction, the entire outer transaction is invalidated. Retrying only
|
|
11
|
+
# a savepoint inside a dead transaction would corrupt state. The outer retry loop
|
|
12
|
+
# handles the full re-execution.
|
|
13
|
+
def transaction(**options, &block)
|
|
14
|
+
# open_transactions > 0 means we're already inside a real transaction.
|
|
15
|
+
# Let it pass through; the outermost call's retry loop covers everything.
|
|
16
|
+
return super(**options, &block) if open_transactions > 0
|
|
17
|
+
|
|
18
|
+
RetryExecutor.run(config: ActiveRecordDeadlockHandler.configuration) do
|
|
19
|
+
super(**options, &block)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
module Logging
|
|
5
|
+
def self.log(message, exception:, attempt:, config:)
|
|
6
|
+
logger = resolve_logger(config)
|
|
7
|
+
return unless logger
|
|
8
|
+
|
|
9
|
+
error_summary = exception.message.lines.first&.strip
|
|
10
|
+
full_message = "[ActiveRecordDeadlockHandler] #{message} " \
|
|
11
|
+
"(attempt=#{attempt}, error=#{exception.class}: #{error_summary})"
|
|
12
|
+
|
|
13
|
+
logger.public_send(config.log_level, full_message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.resolve_logger(config)
|
|
17
|
+
config.logger || (defined?(ActiveRecord::Base) && ActiveRecord::Base.logger)
|
|
18
|
+
end
|
|
19
|
+
private_class_method :resolve_logger
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module ActiveRecordDeadlockHandler
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "active_record_deadlock_handler.configure_active_record" do
|
|
8
|
+
# Wait until ActiveRecord is fully loaded before patching.
|
|
9
|
+
ActiveSupport.on_load(:active_record) do
|
|
10
|
+
if ActiveRecordDeadlockHandler.configuration.auto_patch
|
|
11
|
+
ActiveRecord::ConnectionAdapters::DatabaseStatements.prepend(
|
|
12
|
+
ActiveRecordDeadlockHandler::ConnectionAdapterPatch
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDeadlockHandler
|
|
4
|
+
module RetryExecutor
|
|
5
|
+
RETRYABLE_ERRORS = [ActiveRecord::Deadlocked].freeze
|
|
6
|
+
|
|
7
|
+
# Executes the given block, retrying on deadlock errors with exponential backoff.
|
|
8
|
+
#
|
|
9
|
+
# Thread-safe: all state (attempt counter, delay) is local to the call stack.
|
|
10
|
+
#
|
|
11
|
+
# @param config [Configuration]
|
|
12
|
+
# @yieldreturn [Object] the return value of the block on success
|
|
13
|
+
# @raise [ActiveRecord::Deadlocked] if max_retries is exhausted and reraise_after_exhaustion is true
|
|
14
|
+
def self.run(config: ActiveRecordDeadlockHandler.configuration, &block)
|
|
15
|
+
attempt = 0
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
attempt += 1
|
|
19
|
+
yield
|
|
20
|
+
rescue *RETRYABLE_ERRORS => e
|
|
21
|
+
Callbacks.run(exception: e, attempt: attempt, config: config)
|
|
22
|
+
|
|
23
|
+
if attempt <= config.max_retries
|
|
24
|
+
Logging.log("Deadlock detected, retrying...", exception: e, attempt: attempt, config: config)
|
|
25
|
+
delay = Backoff.compute(attempt: attempt, config: config)
|
|
26
|
+
sleep(delay)
|
|
27
|
+
retry
|
|
28
|
+
else
|
|
29
|
+
Logging.log(
|
|
30
|
+
"Deadlock detected, max retries (#{config.max_retries}) exhausted.",
|
|
31
|
+
exception: e,
|
|
32
|
+
attempt: attempt,
|
|
33
|
+
config: config
|
|
34
|
+
)
|
|
35
|
+
raise if config.reraise_after_exhaustion
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require_relative "active_record_deadlock_handler/version"
|
|
5
|
+
require_relative "active_record_deadlock_handler/configuration"
|
|
6
|
+
require_relative "active_record_deadlock_handler/backoff"
|
|
7
|
+
require_relative "active_record_deadlock_handler/callbacks"
|
|
8
|
+
require_relative "active_record_deadlock_handler/logging"
|
|
9
|
+
require_relative "active_record_deadlock_handler/retry_executor"
|
|
10
|
+
require_relative "active_record_deadlock_handler/connection_adapter_patch"
|
|
11
|
+
require_relative "active_record_deadlock_handler/railtie" if defined?(Rails::Railtie)
|
|
12
|
+
|
|
13
|
+
module ActiveRecordDeadlockHandler
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
@configuration = Configuration.new
|
|
17
|
+
@configuration_mutex = Mutex.new
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Configure the gem. Should be called once from an initializer.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# ActiveRecordDeadlockHandler.configure do |config|
|
|
24
|
+
# config.max_retries = 5
|
|
25
|
+
# config.base_delay = 0.05
|
|
26
|
+
# config.log_level = :warn
|
|
27
|
+
# end
|
|
28
|
+
def configure
|
|
29
|
+
@configuration_mutex.synchronize do
|
|
30
|
+
yield @configuration
|
|
31
|
+
@configuration.validate!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register a callback invoked on every deadlock detection.
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# ActiveRecordDeadlockHandler.on_deadlock do |exception:, attempt:, **|
|
|
43
|
+
# Sentry.capture_exception(exception, extra: { retry_attempt: attempt })
|
|
44
|
+
# end
|
|
45
|
+
def on_deadlock(&block)
|
|
46
|
+
Callbacks.register(&block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Opt-in wrapper for use without Rails or when auto_patch is disabled.
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# ActiveRecordDeadlockHandler.with_retry { User.find_or_create_by!(email: email) }
|
|
53
|
+
def with_retry(config: configuration, &block)
|
|
54
|
+
RetryExecutor.run(config: config, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Resets configuration to defaults. Primarily useful in tests.
|
|
58
|
+
def reset_configuration!
|
|
59
|
+
@configuration_mutex.synchronize do
|
|
60
|
+
@configuration = Configuration.new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active-record-deadlock-handler
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- afshmini
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: sqlite3
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.4'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.4'
|
|
69
|
+
description: |
|
|
70
|
+
Transparently retries ActiveRecord transactions on ActiveRecord::Deadlocked errors
|
|
71
|
+
(including PG::TRDeadlockDetected) with configurable exponential backoff and jitter.
|
|
72
|
+
Provides hooks for custom callbacks, structured logging, and Rails initializer support.
|
|
73
|
+
email:
|
|
74
|
+
- afshmini@gmail.com
|
|
75
|
+
executables: []
|
|
76
|
+
extensions: []
|
|
77
|
+
extra_rdoc_files: []
|
|
78
|
+
files:
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- lib/active_record_deadlock_handler.rb
|
|
82
|
+
- lib/active_record_deadlock_handler/backoff.rb
|
|
83
|
+
- lib/active_record_deadlock_handler/callbacks.rb
|
|
84
|
+
- lib/active_record_deadlock_handler/configuration.rb
|
|
85
|
+
- lib/active_record_deadlock_handler/connection_adapter_patch.rb
|
|
86
|
+
- lib/active_record_deadlock_handler/logging.rb
|
|
87
|
+
- lib/active_record_deadlock_handler/railtie.rb
|
|
88
|
+
- lib/active_record_deadlock_handler/retry_executor.rb
|
|
89
|
+
- lib/active_record_deadlock_handler/version.rb
|
|
90
|
+
homepage: https://github.com/afshmini/active-record-deadlock-handler
|
|
91
|
+
licenses:
|
|
92
|
+
- MIT
|
|
93
|
+
metadata:
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
95
|
+
post_install_message:
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 2.7.0
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 3.5.20
|
|
111
|
+
signing_key:
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Automatic deadlock retry with exponential backoff for ActiveRecord
|
|
114
|
+
test_files: []
|