sidekiq-transaction_guard 1.1.1 → 1.1.2

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: de06ea42809df15bffadb4ee4c00387dbf0dfd8cabb14130b98a0400b4502cee
4
- data.tar.gz: fe97b6610a93aacfb5917ea80bdee919b9587d39e92cfe78ef6eddd16e9648ca
3
+ metadata.gz: 4e78c153bc733cc296bd92ee4ef71be1999660724aed0a4421c68a86bea98233
4
+ data.tar.gz: '095f61285d5cb8d7aade4f5c343ea7a6d7e3b548a3ec8e6a128dae5e0d64e740'
5
5
  SHA512:
6
- metadata.gz: 56d18cbdb4a57f1ef5b32f333c30f36ed1c68889b596abaeeb62185264f5cc3cd2ace8d1b01f049183f461649372b7f11d2c1b5c55e1b48fae42a6ba83012f44
7
- data.tar.gz: 5125a0ea08ccd991f0386a11e5a32cd63ab70ebe20d2594363fced7c34a37d01debd16c637a3fa8e77eb6821b2009255bac18a58136317c0b24e599e603a75df
6
+ metadata.gz: 386abe0d3a8b40f22358dd7e30bd808a6e9837e5dc0d34731b5693ee8a02375edee77a425c56e97efc3deec356207aaa7b247618ceda42d25d9da9756d653e7f
7
+ data.tar.gz: c4c88c36fc2b38e6ddbbec3933873f8983ba9a389d9d1554c4913e1ccbf160ba5ab1768a73e3120cb9fca40a92e5dac159518b627452c3ca5eae8fe517ae1453
data/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ 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.2
8
+
9
+ ### Fixed
10
+
11
+ - `Sidekiq::TransactionGuard.init` now registers the client middleware in the Sidekiq server process as well. Previously jobs enqueued from within other jobs bypassed the transaction guard entirely because `Sidekiq.configure_client` is a no-op in server processes.
12
+ - Fixed the Minitest helper so the transaction tracking set up by `Sidekiq::TransactionGuard.testing` remains active for the duration of the test. Previously the testing context was torn down when `setup` returned, so the transaction level snapshot was discarded before the test body ran and tests using transactional fixtures would raise false positive errors on every enqueue. The helper now uses Minitest's `before_setup`/`after_teardown` lifecycle hooks and takes the snapshot after all other setup hooks (including transactional fixtures) have run.
13
+ - The Minitest helper no longer permanently changes the global mode when it is included and no longer defines `setup`/`teardown` methods that could be silently overridden by test classes defining their own.
14
+ - `Sidekiq::TransactionGuard.disable` and the RSpec/Minitest test integrations now use a thread local mode override instead of mutating the global mode. This fixes race conditions where disabling the guard in one thread would disable it for all threads, and where concurrent save/restore of the global mode could leave it in the wrong state.
15
+ - The RSpec integration now registers its per-example hooks at the example group level so the transaction level snapshot is taken after rspec-rails opens the transactional fixture transaction instead of before it.
16
+ - Worker-level `transaction_guard` sidekiq options are now honored when the value is a string as well as a symbol.
17
+ - Connection errors from registered connection classes without an established connection no longer raise from `in_transaction?`.
18
+ - Use `lease_connection` instead of the deprecated `connection` method on ActiveRecord 7.2+.
19
+
20
+ ### Added
21
+
22
+ - `Sidekiq::TransactionGuard.thread_local_mode` and `thread_local_mode=` to override the mode for the current thread only.
23
+ - `Sidekiq::TransactionGuard.default_mode` to read the globally configured mode, ignoring any thread local override.
24
+
7
25
  ## 1.1.1
8
26
 
9
27
  ### Changed
data/README.md CHANGED
@@ -70,9 +70,11 @@ For non-Rails applications, you need to manually add the middleware in your appl
70
70
  ```ruby
71
71
  require 'sidekiq/transaction_guard'
72
72
 
73
- Sidekiq::TransactionGuard::Middleware.init
73
+ Sidekiq::TransactionGuard.init
74
74
  ```
75
75
 
76
+ This registers the client middleware for both client processes and the Sidekiq server process, so jobs enqueued from within other jobs are checked as well.
77
+
76
78
  ### Mode
77
79
 
78
80
  You can set the mode at any time. The mode can be one of `[:warn, :stderr, :error, :disabled]`.
@@ -94,7 +96,13 @@ Sidekiq::TransactionGuard.mode = :disabled
94
96
  You can set the mode when initializing the middleware:
95
97
 
96
98
  ```ruby
97
- Sidekiq::TransactionGuard::Middleware.init(mode: :error)
99
+ Sidekiq::TransactionGuard.init(mode: :error)
100
+ ```
101
+
102
+ You can also override the mode for the current thread only. This does not affect jobs being enqueued concurrently in other threads. Set it to `nil` to remove the override.
103
+
104
+ ```ruby
105
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
98
106
  ```
99
107
 
100
108
  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.
@@ -175,7 +183,9 @@ If you're using RSpec, you can use the built-in RSpec helper to automatically se
175
183
  require 'sidekiq/transaction_guard/rspec'
176
184
  ```
177
185
 
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:
186
+ Require this file after `rspec-rails` (or anything else that opens transactions in its setup hooks) so that the transaction level snapshot is taken after those transactions are opened.
187
+
188
+ Examples run with the `:error` mode by default. You can control the mode on a per-spec basis with a metadata tag. The value can be a mode symbol, `false` to disable the guard, or `:default` to use the globally configured mode. For example:
179
189
 
180
190
  ```ruby
181
191
  RSpec.describe "Some feature", sidekiq_transaction_guard: :disabled do
@@ -203,7 +213,7 @@ If you're using Minitest with `ActiveSupport::TestCase` (Rails default), you can
203
213
  require 'sidekiq/transaction_guard/minitest'
204
214
  ```
205
215
 
206
- This will automatically wrap each test in the appropriate `testing` block and handle transactional fixtures.
216
+ Tests run with the `:error` mode. The guard is disabled while `setup do` callbacks and teardown hooks run, and the transaction level snapshot is taken after all other setup hooks (including transactional fixtures) have completed.
207
217
 
208
218
  If you're using plain Minitest (without `ActiveSupport::TestCase`), you can manually include the helper module:
209
219
 
@@ -223,7 +233,7 @@ Alternatively, you can manually use the `testing` method with minitest-hooks:
223
233
  class MyTests < Minitest::Test
224
234
  # Using minitest-hooks gem
225
235
  def around(&block)
226
- Sidekiq::TransactionGuard.testing(base_transaction_level: 1) do
236
+ Sidekiq::TransactionGuard.testing do
227
237
  block.call
228
238
  end
229
239
  end
@@ -240,6 +250,8 @@ Sidekiq::TransactionGuard.disable do
240
250
  end
241
251
  ```
242
252
 
253
+ The guard is only disabled for the current thread, so jobs enqueued concurrently in other threads are still checked.
254
+
243
255
  ## Installation
244
256
 
245
257
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.1
1
+ 1.1.2
@@ -24,7 +24,9 @@ module Sidekiq
24
24
  private
25
25
 
26
26
  def worker_mode(job)
27
- read_sidekiq_option(job, :transaction_guard) || Sidekiq::TransactionGuard.mode
27
+ mode = read_sidekiq_option(job, :transaction_guard)
28
+ mode = mode.to_sym if mode.is_a?(String)
29
+ mode || Sidekiq::TransactionGuard.mode
28
30
  end
29
31
 
30
32
  def in_transaction?
@@ -47,7 +49,6 @@ module Sidekiq
47
49
  end
48
50
 
49
51
  def read_sidekiq_option(job, option_name)
50
- # options = worker_class.sidekiq_options_hash
51
52
  job[option_name.to_s]
52
53
  end
53
54
 
@@ -2,12 +2,22 @@
2
2
 
3
3
  require "minitest"
4
4
 
5
+ require "sidekiq/transaction_guard"
6
+
5
7
  module Sidekiq
6
8
  module TransactionGuard
7
9
  # Minitest helper module for testing with Sidekiq::TransactionGuard.
8
10
  #
9
- # Include this module in your test class to automatically wrap tests in the
10
- # Sidekiq::TransactionGuard.testing block and handle transactional fixtures.
11
+ # Include this module in your test class to automatically set up transaction
12
+ # tracking for each test. If `ActiveSupport::TestCase` is defined when this
13
+ # file is required, the module is included there automatically.
14
+ #
15
+ # The integration uses Minitest's `before_setup`/`after_teardown` lifecycle
16
+ # hooks. The transaction level snapshot is taken after all other setup hooks
17
+ # have run (including transactional fixtures), so transactions opened by test
18
+ # setup are ignored, and the snapshot stays in effect for the entire test.
19
+ # The guard runs in `:error` mode during the test and is disabled while
20
+ # setup and teardown hooks run.
11
21
  #
12
22
  # @example
13
23
  # class MyTest < Minitest::Test
@@ -18,46 +28,41 @@ module Sidekiq
18
28
  # end
19
29
  # end
20
30
  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
31
+ def before_setup
32
+ @sidekiq_transaction_guard_saved_mode = Sidekiq::TransactionGuard.thread_local_mode
33
+ @sidekiq_transaction_guard_saved_state = Sidekiq::TransactionGuard.begin_testing
34
+ # Keep the guard disabled while other before_setup hooks (like
35
+ # transactional fixtures) and setup callbacks run.
36
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
37
+ super
38
+ # Take the snapshot now that all other setup hooks have run so that
39
+ # transactions opened during setup are treated as the baseline.
40
+ Sidekiq::TransactionGuard.set_allowed_transaction_level(:all)
41
+ Sidekiq::TransactionGuard.thread_local_mode = :error
42
+ end
26
43
 
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
44
+ def before_teardown
45
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
46
+ super
47
+ end
35
48
 
36
- def teardown
37
- super
38
- Sidekiq::TransactionGuard.mode = @sidekiq_transaction_guard_saved_mode
39
- end
49
+ def after_teardown
50
+ super
51
+ ensure
52
+ if defined?(@sidekiq_transaction_guard_saved_state)
53
+ Sidekiq::TransactionGuard.end_testing(@sidekiq_transaction_guard_saved_state)
54
+ end
55
+ if defined?(@sidekiq_transaction_guard_saved_mode)
56
+ Sidekiq::TransactionGuard.thread_local_mode = @sidekiq_transaction_guard_saved_mode
40
57
  end
41
58
  end
42
59
  end
43
60
  end
44
61
  end
45
62
 
46
- # If using ActiveSupport::TestCase, automatically include the helper
63
+ # If using ActiveSupport::TestCase, automatically include the helper.
47
64
  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
65
+ # Included with `send` so that YARD doesn't try to statically resolve the
66
+ # ActiveSupport::TestCase namespace when generating documentation.
67
+ ActiveSupport::TestCase.send(:include, Sidekiq::TransactionGuard::MinitestHelper)
63
68
  end
@@ -1,46 +1,84 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.configure do |config|
4
- global_sidekiq_transaction_guard_mode = nil
3
+ require "sidekiq/transaction_guard"
4
+
5
+ module Sidekiq
6
+ module TransactionGuard
7
+ # RSpec integration. Requiring this file wraps every example in a
8
+ # `Sidekiq::TransactionGuard.testing` block and manages the transaction guard
9
+ # mode with a thread local override so parallel test threads can't interfere
10
+ # with each other.
11
+ #
12
+ # The per example mode can be controlled with the `:sidekiq_transaction_guard`
13
+ # metadata tag. The value can be a mode symbol, `false` to disable the guard,
14
+ # or `:default` to use the globally configured mode. Untagged examples run
15
+ # with the `:error` mode.
16
+ #
17
+ # This file should be required after rspec-rails (or any other library that
18
+ # opens transactions in its setup hooks) so that the transaction level
19
+ # snapshot taken before each example runs after those transactions are opened.
20
+ #
21
+ # @api private
22
+ module RSpecIntegration
23
+ def self.included(example_group)
24
+ # Nested example groups inherit hooks from their parent group, so only
25
+ # register the hooks on the outermost group that includes this module.
26
+ return if example_group.is_a?(Class) && example_group.superclass.include?(RSpecIntegration)
27
+
28
+ # These are registered as group level hooks so they run after hooks added
29
+ # by modules included earlier — in particular after rspec-rails has opened
30
+ # the transactional fixture transaction — so the snapshot taken here
31
+ # treats transactions opened by test setup as the baseline.
32
+ example_group.before do |example|
33
+ Sidekiq::TransactionGuard::RSpecIntegration.apply_example_mode(example)
34
+ Sidekiq::TransactionGuard.set_allowed_transaction_level(:all)
35
+ end
36
+
37
+ example_group.after do
38
+ # Disable the guard before fixture rollback and other teardown hooks run.
39
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
40
+ end
41
+ end
42
+
43
+ def self.apply_example_mode(example)
44
+ metadata = example.metadata
45
+ if metadata.key?(:sidekiq_transaction_guard)
46
+ mode = metadata[:sidekiq_transaction_guard]
47
+ mode = :disabled if mode == false
48
+ mode = nil if mode == :default
49
+ mode = :error unless mode.nil? || mode.is_a?(Symbol)
50
+ Sidekiq::TransactionGuard.thread_local_mode = mode
51
+ elsif Sidekiq::TransactionGuard.thread_local_mode == :disabled
52
+ # Apply the default mode only if an earlier hook hasn't already set one.
53
+ Sidekiq::TransactionGuard.thread_local_mode = :error
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
5
59
 
6
- # Disable by default to avoid raising errors in test setup and teardown.
60
+ RSpec.configure do |config|
61
+ # Disable the guard for code running outside of examples, such as suite level
62
+ # setup and teardown.
7
63
  config.before(:suite) do
8
- global_sidekiq_transaction_guard_mode = Sidekiq::TransactionGuard.mode
9
- Sidekiq::TransactionGuard.mode = :disabled
64
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
10
65
  end
11
66
 
12
67
  config.after(:suite) do
13
- Sidekiq::TransactionGuard.mode = global_sidekiq_transaction_guard_mode
68
+ Sidekiq::TransactionGuard.thread_local_mode = nil
14
69
  end
15
70
 
16
- # Wrap each example in a block that sets up the testing block.
71
+ # Wrap each example in a testing block and keep the guard disabled except
72
+ # while the example itself is running.
17
73
  config.around do |example|
18
- save_val = Sidekiq::TransactionGuard.mode
74
+ saved_mode = Sidekiq::TransactionGuard.thread_local_mode
19
75
  begin
20
- # Set mode to disabled to avoid errors in setup and teardown.
21
- Sidekiq::TransactionGuard.mode = :disabled
22
- Sidekiq::TransactionGuard.testing do
23
- example.run
24
- end
76
+ Sidekiq::TransactionGuard.thread_local_mode = :disabled
77
+ Sidekiq::TransactionGuard.testing { example.run }
25
78
  ensure
26
- Sidekiq::TransactionGuard.mode = save_val
79
+ Sidekiq::TransactionGuard.thread_local_mode = saved_mode
27
80
  end
28
81
  end
29
82
 
30
- # Re-snapshot the allowed transaction level after all setup (including
31
- # transactional fixtures) has run so that setup transactions are ignored.
32
- config.before(:each) do |example|
33
- mode = example.metadata[:sidekiq_transaction_guard]
34
- mode = :disabled if mode == false
35
- mode = global_sidekiq_transaction_guard_mode if mode == :default
36
- mode = :error unless mode.is_a?(Symbol)
37
- Sidekiq::TransactionGuard.mode = mode if Sidekiq::TransactionGuard.mode == :disabled
38
-
39
- Sidekiq::TransactionGuard.set_allowed_transaction_level(:all)
40
- end
41
-
42
- # Restore the disabled mode after example so test teardown doesn't raise errors about transaction levels.
43
- config.after(:each) do
44
- Sidekiq::TransactionGuard.mode = :disabled
45
- end
83
+ config.include Sidekiq::TransactionGuard::RSpecIntegration
46
84
  end
@@ -19,18 +19,22 @@ module Sidekiq
19
19
  class << self
20
20
  VALID_MODES = [:warn, :stderr, :error, :disabled].freeze
21
21
 
22
- # Helper method to add the client middleware to Sidekiq.
22
+ # Initialize Sidekiq::TransactionGuard by adding its client middleware to
23
+ # Sidekiq. The middleware is added to both the client and server
24
+ # configurations since jobs can also be enqueued from within other jobs
25
+ # running in the Sidekiq server process.
23
26
  #
27
+ # @param mode [Symbol, nil] optionally set the global mode (see `mode=`)
24
28
  # @return [void]
25
29
  def init(mode: nil)
26
30
  self.mode = mode if mode
27
31
 
28
32
  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
33
+ add_client_middleware(config)
34
+ end
35
+
36
+ Sidekiq.configure_server do |config|
37
+ add_client_middleware(config)
34
38
  end
35
39
  end
36
40
 
@@ -46,16 +50,48 @@ module Sidekiq
46
50
  # @return [Symbol] the mode that was set
47
51
  def mode=(symbol)
48
52
  if VALID_MODES.include?(symbol)
49
- @mode = symbol
53
+ @lock.synchronize { @mode = symbol }
50
54
  else
51
55
  raise ArgumentError.new("mode must be one of #{VALID_MODES.inspect}")
52
56
  end
53
57
  end
54
58
 
55
- # Return the current mode.
59
+ # Return the mode in effect for the current thread. This is the thread local
60
+ # mode if one has been set, otherwise the global mode.
61
+ #
62
+ # @return [Symbol]
63
+ def mode
64
+ thread_local_mode || default_mode
65
+ end
66
+
67
+ # Return the globally configured mode, ignoring any thread local override.
56
68
  #
57
69
  # @return [Symbol]
58
- attr_reader :mode
70
+ def default_mode
71
+ @lock.synchronize { @mode }
72
+ end
73
+
74
+ # Set a mode override for the current thread only. This is used by `disable`
75
+ # and the test integrations so that changing the mode in one thread cannot
76
+ # affect jobs being enqueued concurrently in other threads. Set to `nil` to
77
+ # remove the override and fall back to the global mode.
78
+ #
79
+ # @param symbol [Symbol, nil] one of `:warn`, `:stderr`, `:error`, `:disabled`, or nil
80
+ # @return [Symbol, nil] the mode that was set
81
+ def thread_local_mode=(symbol)
82
+ if symbol.nil? || VALID_MODES.include?(symbol)
83
+ Thread.current[:sidekiq_transaction_guard_mode] = symbol
84
+ else
85
+ raise ArgumentError.new("mode must be one of #{VALID_MODES.inspect}")
86
+ end
87
+ end
88
+
89
+ # Return the mode override for the current thread, or nil if none is set.
90
+ #
91
+ # @return [Symbol, nil]
92
+ def thread_local_mode
93
+ Thread.current[:sidekiq_transaction_guard_mode]
94
+ end
59
95
 
60
96
  # Define the global notify block. This block will be called with a Sidekiq
61
97
  # job hash for all jobs enqueued inside transactions if the mode is `:warn`
@@ -64,14 +100,14 @@ module Sidekiq
64
100
  # @yield [Hash] the Sidekiq job hash
65
101
  # @return [void]
66
102
  def notify(&block)
67
- @notify = block
103
+ @lock.synchronize { @notify = block }
68
104
  end
69
105
 
70
106
  # Return the block set as the notify handler with a call to `notify`.
71
107
  #
72
108
  # @return [Proc, nil] the notify block, or nil if none has been set
73
109
  def notify_block
74
- @notify
110
+ @lock.synchronize { @notify }
75
111
  end
76
112
 
77
113
  # Add a class that maintains its own connection pool to the connections
@@ -97,8 +133,7 @@ module Sidekiq
97
133
  # @return [Boolean]
98
134
  def in_transaction?
99
135
  connection_classes.any? do |connection_class|
100
- connection_pool = connection_class.connection_pool
101
- connection = connection_class.connection if connection_pool.active_connection?
136
+ connection = active_connection(connection_class)
102
137
  if connection
103
138
  connection.open_transactions > allowed_transaction_level(connection_class)
104
139
  else
@@ -110,15 +145,18 @@ module Sidekiq
110
145
  # Disable the transaction guard within the provided block. This is useful in test environments when you want to
111
146
  # setup data for your tests without worrying about transaction levels.
112
147
  #
148
+ # The guard is only disabled for the current thread so that jobs enqueued
149
+ # concurrently in other threads are still checked.
150
+ #
113
151
  # @yield the block to execute with the transaction guard disabled
114
152
  # @return [Object] the return value of the block
115
153
  def disable
116
- save_mode = mode
154
+ save_mode = thread_local_mode
117
155
  begin
118
- self.mode = :disabled
156
+ self.thread_local_mode = :disabled
119
157
  yield
120
158
  ensure
121
- self.mode = save_mode
159
+ self.thread_local_mode = save_mode
122
160
  end
123
161
  end
124
162
 
@@ -130,17 +168,44 @@ module Sidekiq
130
168
  # @yield the test block to execute
131
169
  # @return [Object] the return value of the block
132
170
  def testing
133
- var = :sidekiq_rails_transaction_guard
134
- save_val = Thread.current[var]
171
+ saved_state = begin_testing
135
172
  begin
136
- Thread.current[var] = (save_val ? save_val.dup : {})
137
173
  set_allowed_transaction_level(:all)
138
174
  yield
139
175
  ensure
140
- Thread.current[var] = save_val
176
+ end_testing(saved_state)
141
177
  end
142
178
  end
143
179
 
180
+ # Start a testing context on the current thread without a block. This is used
181
+ # by test framework integrations (like the Minitest helper) that cannot wrap
182
+ # the entire test in a single block. Use `testing` instead whenever a block
183
+ # can be used. The returned state must be passed to `end_testing` when the
184
+ # test finishes.
185
+ #
186
+ # This method only allocates the transaction tracking state; it does not
187
+ # capture a transaction level baseline. The caller is responsible for calling
188
+ # `set_allowed_transaction_level` once any setup that opens transactions
189
+ # (e.g. transactional fixtures) has run.
190
+ #
191
+ # @api private
192
+ # @return [Object] opaque saved state to pass to `end_testing`
193
+ def begin_testing
194
+ var = :sidekiq_rails_transaction_guard
195
+ saved_state = Thread.current[var]
196
+ Thread.current[var] = (saved_state ? saved_state.dup : {})
197
+ saved_state
198
+ end
199
+
200
+ # End a testing context started with `begin_testing`.
201
+ #
202
+ # @api private
203
+ # @param saved_state [Object] the state returned by `begin_testing`
204
+ # @return [void]
205
+ def end_testing(saved_state)
206
+ Thread.current[:sidekiq_rails_transaction_guard] = saved_state
207
+ end
208
+
144
209
  # This method needs to be called to set the allowed transaction level for a connection
145
210
  # class (see `add_connection_class` for more info). The current transaction level
146
211
  # for that class's connection will be set as the zero point. This method can only
@@ -161,7 +226,11 @@ module Sidekiq
161
226
 
162
227
  connection_classes = self.connection_classes if connection_classes == :all
163
228
  Array(connection_classes).each do |connection_class|
164
- class_count = connection_class.connection.open_transactions + base_transaction_level
229
+ class_count = begin
230
+ lease_connection(connection_class).open_transactions + base_transaction_level
231
+ rescue ActiveRecord::ConnectionNotEstablished
232
+ base_transaction_level
233
+ end
165
234
  connection_counts[connection_class.name] = class_count
166
235
  end
167
236
  end
@@ -172,6 +241,35 @@ module Sidekiq
172
241
  connection_counts = Thread.current[:sidekiq_rails_transaction_guard]
173
242
  (connection_counts && connection_counts[connection_class.name]) || 0
174
243
  end
244
+
245
+ def add_client_middleware(config)
246
+ config.client_middleware do |chain|
247
+ unless chain.exists?(Sidekiq::TransactionGuard::Middleware)
248
+ chain.add Sidekiq::TransactionGuard::Middleware
249
+ end
250
+ end
251
+ end
252
+
253
+ # Return the connection for the class only if the current thread already has
254
+ # one checked out. Checking out a new connection here would be pointless (a
255
+ # freshly checked out connection can't be inside an application transaction)
256
+ # and would tie up connections from pools the code isn't even using.
257
+ def active_connection(connection_class)
258
+ connection = nil
259
+ pool = connection_class.connection_pool
260
+ connection = lease_connection(connection_class) if pool.active_connection?
261
+ connection
262
+ rescue ActiveRecord::ConnectionNotEstablished
263
+ nil
264
+ end
265
+
266
+ def lease_connection(connection_class)
267
+ if connection_class.respond_to?(:lease_connection)
268
+ connection_class.lease_connection
269
+ else
270
+ connection_class.connection
271
+ end
272
+ end
175
273
  end
176
274
  end
177
275
  end
@@ -181,6 +279,7 @@ if defined?(Rails::Railtie)
181
279
  end
182
280
 
183
281
  # Configure the default transaction guard mode for known testing environments.
282
+ # In a Rails application the Railtie initializer will override this default.
184
283
  if ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
185
284
  Sidekiq::TransactionGuard.mode = :stderr
186
285
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-transaction_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
96
  requirements: []
97
- rubygems_version: 3.6.9
97
+ rubygems_version: 4.0.3
98
98
  specification_version: 4
99
99
  summary: Protect from accidentally invoking Sidekiq jobs when there are open database
100
100
  transactions