sidekiq-transaction_guard 1.0.3 → 1.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 +4 -4
- data/AGENTS.md +13 -0
- data/CHANGELOG.md +17 -0
- data/README.md +132 -23
- data/VERSION +1 -1
- data/lib/sidekiq/transaction_guard/database_cleaner.rb +4 -1
- data/lib/sidekiq/transaction_guard/middleware.rb +17 -16
- data/lib/sidekiq/transaction_guard/minitest.rb +63 -0
- data/lib/sidekiq/transaction_guard/railtie.rb +10 -0
- data/lib/sidekiq/transaction_guard/rspec.rb +38 -0
- data/lib/sidekiq/transaction_guard.rb +68 -15
- data/sidekiq-transaction_guard.gemspec +10 -3
- metadata +16 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 118711493ed2b0a682e2245f00fe8f204aa3e3212d51187c8482d2c526898087
|
|
4
|
+
data.tar.gz: 23efcba40a7c8f96e95045e489387ab6199ef0fc951d4b9f10b4894bf89c166c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c3b67986b8662c2ce6ba291a2ca927e2727fe6a942a4a77fac5291d2f52374d01d8007e1902e6c69c9b93487f2ac0bde71f4455a4b76d80c0b061e0cc140efe
|
|
7
|
+
data.tar.gz: 62dc769e7881266b57219f6c686adcd2db301c16ec66035913e874428ed2293686c073806e4c8dc6d8ec885420ea4be02056dae3f225c5e3ffc68b2d5f13bbba
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Coding style
|
|
2
|
+
|
|
3
|
+
Always include the # frozen_string_literal: true magic comment at the top of each ruby file.
|
|
4
|
+
|
|
5
|
+
Use `class << self` syntax for defining class methods. instead of `def self.method_name`.
|
|
6
|
+
|
|
7
|
+
All public methods should have YARD documentation. Include an empty comment line between the method description and the first YARD tag.
|
|
8
|
+
|
|
9
|
+
This project uses the standardrb style guide. Run `bundle exec standardrb --fix` to automatically fix style issues.
|
|
10
|
+
|
|
11
|
+
## Testing
|
|
12
|
+
|
|
13
|
+
Run the test suite with `bundle exec rspec`.
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.1.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Sidekiq::TransactionGuard.testing` now automatically sets the allowed transaction level when the block begins. This provides better support transactional fixtures in test environments.
|
|
12
|
+
- Added `Sidekiq::TransactionGuard.disable` method to allow temporarily disabling the transaction guard within a block. This is useful in test environments when you want to setup data for your tests without worrying about transaction levels.
|
|
13
|
+
- Added `count` parameter to `set_allowed_transaction_level` to allow setting the allowed transaction level explicitly. This is useful for test setups where the transaction level cannot be determined automatically, such as when using ActiveRecord transactional fixtures.
|
|
14
|
+
- Added Railtie for automatic integration with Rails applications.
|
|
15
|
+
- Added helpers for easier testing setup with RSpec.
|
|
16
|
+
- Added `Sidekiq::TransactionGuard::Middleware.init` method to simplify middleware initialization.
|
|
17
|
+
- Added minitest helper module for easier integration with Minitest test suites.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Removed support for ActiveRecord versions prior to 6.0.
|
|
22
|
+
- Removed support for Sidekiq versions prior to 6.0.
|
|
23
|
+
|
|
7
24
|
## 1.0.3
|
|
8
25
|
|
|
9
26
|
### Changed
|
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Sidekiq Transaction Guard
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/continuous_integration.yml)
|
|
4
|
-
[](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/regression_test.yml)
|
|
5
4
|
[](https://github.com/testdouble/standard)
|
|
6
5
|
[](https://badge.fury.io/rb/sidekiq-transaction_guard)
|
|
7
6
|
|
|
@@ -31,11 +30,11 @@ class PostCreatedWorker
|
|
|
31
30
|
end
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
In this case, the `PostCreatedWorker` job will be created for a new `Post` record in Sidekiq before the data is actually written to the database. If Sidekiq picks up that worker and tries to execute it before the transaction is committed, `Post.find_by(id: post_id)` won't find anything and the worker will exit without performing
|
|
33
|
+
In this case, the `PostCreatedWorker` job will be created for a new `Post` record in Sidekiq before the data is actually written to the database. If Sidekiq picks up that worker and tries to execute it before the transaction is committed, `Post.find_by(id: post_id)` won't find anything and the worker will exit without performing its task. Even if the worker doesn't need to read from the database, there is still a chance for an error to rollback the transaction leaving a possibility of workers running that should not have been scheduled.
|
|
35
34
|
|
|
36
35
|
To solve this, workers like this should be invoked in ActiveRecord from an `after_commit` callback. These callbacks are guaranteed to only execute after the data has been written to the database. However, as your application grows and gets more complicated, it can be difficult to ensure that workers are not being scheduled in the middle of transactions.
|
|
37
36
|
|
|
38
|
-
Switching from callbacks to service objects won't help you either, because service objects can be wrapped in transactions as well.
|
|
37
|
+
Switching from callbacks to service objects won't help you either, because service objects can be wrapped in transactions as well. They will just give you a new problem to solve.
|
|
39
38
|
|
|
40
39
|
```ruby
|
|
41
40
|
class CreatePost
|
|
@@ -44,35 +43,39 @@ class CreatePost
|
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
def call
|
|
47
|
-
post = Post.create!(attributes)
|
|
46
|
+
post = Post.create!(@attributes)
|
|
48
47
|
PostCreatedWorker.perform_async(post.id)
|
|
49
48
|
end
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
# Still calling `perform_async` inside a transaction.
|
|
53
52
|
Post.transaction do
|
|
54
|
-
CreatePost.new(post_1_attributes)
|
|
55
|
-
CreatePost.new(post_2_attributes)
|
|
53
|
+
CreatePost.new(post_1_attributes).call
|
|
54
|
+
CreatePost.new(post_2_attributes).call
|
|
56
55
|
end
|
|
57
56
|
```
|
|
58
57
|
|
|
59
58
|
## The Solution
|
|
60
59
|
|
|
61
|
-
You can use this gem to add Sidekiq client middleware that will either warn you or raise an error when workers are scheduled inside of a database transaction.
|
|
60
|
+
You can use this gem to add Sidekiq client middleware that will either warn you or raise an error when workers are scheduled inside of a database transaction.
|
|
61
|
+
|
|
62
|
+
### Rails Applications
|
|
63
|
+
|
|
64
|
+
If you're using Rails, the middleware is automatically added via a Railtie. The default mode will be `:error` in development and test environments, and `:warn` in production. You don't need any additional configuration, though you can customize the mode as described below.
|
|
65
|
+
|
|
66
|
+
### Non-Rails Applications
|
|
67
|
+
|
|
68
|
+
For non-Rails applications, you need to manually add the middleware in your application's initialization code:
|
|
62
69
|
|
|
63
70
|
```ruby
|
|
64
71
|
require 'sidekiq/transaction_guard'
|
|
65
72
|
|
|
66
|
-
Sidekiq.
|
|
67
|
-
config.client_middleware do |chain|
|
|
68
|
-
chain.add(Sidekiq::TransactionGuard::Middleware)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
73
|
+
Sidekiq::TransactionGuard::Middleware.init
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
### Mode
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
You can set the mode at any time. The mode can be one of `[:warn, :stderr, :error, :disabled]`.
|
|
76
79
|
|
|
77
80
|
```ruby
|
|
78
81
|
# Raise errors
|
|
@@ -88,7 +91,13 @@ Sidekiq::TransactionGuard.mode = :warn
|
|
|
88
91
|
Sidekiq::TransactionGuard.mode = :disabled
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
You can
|
|
94
|
+
You can set the mode when initializing the middleware:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Sidekiq::TransactionGuard::Middleware.init(mode: :error)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can also set the mode on individual worker classes with `sidekiq_options transaction_guard: mode`. The worker-specific mode will override the global mode.
|
|
92
101
|
|
|
93
102
|
```ruby
|
|
94
103
|
class SomeWorker
|
|
@@ -98,23 +107,26 @@ class SomeWorker
|
|
|
98
107
|
end
|
|
99
108
|
```
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
You can use the `:disabled` mode to allow individual worker classes to be scheduled inside of transactions where the worker logic doesn't care about the state of the database. For instance, if you use a Sidekiq worker to report errors, you would want to all it inside of transactions. If you don't control the worker you want to change the mode on, you simply call this in an initializer:
|
|
110
|
+
You can use the `:disabled` mode to allow individual worker classes to be scheduled inside of transactions where the worker logic doesn't care about the state of the database. For instance, if you use a Sidekiq worker to report errors, you would want to allow it inside of transactions. If you don't control the worker you want to change the mode on, you can simply call this in an initializer:
|
|
103
111
|
|
|
104
112
|
```ruby
|
|
105
113
|
SomeWorker.sidekiq_options.merge(transaction_guard: :disabled)
|
|
106
114
|
```
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
#### Default Modes
|
|
117
|
+
|
|
118
|
+
**Rails applications**: The default mode is `:error` in development and test environments, and `:warn` in production or other environments.
|
|
119
|
+
|
|
120
|
+
**Non-Rails applications**: The default mode is `:stderr` if `ENV["RAILS_ENV"]` or `ENV["RACK_ENV"]` is set to `"test"`, otherwise `:warn`.
|
|
109
121
|
|
|
110
122
|
### Notification Handlers
|
|
111
123
|
|
|
112
|
-
You can also set a block to be called if a worker is scheduled inside of a transaction. This can be useful if you use an error logging service to notify you of problematic calls in production so you can fix them.
|
|
124
|
+
You can also set a block to be called if a worker is scheduled inside of a transaction. This can be useful if you use an error logging service to notify you of problematic calls in production so you can fix them. Note that notification handlers are only called when the mode is `:warn` or `:stderr` (not when mode is `:error` or `:disabled`).
|
|
113
125
|
|
|
114
126
|
```ruby
|
|
115
127
|
# Define a global notify handler
|
|
116
128
|
Sidekiq::TransactionGuard.notify do |job|
|
|
117
|
-
# Do
|
|
129
|
+
# Do whatever you need to. The job argument will be a Sidekiq job hash.
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
# Define on a per worker level
|
|
@@ -138,7 +150,7 @@ Out of the box, this gem only deals with one database and monitors the connectio
|
|
|
138
150
|
|
|
139
151
|
```ruby
|
|
140
152
|
class MyClass < ActiveRecord::Base
|
|
141
|
-
# This
|
|
153
|
+
# This establishes a new connection pool.
|
|
142
154
|
establish_connection(configurations["otherdb"])
|
|
143
155
|
end
|
|
144
156
|
|
|
@@ -147,9 +159,35 @@ Sidekiq::TransactionGuard.add_connection_class(MyClass)
|
|
|
147
159
|
|
|
148
160
|
The class is used to get to the connection pool used for the class. You only need to add one class per connection pool, so you don't need to add any subclasses of `MyClass`.
|
|
149
161
|
|
|
150
|
-
##
|
|
162
|
+
## Transactional Fixtures In Tests
|
|
163
|
+
|
|
164
|
+
If you're using transaction fixtures in your tests, there will always be a database transaction open.
|
|
165
|
+
|
|
166
|
+
### Rails Transactional Fixtures
|
|
167
|
+
|
|
168
|
+
When using Rails transactional fixtures, you'll need to wrap each test in a `Sidekiq::TransactionGuard.testing` block and set the number of transaction levels to ignore.
|
|
169
|
+
|
|
170
|
+
### RSpec Support
|
|
151
171
|
|
|
152
|
-
If you're using
|
|
172
|
+
If you're using RSpec, you can use the built-in RSpec helper to automatically set up the hooks to deal with transactional fixtures. Add this line to your `spec_helper.rb` or `rails_helper.rb` file:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
require 'sidekiq/transaction_guard/rspec'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This will also add support for adding a metadata tag to your specs to control the transaction guard mode on a per-spec basis. For example:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
RSpec.describe "Some feature", sidekiq_transaction_guard: :disabled do
|
|
182
|
+
it "does something that schedules workers inside transactions" do
|
|
183
|
+
# ...
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### DatabaseCleaner Support
|
|
189
|
+
|
|
190
|
+
If you're using [DatabaseCleaner](https://github.com/DatabaseCleaner/database_cleaner) in your tests, you just need to include this snippet in your test suite initializer:
|
|
153
191
|
|
|
154
192
|
```ruby
|
|
155
193
|
require 'sidekiq/transaction_guard/database_cleaner'
|
|
@@ -157,4 +195,75 @@ require 'sidekiq/transaction_guard/database_cleaner'
|
|
|
157
195
|
|
|
158
196
|
This will add the appropriate code so that the surrounding transaction in the test suite is ignored (i.e. workers will only warn/error if there is more than one open transaction).
|
|
159
197
|
|
|
160
|
-
|
|
198
|
+
### Minitest Support
|
|
199
|
+
|
|
200
|
+
If you're using Minitest with `ActiveSupport::TestCase` (Rails default), you can use the built-in Minitest helper to automatically set up the hooks for transactional fixtures. Add this line to your `test_helper.rb` file:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
require 'sidekiq/transaction_guard/minitest'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This will automatically wrap each test in the appropriate `testing` block and handle transactional fixtures.
|
|
207
|
+
|
|
208
|
+
If you're using plain Minitest (without `ActiveSupport::TestCase`), you can manually include the helper module:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class MyTests < Minitest::Test
|
|
212
|
+
include Sidekiq::TransactionGuard::MinitestHelper
|
|
213
|
+
|
|
214
|
+
def test_something
|
|
215
|
+
# Test code here
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Alternatively, you can manually use the `testing` method with minitest-hooks:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
class MyTests < Minitest::Test
|
|
224
|
+
# Using minitest-hooks gem
|
|
225
|
+
def around(&block)
|
|
226
|
+
Sidekiq::TransactionGuard.testing(base_transaction_level: 1) do
|
|
227
|
+
block.call
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Disabling When Setting Up Test Data
|
|
234
|
+
|
|
235
|
+
If you have test setup code that is triggering the transaction guard with false positives, you can temporarily disable the transaction guard within a block:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
Sidekiq::TransactionGuard.disable do
|
|
239
|
+
# Code that schedules workers inside transactions, such as test setup code.
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Installation
|
|
244
|
+
|
|
245
|
+
Add this line to your application's Gemfile:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
gem "sidekiq-transaction_guard"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
And then execute:
|
|
252
|
+
```bash
|
|
253
|
+
$ bundle install
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Or install it yourself as:
|
|
257
|
+
```bash
|
|
258
|
+
$ gem install sidekiq-transaction_guard
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
Open a pull request on GitHub.
|
|
264
|
+
|
|
265
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0
|
|
1
|
+
1.1.0
|
|
@@ -9,15 +9,18 @@ module Sidekiq
|
|
|
9
9
|
# Override the start method to set the base number of allowed transactions to
|
|
10
10
|
# the current level. Anything above this number will then be considered to be
|
|
11
11
|
# in a transaction.
|
|
12
|
+
#
|
|
13
|
+
# @return [Object] the return value from the superclass's start method
|
|
12
14
|
def start
|
|
13
15
|
retval = super
|
|
14
16
|
Sidekiq::TransactionGuard.set_allowed_transaction_level(connection_class)
|
|
15
17
|
retval
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
# Wrap the `Sidekiq::TransactionGuard.testing` which sets up the data structures
|
|
20
|
+
# Wrap the `Sidekiq::TransactionGuard.testing` method which sets up the data structures
|
|
19
21
|
# needed for custom counting of the transaction level within a test block.
|
|
20
22
|
#
|
|
23
|
+
# @yield the cleaning block to execute
|
|
21
24
|
# @return [Object] the return value of the block
|
|
22
25
|
def cleaning(&block)
|
|
23
26
|
Sidekiq::TransactionGuard.testing { super(&block) }
|
|
@@ -10,9 +10,7 @@ module Sidekiq
|
|
|
10
10
|
# the default behavior set in `Sidekiq::TransactionGuard.mode` and
|
|
11
11
|
# `Sidekiq::TransactionGuard.notify` respectively.
|
|
12
12
|
class Middleware
|
|
13
|
-
|
|
14
|
-
include Sidekiq::ClientMiddleware
|
|
15
|
-
end
|
|
13
|
+
include Sidekiq::ClientMiddleware if defined?(Sidekiq::ClientMiddleware)
|
|
16
14
|
|
|
17
15
|
def call(worker_class, job, queue, redis_pool)
|
|
18
16
|
# Check if we need to log this. Also, convert worker_class to its actual class
|
|
@@ -66,20 +64,23 @@ module Sidekiq
|
|
|
66
64
|
|
|
67
65
|
def log_transaction(worker_class, job)
|
|
68
66
|
mode = worker_mode(job)
|
|
69
|
-
if mode
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
logger.warn(message)
|
|
77
|
-
else
|
|
78
|
-
$stderr.write("WARNING #{message}\n")
|
|
79
|
-
end
|
|
80
|
-
notify!(worker_class, job)
|
|
81
|
-
end
|
|
67
|
+
return if mode == :disabled
|
|
68
|
+
|
|
69
|
+
message = "#{worker_class.name} was called from inside a database transaction. " \
|
|
70
|
+
"Resolve by moving the job outside the transaction, using an after_commit callback, " \
|
|
71
|
+
"or setting `sidekiq_options transaction_guard: :disabled` if the job is safe to run before the transaction commits."
|
|
72
|
+
if mode == :error
|
|
73
|
+
raise Sidekiq::TransactionGuard::InsideTransactionError.new(message)
|
|
82
74
|
end
|
|
75
|
+
|
|
76
|
+
logger = Sidekiq.logger unless mode == :stderr
|
|
77
|
+
if logger
|
|
78
|
+
logger.warn(message)
|
|
79
|
+
else
|
|
80
|
+
$stderr.write("WARNING #{message}\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
notify!(worker_class, job)
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest"
|
|
4
|
+
|
|
5
|
+
module Sidekiq
|
|
6
|
+
module TransactionGuard
|
|
7
|
+
# Minitest helper module for testing with Sidekiq::TransactionGuard.
|
|
8
|
+
#
|
|
9
|
+
# Include this module in your test class to automatically wrap tests in the
|
|
10
|
+
# Sidekiq::TransactionGuard.testing block and handle transactional fixtures.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# class MyTest < Minitest::Test
|
|
14
|
+
# include Sidekiq::TransactionGuard::MinitestHelper
|
|
15
|
+
#
|
|
16
|
+
# def test_something
|
|
17
|
+
# # Test code here
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module MinitestHelper
|
|
21
|
+
def self.included(base)
|
|
22
|
+
base.class_eval do
|
|
23
|
+
# Save the original mode before the test suite runs
|
|
24
|
+
@@sidekiq_transaction_guard_mode = Sidekiq::TransactionGuard.mode
|
|
25
|
+
Sidekiq::TransactionGuard.mode = :disabled
|
|
26
|
+
|
|
27
|
+
def setup
|
|
28
|
+
@sidekiq_transaction_guard_saved_mode = Sidekiq::TransactionGuard.mode
|
|
29
|
+
Sidekiq::TransactionGuard.mode = :error
|
|
30
|
+
Sidekiq::TransactionGuard.testing do
|
|
31
|
+
@sidekiq_transaction_guard_testing_block = true
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def teardown
|
|
37
|
+
super
|
|
38
|
+
Sidekiq::TransactionGuard.mode = @sidekiq_transaction_guard_saved_mode
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If using ActiveSupport::TestCase, automatically include the helper
|
|
47
|
+
if defined?(ActiveSupport::TestCase)
|
|
48
|
+
ActiveSupport::TestCase.class_eval do
|
|
49
|
+
def setup
|
|
50
|
+
@sidekiq_transaction_guard_saved_mode = Sidekiq::TransactionGuard.mode
|
|
51
|
+
Sidekiq::TransactionGuard.mode = :error
|
|
52
|
+
|
|
53
|
+
Sidekiq::TransactionGuard.testing do
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def teardown
|
|
59
|
+
super
|
|
60
|
+
Sidekiq::TransactionGuard.mode = @sidekiq_transaction_guard_saved_mode
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sidekiq::TransactionGuard
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
initializer "sidekiq.transaction_guard" do
|
|
6
|
+
mode = (Rails.env.development? || Rails.env.test?) ? :error : :warn
|
|
7
|
+
Sidekiq::TransactionGuard.init(mode: mode)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.configure do |config|
|
|
4
|
+
global_sidekiq_transaction_guard_mode = nil
|
|
5
|
+
|
|
6
|
+
# Disable by default to avoid raising errors in test setup and teardown.
|
|
7
|
+
config.before(:suite) do
|
|
8
|
+
global_sidekiq_transaction_guard_mode = Sidekiq::TransactionGuard.mode
|
|
9
|
+
Sidekiq::TransactionGuard.mode = :disabled
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
config.after(:suite) do
|
|
13
|
+
Sidekiq::TransactionGuard.mode = global_sidekiq_transaction_guard_mode
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
config.around do |example|
|
|
17
|
+
mode = example.metadata[:sidekiq_transaction_guard]
|
|
18
|
+
mode = :disabled if mode == false
|
|
19
|
+
mode = global_sidekiq_transaction_guard_mode if mode == :default
|
|
20
|
+
mode = :error unless mode.is_a?(Symbol)
|
|
21
|
+
|
|
22
|
+
save_val = Sidekiq::TransactionGuard.mode
|
|
23
|
+
begin
|
|
24
|
+
Sidekiq::TransactionGuard.mode = mode
|
|
25
|
+
Sidekiq::TransactionGuard.testing do
|
|
26
|
+
example.run
|
|
27
|
+
end
|
|
28
|
+
ensure
|
|
29
|
+
Sidekiq::TransactionGuard.mode = save_val
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Re-snapshot the allowed transaction level after all setup (including
|
|
34
|
+
# transactional fixtures) has run so that setup transactions are ignored.
|
|
35
|
+
config.before(:each) do
|
|
36
|
+
Sidekiq::TransactionGuard.set_allowed_transaction_level(:all)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_record"
|
|
3
4
|
require "sidekiq"
|
|
4
5
|
require "set"
|
|
5
6
|
|
|
@@ -18,16 +19,31 @@ module Sidekiq
|
|
|
18
19
|
class << self
|
|
19
20
|
VALID_MODES = [:warn, :stderr, :error, :disabled].freeze
|
|
20
21
|
|
|
22
|
+
# Helper method to add the client middleware to Sidekiq.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def init(mode: nil)
|
|
26
|
+
self.mode = mode if mode
|
|
27
|
+
|
|
28
|
+
Sidekiq.configure_client do |config|
|
|
29
|
+
config.client_middleware do |chain|
|
|
30
|
+
unless chain.exists?(Sidekiq::TransactionGuard::Middleware)
|
|
31
|
+
chain.add Sidekiq::TransactionGuard::Middleware
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
21
37
|
# Set the global mode to one of `[:warn, :stderr, :error, :disabled]`. The
|
|
22
38
|
# default mode is `:warn`. This controls the behavior of workers enqueued
|
|
23
39
|
# inside of transactions.
|
|
24
40
|
# * :warn - Log to Sidekiq.logger
|
|
25
41
|
# * :stderr - Log to STDERR
|
|
26
|
-
# * :error -
|
|
42
|
+
# * :error - Raise a `Sidekiq::TransactionGuard::InsideTransactionError`
|
|
27
43
|
# * :disabled - Allow workers inside of transactions
|
|
28
44
|
#
|
|
29
|
-
# @param
|
|
30
|
-
# @return [
|
|
45
|
+
# @param symbol [Symbol] one of `:warn`, `:stderr`, `:error`, or `:disabled`
|
|
46
|
+
# @return [Symbol] the mode that was set
|
|
31
47
|
def mode=(symbol)
|
|
32
48
|
if VALID_MODES.include?(symbol)
|
|
33
49
|
@mode = symbol
|
|
@@ -45,6 +61,7 @@ module Sidekiq
|
|
|
45
61
|
# job hash for all jobs enqueued inside transactions if the mode is `:warn`
|
|
46
62
|
# or `:stderr`.
|
|
47
63
|
#
|
|
64
|
+
# @yield [Hash] the Sidekiq job hash
|
|
48
65
|
# @return [void]
|
|
49
66
|
def notify(&block)
|
|
50
67
|
@notify = block
|
|
@@ -52,30 +69,33 @@ module Sidekiq
|
|
|
52
69
|
|
|
53
70
|
# Return the block set as the notify handler with a call to `notify`.
|
|
54
71
|
#
|
|
55
|
-
# @return [Proc]
|
|
72
|
+
# @return [Proc, nil] the notify block, or nil if none has been set
|
|
56
73
|
def notify_block
|
|
57
74
|
@notify
|
|
58
75
|
end
|
|
59
76
|
|
|
60
|
-
# Add a class that maintains
|
|
77
|
+
# Add a class that maintains its own connection pool to the connections
|
|
61
78
|
# being monitored for open transactions. You don't need to add `ActiveRecord::Base`
|
|
62
79
|
# or subclasses. Only the base class that establishes a new connection pool
|
|
63
80
|
# with a call to `establish_connection` needs to be added.
|
|
64
81
|
#
|
|
65
|
-
# @param connection_class [Class]
|
|
82
|
+
# @param connection_class [Class] an ActiveRecord model class with its own connection pool
|
|
66
83
|
# @return [void]
|
|
67
84
|
def add_connection_class(connection_class)
|
|
68
85
|
@lock.synchronize { @connection_classes << connection_class }
|
|
69
86
|
end
|
|
70
87
|
|
|
88
|
+
# Return the classes that have been added via `add_connection_class`.
|
|
89
|
+
#
|
|
90
|
+
# @return [Array<Class>]
|
|
91
|
+
def connection_classes
|
|
92
|
+
([ActiveRecord::Base] + @lock.synchronize { @connection_classes.to_a }).uniq
|
|
93
|
+
end
|
|
94
|
+
|
|
71
95
|
# Return true if any connection is currently inside of a transaction.
|
|
72
96
|
#
|
|
73
97
|
# @return [Boolean]
|
|
74
98
|
def in_transaction?
|
|
75
|
-
connection_classes = [ActiveRecord::Base]
|
|
76
|
-
unless @connection_classes.empty?
|
|
77
|
-
connection_classes.concat(@lock.synchronize { @connection_classes.to_a })
|
|
78
|
-
end
|
|
79
99
|
connection_classes.any? do |connection_class|
|
|
80
100
|
connection_pool = connection_class.connection_pool
|
|
81
101
|
connection = connection_class.connection if connection_pool.active_connection?
|
|
@@ -87,15 +107,34 @@ module Sidekiq
|
|
|
87
107
|
end
|
|
88
108
|
end
|
|
89
109
|
|
|
110
|
+
# Disable the transaction guard within the provided block. This is useful in test environments when you want to
|
|
111
|
+
# setup data for your tests without worrying about transaction levels.
|
|
112
|
+
#
|
|
113
|
+
# @yield the block to execute with the transaction guard disabled
|
|
114
|
+
# @return [Object] the return value of the block
|
|
115
|
+
def disable
|
|
116
|
+
save_mode = mode
|
|
117
|
+
begin
|
|
118
|
+
self.mode = :disabled
|
|
119
|
+
yield
|
|
120
|
+
ensure
|
|
121
|
+
self.mode = save_mode
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
90
125
|
# This method call needs to be wrapped around tests that use transactional fixtures.
|
|
91
126
|
# It sets up data structures used to track the number of open transactions.
|
|
127
|
+
# The current transaction level is automatically captured as the baseline so that
|
|
128
|
+
# any transactions opened by test setup (e.g. transactional fixtures) are ignored.
|
|
92
129
|
#
|
|
130
|
+
# @yield the test block to execute
|
|
93
131
|
# @return [Object] the return value of the block
|
|
94
|
-
def testing
|
|
132
|
+
def testing
|
|
95
133
|
var = :sidekiq_rails_transaction_guard
|
|
96
134
|
save_val = Thread.current[var]
|
|
97
135
|
begin
|
|
98
136
|
Thread.current[var] = (save_val ? save_val.dup : {})
|
|
137
|
+
set_allowed_transaction_level(:all)
|
|
99
138
|
yield
|
|
100
139
|
ensure
|
|
101
140
|
Thread.current[var] = save_val
|
|
@@ -104,17 +143,27 @@ module Sidekiq
|
|
|
104
143
|
|
|
105
144
|
# This method needs to be called to set the allowed transaction level for a connection
|
|
106
145
|
# class (see `add_connection_class` for more info). The current transaction level
|
|
107
|
-
# for that class' connection will be set as the zero point. This method can only
|
|
146
|
+
# for that class's connection will be set as the zero point. This method can only
|
|
108
147
|
# be called inside a block wrapped with the `testing` method.
|
|
109
148
|
#
|
|
110
|
-
# @param
|
|
149
|
+
# @param connection_classes [Class, Array<Class>, Symbol] the connection class(es) to set the allowed
|
|
150
|
+
# transaction level for. If `:all` is provided, set the allowed transaction level
|
|
151
|
+
# for all connection classes set via `add_connection_class`.
|
|
152
|
+
# @param base_transaction_level [Integer] if provided, increment the allowed transaction level
|
|
153
|
+
# by this amount. This is used when using transactional fixtures to ignore the transaction
|
|
154
|
+
# opened within the test setup.
|
|
111
155
|
# @return [void]
|
|
112
|
-
def set_allowed_transaction_level(
|
|
156
|
+
def set_allowed_transaction_level(connection_classes, base_transaction_level = 0)
|
|
113
157
|
connection_counts = Thread.current[:sidekiq_rails_transaction_guard]
|
|
114
158
|
unless connection_counts
|
|
115
159
|
raise("set_allowed_transaction_level is only allowed inside a testing block")
|
|
116
160
|
end
|
|
117
|
-
|
|
161
|
+
|
|
162
|
+
connection_classes = self.connection_classes if connection_classes == :all
|
|
163
|
+
Array(connection_classes).each do |connection_class|
|
|
164
|
+
class_count = connection_class.connection.open_transactions + base_transaction_level
|
|
165
|
+
connection_counts[connection_class.name] = class_count
|
|
166
|
+
end
|
|
118
167
|
end
|
|
119
168
|
|
|
120
169
|
private
|
|
@@ -127,6 +176,10 @@ module Sidekiq
|
|
|
127
176
|
end
|
|
128
177
|
end
|
|
129
178
|
|
|
179
|
+
if defined?(Rails::Railtie)
|
|
180
|
+
require_relative "transaction_guard/railtie"
|
|
181
|
+
end
|
|
182
|
+
|
|
130
183
|
# Configure the default transaction guard mode for known testing environments.
|
|
131
184
|
if ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
132
185
|
Sidekiq::TransactionGuard.mode = :stderr
|
|
@@ -7,9 +7,16 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.email = ["bbdurand@gmail.com", "me@winstondurand.com"]
|
|
8
8
|
|
|
9
9
|
spec.summary = "Protect from accidentally invoking Sidekiq jobs when there are open database transactions"
|
|
10
|
+
|
|
10
11
|
spec.homepage = "https://github.com/bdurand/sidekiq-transaction_guard"
|
|
11
12
|
spec.license = "MIT"
|
|
12
13
|
|
|
14
|
+
spec.metadata = {
|
|
15
|
+
"homepage_uri" => spec.homepage,
|
|
16
|
+
"source_code_uri" => spec.homepage,
|
|
17
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
# Specify which files should be added to the gem when it is released.
|
|
14
21
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
15
22
|
ignore_files = %w[
|
|
@@ -28,10 +35,10 @@ Gem::Specification.new do |spec|
|
|
|
28
35
|
|
|
29
36
|
spec.require_paths = ["lib"]
|
|
30
37
|
|
|
31
|
-
spec.required_ruby_version = ">= 2.
|
|
38
|
+
spec.required_ruby_version = ">= 2.7"
|
|
32
39
|
|
|
33
|
-
spec.add_dependency "activerecord", ">=
|
|
34
|
-
spec.add_dependency "sidekiq", ">=
|
|
40
|
+
spec.add_dependency "activerecord", ">= 6.0"
|
|
41
|
+
spec.add_dependency "sidekiq", ">= 6.0"
|
|
35
42
|
|
|
36
43
|
spec.add_development_dependency "bundler"
|
|
37
44
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sidekiq-transaction_guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
8
|
- Winston Durand
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: activerecord
|
|
@@ -17,28 +16,28 @@ dependencies:
|
|
|
17
16
|
requirements:
|
|
18
17
|
- - ">="
|
|
19
18
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: '
|
|
19
|
+
version: '6.0'
|
|
21
20
|
type: :runtime
|
|
22
21
|
prerelease: false
|
|
23
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
23
|
requirements:
|
|
25
24
|
- - ">="
|
|
26
25
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: '
|
|
26
|
+
version: '6.0'
|
|
28
27
|
- !ruby/object:Gem::Dependency
|
|
29
28
|
name: sidekiq
|
|
30
29
|
requirement: !ruby/object:Gem::Requirement
|
|
31
30
|
requirements:
|
|
32
31
|
- - ">="
|
|
33
32
|
- !ruby/object:Gem::Version
|
|
34
|
-
version: '
|
|
33
|
+
version: '6.0'
|
|
35
34
|
type: :runtime
|
|
36
35
|
prerelease: false
|
|
37
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
38
37
|
requirements:
|
|
39
38
|
- - ">="
|
|
40
39
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: '
|
|
40
|
+
version: '6.0'
|
|
42
41
|
- !ruby/object:Gem::Dependency
|
|
43
42
|
name: bundler
|
|
44
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,7 +52,6 @@ dependencies:
|
|
|
53
52
|
- - ">="
|
|
54
53
|
- !ruby/object:Gem::Version
|
|
55
54
|
version: '0'
|
|
56
|
-
description:
|
|
57
55
|
email:
|
|
58
56
|
- bbdurand@gmail.com
|
|
59
57
|
- me@winstondurand.com
|
|
@@ -61,6 +59,7 @@ executables: []
|
|
|
61
59
|
extensions: []
|
|
62
60
|
extra_rdoc_files: []
|
|
63
61
|
files:
|
|
62
|
+
- AGENTS.md
|
|
64
63
|
- CHANGELOG.md
|
|
65
64
|
- MIT_LICENSE.txt
|
|
66
65
|
- README.md
|
|
@@ -69,13 +68,18 @@ files:
|
|
|
69
68
|
- lib/sidekiq/transaction_guard.rb
|
|
70
69
|
- lib/sidekiq/transaction_guard/database_cleaner.rb
|
|
71
70
|
- lib/sidekiq/transaction_guard/middleware.rb
|
|
71
|
+
- lib/sidekiq/transaction_guard/minitest.rb
|
|
72
|
+
- lib/sidekiq/transaction_guard/railtie.rb
|
|
73
|
+
- lib/sidekiq/transaction_guard/rspec.rb
|
|
72
74
|
- lib/sidekiq/transaction_guard/version.rb
|
|
73
75
|
- sidekiq-transaction_guard.gemspec
|
|
74
76
|
homepage: https://github.com/bdurand/sidekiq-transaction_guard
|
|
75
77
|
licenses:
|
|
76
78
|
- MIT
|
|
77
|
-
metadata:
|
|
78
|
-
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://github.com/bdurand/sidekiq-transaction_guard
|
|
81
|
+
source_code_uri: https://github.com/bdurand/sidekiq-transaction_guard
|
|
82
|
+
changelog_uri: https://github.com/bdurand/sidekiq-transaction_guard/blob/main/CHANGELOG.md
|
|
79
83
|
rdoc_options: []
|
|
80
84
|
require_paths:
|
|
81
85
|
- lib
|
|
@@ -83,15 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
83
87
|
requirements:
|
|
84
88
|
- - ">="
|
|
85
89
|
- !ruby/object:Gem::Version
|
|
86
|
-
version: 2.
|
|
90
|
+
version: '2.7'
|
|
87
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
92
|
requirements:
|
|
89
93
|
- - ">="
|
|
90
94
|
- !ruby/object:Gem::Version
|
|
91
95
|
version: '0'
|
|
92
96
|
requirements: []
|
|
93
|
-
rubygems_version: 3.
|
|
94
|
-
signing_key:
|
|
97
|
+
rubygems_version: 3.6.9
|
|
95
98
|
specification_version: 4
|
|
96
99
|
summary: Protect from accidentally invoking Sidekiq jobs when there are open database
|
|
97
100
|
transactions
|