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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f1c8f731c16ff8a3da91b3073be50dd1544a1b17a82521b60637baaa4643421
4
- data.tar.gz: 711cd9daff96b69e24d54a8e21803bcf85bf64e60ab813ba168d943e7092f236
3
+ metadata.gz: 118711493ed2b0a682e2245f00fe8f204aa3e3212d51187c8482d2c526898087
4
+ data.tar.gz: 23efcba40a7c8f96e95045e489387ab6199ef0fc951d4b9f10b4894bf89c166c
5
5
  SHA512:
6
- metadata.gz: ff23694280b688a5d0cdf7274408aa3640385dd4269814fb54421848c339f101b7dc865c7013dde0ff83ce6a9031810082155f3bfed727eaf9643a5b7cffdaa0
7
- data.tar.gz: 36ad8e565e052c16e309fffa3f6e2b6fa715c8c694546b74b63d4aeee7f04224b68ebd193421a1f1f550fc8d02c8332507f0acf9583e1f074247d24eea60b3c4
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
  [![Continuous Integration](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/continuous_integration.yml)
4
- [![Regression Test](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/sidekiq-transaction_guard/actions/workflows/regression_test.yml)
5
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
5
  [![Gem Version](https://badge.fury.io/rb/sidekiq-transaction_guard.svg)](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 it's 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.
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. The will just give you a new problem to solve.
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. You can do this by simply adding this to your application's initialization code:
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.configure_client do |config|
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
- By default, the behavior is to log that a worker is being scheduled inside of a transaction to the `Sidekiq.logger`. If you are running a test suite, you may want to expose the problematic calls by either raising errors or logging the calls to standard error. The mode can be one of `[:warn, :stderr, :error, :disabled]`.
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 also set the mode on individual worker classes with `sidekiq_options transaction_guard: mode`.
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
- You could
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 what ever you need to. The job argument will be a Sidekiq job hash.
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 estabilishes a new connection pool.
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
- ## Transaction Fixtures In Tests
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 transaction fixtures in your tests, there will always be a database transaction open. 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:
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
- If you're using something else for your transactional fixtures or have some other weird setup, look in the `lib/sidekiq_transaction_guard/database_cleaner.rb` file for an example of what you need to do.
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.3
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
- if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("7.0")
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 != :disabled
70
- message = "#{worker_class.name} was called from inside a database transaction"
71
- if mode == :error
72
- raise Sidekiq::TransactionGuard::InsideTransactionError.new(message)
73
- else
74
- logger = Sidekiq.logger unless mode == :stderr
75
- if logger
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 - Throw a `Sidekiq::TransactionGuard::InsideTransactionError`
42
+ # * :error - Raise a `Sidekiq::TransactionGuard::InsideTransactionError`
27
43
  # * :disabled - Allow workers inside of transactions
28
44
  #
29
- # @param mode [Symbol]
30
- # @return [void]
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 it's own connection pool to the connections
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(&block)
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 connection_class [Class]
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(connection_class)
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
- connection_counts[connection_class.name] = connection_class.connection.open_transactions if connection_counts
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.2.2"
38
+ spec.required_ruby_version = ">= 2.7"
32
39
 
33
- spec.add_dependency "activerecord", ">= 4.0"
34
- spec.add_dependency "sidekiq", ">= 3.0"
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.3
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: 2023-12-11 00:00:00.000000000 Z
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: '4.0'
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: '4.0'
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: '3.0'
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: '3.0'
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
- post_install_message:
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.2.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.4.20
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