isolator 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f7e62d7f280c06f8ad704770d5ba5f14198006bccec0ce69e0e1aa99a8b32c7
4
- data.tar.gz: 4c4aabcfad57d3ba2da54b5e75b7ee057bf25a10d382094d05a04fec2f371c85
3
+ metadata.gz: dc5300f95e1333baa45e8e10e1e13b1ebc03175b648791bfec5a87e160643e9e
4
+ data.tar.gz: e184eb7fc257b0ab5ae95878ea955769d9925b8b428dc6c469bcde5af89c69f5
5
5
  SHA512:
6
- metadata.gz: 565c283a6ddebe9e7d39472094f44f346a1ea1b49afc5ad9fe7aee1ecb98e5910fd5cdc4c0804c581132363338e39465faadab3f51eccbddaea8f731225fd364
7
- data.tar.gz: ab01a1494c0fac2c1920bced02ae86c6b6b26a6e420f3ef6e43fdd1bd300e14e259c3353b9af08b14787b5ebe499d816222998e837d0af4e16006d9c77a9f5d1
6
+ metadata.gz: 2668126bfa3de56db202b28dc4eeb5267a2acccf2843d2843d7928cc7fbd96d3a7f42b90b1e652b97857b9f2e3f75e009bacacc4ab9532d4c7ebfbbeea35fb56
7
+ data.tar.gz: 252e668eb55d8f4b8ddda8567833be514b595817d2064d35190bdd74719409568a7779e0bcf8564da4d32e45bc3d3fe6a5ae5afe3ec1d6018dfb0db107c1418d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.0.0 (2023-11-30)
6
+
7
+ - Add ability to track concurrent transactions to with a thread (e.g., to multiple databases). ([@palkan][])
8
+
9
+ This feature is disabled by default, opt-in via: `Isolator.config.disallow_per_thread_concurrent_transactions = true`.
10
+
11
+ - Add `Isolator.on_transaction_begin` and `Isolator.on_transaction_end` callbacks. ([@palkan][])
12
+
13
+ - Drop Ruby 2.6 and Rails 5 support. ([@palkan][])
14
+
15
+ ## 0.11.0 (2023-09-27)
16
+
17
+ - Use Rails new `transaction.active_record` event if available to better handle edge cases. ([@palkan][])
18
+
19
+ - Fix logging non-UTF8 strings. ([@palkan][])
20
+
21
+ Fixes [#66](https://github.com/palkan/isolator/issues/66)
22
+
5
23
  ## 0.10.0 (2023-08-15)
6
24
 
7
25
  - Support multiple databases with DatabaseCleaner. ([@palkan][])
data/README.md CHANGED
@@ -101,6 +101,9 @@ Isolator.configure do |config|
101
101
  # Define a custom ignorer class (must implement .prepare)
102
102
  # uses a row number based list from the .isolator_todo.yml file
103
103
  config.ignorer = Isolator::Ignorer
104
+
105
+ # Turn on/off raising exceptions for simultaneous transactions to different databases
106
+ config.disallow_cross_database_transactions = false
104
107
  end
105
108
  ```
106
109
 
@@ -108,6 +111,34 @@ Isolator relies on [uniform_notifier][] to send custom notifications.
108
111
 
109
112
  **NOTE:** `uniform_notifier` should be installed separately (i.e., added to Gemfile).
110
113
 
114
+ ### Callbacks
115
+
116
+ Isolator different callbacks so you can inject your own logic or build custom extensions.
117
+
118
+ ```ruby
119
+ # This callback is called when Isolator enters the "danger zone"—a within-transaction context
120
+ Isolator.before_isolate do
121
+ puts "Entering a database transaction. Be careful!"
122
+ end
123
+
124
+ # This callback is called when Isolator leaves the "danger zone"
125
+ Isolator.after_isolate do
126
+ puts "Leaving a database transaction. Everything is fine. Feel free to call slow HTTP APIs"
127
+ end
128
+
129
+ # This callback is called every time a new transaction is open (root or nested)
130
+ Isolator.on_transaction_open do |event|
131
+ puts "New transaction from #{event[:connection_id]}. " \
132
+ "Current depth: #{event[:depth]}"
133
+ end
134
+
135
+ # This callback is called every time a transaction is completed
136
+ Isolator.on_transaction_close do |event|
137
+ puts "Transaction completed from #{event[:connection_id]}. " \
138
+ "Current depth: #{event[:depth]}"
139
+ end
140
+ ```
141
+
111
142
  ### Transactional tests support
112
143
 
113
144
  - Rails' baked-in [use_transactional_tests](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Transactional+Tests)
@@ -115,7 +146,7 @@ Isolator relies on [uniform_notifier][] to send custom notifications.
115
146
 
116
147
  ### Supported ORMs
117
148
 
118
- - `ActiveRecord` >= 5.1 (4.2 likely till works, but we do not test against it anymore)
149
+ - `ActiveRecord` >= 6.0 (see older versions of Isolator for previous versions)
119
150
  - `ROM::SQL` (only if Active Support instrumentation extension is loaded)
120
151
 
121
152
  ### Adapters
@@ -159,7 +190,6 @@ Isolator.adapters.sidekiq.ignore_if { Thread.current[:sidekiq_postpone] }
159
190
 
160
191
  You can add as many _ignores_ as you want, the offense is registered iff all of them return false.
161
192
 
162
-
163
193
  ### Using with sidekiq/testing
164
194
 
165
195
  If you require sidekiq/testing in your tests after isolator is required then it will blow away isolator's hooks, so you need to require isolator after requiring sidekiq/testing.
@@ -31,12 +31,18 @@ module Isolator
31
31
  def build_mod(method_name, adapter)
32
32
  return nil unless method_name
33
33
 
34
+ adapter_name = "__isolator_adapter_#{adapter.object_id}"
35
+
34
36
  Module.new do
35
- define_method method_name do |*args, **kwargs, &block|
36
- # check if we are even notifying before calling `caller`, which is well known to be slow
37
- adapter.notify(caller, self, *args, **kwargs) if adapter.notify?(*args, **kwargs)
38
- super(*args, **kwargs, &block)
39
- end
37
+ define_method(adapter_name) { adapter }
38
+
39
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
40
+ def #{method_name}(...)
41
+ # check if we are even notifying before calling `caller`, which is well known to be slow
42
+ #{adapter_name}.notify(caller, self, ...) if #{adapter_name}.notify?(...)
43
+ super
44
+ end
45
+ RUBY
40
46
  end
41
47
  end
42
48
  end
@@ -27,8 +27,8 @@ module Isolator
27
27
  Isolator.notify(exception: build_exception(obj, args, kwargs), backtrace: backtrace)
28
28
  end
29
29
 
30
- def notify?(*args, **kwargs)
31
- enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(*args, **kwargs)
30
+ def notify?(...)
31
+ enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(...)
32
32
  end
33
33
 
34
34
  def ignore_if(&block)
@@ -6,12 +6,12 @@ require "sniffer"
6
6
  Sniffer::Config.defaults["logger"] = nil
7
7
 
8
8
  Isolator.isolate :http, target: Sniffer.singleton_class,
9
- method_name: :store,
10
- exception_class: Isolator::HTTPError,
11
- details_message: ->(_obj, args) {
12
- req = args.first.request
13
- "#{req.method} #{req.host}:#{req.port}#{req.query}"
14
- }
9
+ method_name: :store,
10
+ exception_class: Isolator::HTTPError,
11
+ details_message: ->(_obj, args) {
12
+ req = args.first.request
13
+ "#{req.method} #{req.host}:#{req.port}#{req.query}"
14
+ }
15
15
 
16
16
  Isolator.before_isolate do
17
17
  next if Isolator.adapters.http.disabled?
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :mailer, target: Mail::Message,
4
- method_name: :deliver,
5
- exception_class: Isolator::MailerError,
6
- details_message: ->(obj) {
7
- "From: #{obj.from}\n" \
8
- "To: #{obj.to}\n" \
9
- "Subject: #{obj.subject}"
10
- }
4
+ method_name: :deliver,
5
+ exception_class: Isolator::MailerError,
6
+ details_message: ->(obj) {
7
+ "From: #{obj.from}\n" \
8
+ "To: #{obj.to}\n" \
9
+ "Subject: #{obj.subject}"
10
+ }
@@ -11,6 +11,14 @@ module Isolator
11
11
  after_isolate_callbacks << block
12
12
  end
13
13
 
14
+ def on_transaction_begin(&block)
15
+ transaction_begin_callbacks << block
16
+ end
17
+
18
+ def on_transaction_end(&block)
19
+ transaction_end_callbacks << block
20
+ end
21
+
14
22
  def start!
15
23
  return if Isolator.disabled?
16
24
  before_isolate_callbacks.each(&:call)
@@ -21,6 +29,14 @@ module Isolator
21
29
  after_isolate_callbacks.each(&:call)
22
30
  end
23
31
 
32
+ def notify!(event, payload)
33
+ if event == :begin
34
+ transaction_begin_callbacks.each { |cb| cb.call(payload) }
35
+ elsif event == :end
36
+ transaction_end_callbacks.each { |cb| cb.call(payload) }
37
+ end
38
+ end
39
+
24
40
  def before_isolate_callbacks
25
41
  @before_isolate_callbacks ||= []
26
42
  end
@@ -28,5 +44,13 @@ module Isolator
28
44
  def after_isolate_callbacks
29
45
  @after_isolate_callbacks ||= []
30
46
  end
47
+
48
+ def transaction_begin_callbacks
49
+ @transaction_begin_callbacks ||= []
50
+ end
51
+
52
+ def transaction_end_callbacks
53
+ @transaction_end_callbacks ||= []
54
+ end
31
55
  end
32
56
  end
@@ -29,14 +29,22 @@ module Isolator
29
29
  def log_exception
30
30
  return unless Isolator.config.logger
31
31
 
32
- msg = "[ISOLATOR EXCEPTION]\n" \
33
- "#{exception.message}"
32
+ separator = " "
34
33
 
35
- filtered_backtrace.each do |offense_line|
36
- msg += "\n #{offense_line}"
37
- end
34
+ begin
35
+ msg = "[ISOLATOR EXCEPTION]\n" \
36
+ "#{exception.message}"
38
37
 
39
- Isolator.config.logger.warn(msg)
38
+ filtered_backtrace.each do |offense_line|
39
+ msg += "\n #{separator}#{offense_line}"
40
+ end
41
+
42
+ Isolator.config.logger.warn(msg)
43
+ rescue Encoding::CompatibilityError
44
+ should_retry = separator != " - "
45
+ separator = " - "
46
+ retry if should_retry
47
+ end
40
48
  end
41
49
 
42
50
  def send_notifications
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./active_support_subscriber"
3
+ require_relative "active_support_subscriber"
4
4
 
5
- Isolator::ActiveSupportSubscriber.subscribe!("sql.active_record")
5
+ # We rely on this feature introduced in 7.1.0.beta1: https://github.com/rails/rails/pull/49192
6
+ if ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 1
7
+ require_relative "active_support_transaction_subscriber"
8
+ Isolator::ActiveSupportTransactionSubscriber.subscribe!
9
+ else
10
+ Isolator::ActiveSupportSubscriber.subscribe!("sql.active_record")
11
+ end
@@ -7,14 +7,46 @@ module Isolator
7
7
  START_PATTERN = %r{(\ABEGIN|\ASAVEPOINT)}xi
8
8
  FINISH_PATTERN = %r{(\ACOMMIT|\AROLLBACK|\ARELEASE|\AEND TRANSACTION)}xi
9
9
 
10
- def self.subscribe!(event)
11
- ::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
12
- connection_id = query[:connection_id] || query[:connection]&.object_id || 0
13
- # Prevents "ArgumentError: invalid byte sequence in UTF-8" by replacing invalid byte sequence with "?"
14
- sanitized_query = query[:sql].encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "?")
15
- Isolator.incr_transactions!(connection_id) if START_PATTERN.match?(sanitized_query)
16
- Isolator.decr_transactions!(connection_id) if FINISH_PATTERN.match?(sanitized_query)
10
+ class Subscriber
11
+ def start(event, id, payload)
12
+ return unless start_event?(payload[:sql])
13
+
14
+ connection_id = extract_connection_id(payload)
15
+
16
+ Isolator.incr_transactions!(connection_id)
17
+ end
18
+
19
+ def finish(event, id, payload)
20
+ return unless finish_event?(payload[:sql])
21
+
22
+ connection_id = extract_connection_id(payload)
23
+
24
+ Isolator.decr_transactions!(connection_id)
25
+ end
26
+
27
+ private
28
+
29
+ def start_event?(sql)
30
+ START_PATTERN.match?(sanitize_query(sql))
17
31
  end
32
+
33
+ def finish_event?(sql)
34
+ FINISH_PATTERN.match?(sanitize_query(sql))
35
+ end
36
+
37
+ # Prevents "ArgumentError: invalid byte sequence in UTF-8" by replacing invalid byte sequence with "?"
38
+ def sanitize_query(sql)
39
+ sql.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "?")
40
+ end
41
+
42
+ def extract_connection_id(payload)
43
+ payload[:connection_id] || payload[:connection]&.object_id || 0
44
+ end
45
+ end
46
+
47
+ def self.subscribe!(event)
48
+ subscriber = Subscriber.new
49
+ ::ActiveSupport::Notifications.subscribe(event, subscriber)
18
50
  end
19
51
  end
20
52
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ # ActiveSupport notifications subscriber for "transaction.active_record" event (new in Rails 7.1)
5
+ module ActiveSupportTransactionSubscriber
6
+ class Subscriber < ActiveSupportSubscriber::Subscriber
7
+ attr_reader :stacks
8
+
9
+ def initialize
10
+ @stacks = Hash.new { |h, k| h[k] = [] }
11
+ end
12
+
13
+ def start(event, id, payload)
14
+ if event.start_with?("transaction.")
15
+ connection_id = extract_transaction_connection_id(payload)
16
+
17
+ # transaction.active_record can be issued without a query (when we restart the transaction),
18
+ # so we should add a new one on the stack.
19
+ # Example: https://github.com/rails/rails/blob/ce49fa9b31cd4a21d43db39d0cea364bce28b51d/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L337
20
+ if stacks[connection_id].last == :raw
21
+ # Update the type of the last transaction event
22
+ stacks[connection_id].pop
23
+ stacks[connection_id] << :transaction
24
+ else
25
+ stacks[connection_id] << :transaction
26
+ Isolator.incr_transactions!(connection_id)
27
+ end
28
+ end
29
+ end
30
+
31
+ def finish(event, id, payload)
32
+ if event.start_with?("sql.")
33
+ if start_event?(payload[:sql])
34
+ connection_id = extract_connection_id(payload)
35
+
36
+ stacks[connection_id] << :raw
37
+
38
+ Isolator.incr_transactions!(connection_id)
39
+ end
40
+
41
+ if finish_event?(payload[:sql])
42
+ connection_id = extract_connection_id(payload)
43
+
44
+ # Decrement only if the transaction was started in the raw mode,
45
+ # otherwise we should wait for the "transaction" event
46
+ if stacks[connection_id].last == :raw
47
+ stacks[connection_id].pop
48
+ Isolator.decr_transactions!(connection_id)
49
+ end
50
+ end
51
+ end
52
+
53
+ if event.start_with?("transaction.")
54
+ connection_id = extract_transaction_connection_id(payload)
55
+ stacks[connection_id].pop
56
+
57
+ Isolator.decr_transactions!(connection_id)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def extract_transaction_connection_id(payload)
64
+ payload[:connection]&.object_id || 0
65
+ end
66
+ end
67
+
68
+ def self.subscribe!(event = "transaction.active_record", sql_event = "sql.active_record")
69
+ subscriber = Subscriber.new
70
+ ::ActiveSupport::Notifications.subscribe(event, subscriber)
71
+ ::ActiveSupport::Notifications.subscribe(sql_event, subscriber)
72
+ end
73
+ end
74
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./active_support_subscriber"
3
+ require_relative "active_support_subscriber"
4
4
 
5
5
  Isolator::ActiveSupportSubscriber.subscribe!("sql.rom")
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isolator
4
+ class Configuration
5
+ attr_accessor :disallow_per_thread_concurrent_transactions
6
+
7
+ alias_method :disallow_per_thread_concurrent_transactions?, :disallow_per_thread_concurrent_transactions
8
+ end
9
+
10
+ class ConcurrentTransactionError < UnsafeOperationError
11
+ MESSAGE = "You are trying to open a transaction while there is an open transation to another database." \
12
+ end
13
+
14
+ Isolator.before_isolate do
15
+ next unless Isolator.config.disallow_per_thread_concurrent_transactions?
16
+
17
+ isolated_connections = Isolator.all_transactions.count do |conn_id, depth|
18
+ depth >= Isolator.connection_threshold(conn_id)
19
+ end
20
+
21
+ next unless isolated_connections > 1
22
+
23
+ Isolator.notify(exception: ConcurrentTransactionError.new, backtrace: caller)
24
+ end
25
+ end
@@ -2,6 +2,22 @@
2
2
 
3
3
  module Isolator
4
4
  class Railtie < ::Rails::Railtie # :nodoc:
5
+ module TestFixtures
6
+ def setup_fixtures(*)
7
+ super
8
+ return unless run_in_transaction?
9
+
10
+ Isolator.incr_thresholds!
11
+ end
12
+
13
+ def teardown_fixtures(*)
14
+ if run_in_transaction?
15
+ Isolator.decr_thresholds!
16
+ end
17
+ super
18
+ end
19
+ end
20
+
5
21
  initializer "isolator.backtrace_cleaner" do
6
22
  ActiveSupport.on_load(:active_record) do
7
23
  Isolator.backtrace_cleaner = lambda do |locations|
@@ -31,24 +47,13 @@ module Isolator
31
47
 
32
48
  next unless Rails.env.test?
33
49
 
34
- if defined?(::ActiveRecord::TestFixtures)
35
- ::ActiveRecord::TestFixtures.prepend(
36
- Module.new do
37
- def setup_fixtures(*)
38
- super
39
- return unless run_in_transaction?
40
-
41
- Isolator.incr_thresholds!
42
- end
43
-
44
- def teardown_fixtures(*)
45
- if run_in_transaction?
46
- Isolator.decr_thresholds!
47
- end
48
- super
49
- end
50
- end
51
- )
50
+ ActiveSupport.on_load(:active_record_fixtures) do
51
+ ::ActiveRecord::TestFixtures.prepend(TestFixtures)
52
+ end
53
+
54
+ # Rails 6 doesn't support this load hook, so we can emulate it
55
+ if ActiveRecord::VERSION::MAJOR < 7 && defined?(::ActiveRecord::TestFixtures)
56
+ ::ActiveRecord::TestFixtures.prepend(TestFixtures)
52
57
  end
53
58
  end
54
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.10.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/isolator.rb CHANGED
@@ -94,6 +94,14 @@ module Isolator
94
94
  state[:transactions]&.[](connection_id) || 0
95
95
  end
96
96
 
97
+ def all_transactions
98
+ state[:transactions] || {}
99
+ end
100
+
101
+ def connection_threshold(connection_id)
102
+ state[:thresholds]&.[](connection_id) || default_threshold
103
+ end
104
+
97
105
  def set_connection_threshold(val, connection_id = default_connection_id.call)
98
106
  state[:thresholds] ||= Hash.new { |h, k| h[k] = Isolator.default_threshold }
99
107
  state[:thresholds][connection_id] = val
@@ -131,7 +139,14 @@ module Isolator
131
139
 
132
140
  debug!("Transaction opened for connection #{connection_id} (total: #{state[:transactions][connection_id]}, threshold: #{state[:thresholds]&.fetch(connection_id, default_threshold)})")
133
141
 
134
- start! if current_transactions(connection_id) == connection_threshold(connection_id)
142
+ current_depth = current_transactions(connection_id)
143
+ threshold = connection_threshold(connection_id)
144
+
145
+ start! if current_depth == threshold
146
+ if current_depth >= threshold
147
+ event = {connection_id: connection_id, depth: current_depth - threshold + 1}.freeze
148
+ notify!(:begin, event)
149
+ end
135
150
  end
136
151
 
137
152
  def decr_transactions!(connection_id = default_connection_id.call)
@@ -144,7 +159,15 @@ module Isolator
144
159
 
145
160
  state[:transactions][connection_id] -= 1
146
161
 
147
- finish! if current_transactions(connection_id) == (connection_threshold(connection_id) - 1)
162
+ current_depth = current_transactions(connection_id)
163
+ threshold = connection_threshold(connection_id)
164
+
165
+ if current_depth >= threshold - 1
166
+ event = {connection_id: connection_id, depth: current_depth - threshold + 1}.freeze
167
+ notify!(:end, event)
168
+ end
169
+
170
+ finish! if current_depth == (threshold - 1)
148
171
 
149
172
  state[:transactions].delete(connection_id) if state[:transactions][connection_id].zero?
150
173
 
@@ -192,21 +215,25 @@ module Isolator
192
215
 
193
216
  attr_accessor :state
194
217
 
195
- def connection_threshold(connection_id)
196
- state[:thresholds]&.[](connection_id) || default_threshold
197
- end
198
-
199
218
  def debug!(msg)
200
219
  return unless debug_enabled
201
- msg = "[ISOLATOR DEBUG] #{msg}"
202
220
 
203
- if backtrace_cleaner && backtrace_length.positive?
204
- source = extract_source_location(caller)
221
+ separator = " ↳ "
205
222
 
206
- msg = "#{msg}\n ↳ #{source.join("\n")}" unless source.empty?
223
+ begin
224
+ msg = "[ISOLATOR DEBUG] #{msg}"
225
+ if backtrace_cleaner && backtrace_length.positive?
226
+ source = extract_source_location(caller)
227
+
228
+ msg = "#{msg}\n #{separator}#{source.join("\n")}" unless source.empty?
229
+ end
230
+
231
+ $stdout.puts(colorize_debug(msg))
232
+ rescue Encoding::CompatibilityError
233
+ should_retry = separator != " - "
234
+ separator = " - "
235
+ retry if should_retry
207
236
  end
208
-
209
- $stdout.puts(colorize_debug(msg))
210
237
  end
211
238
 
212
239
  def extract_source_location(locations)
@@ -233,3 +260,6 @@ require "isolator/orm_adapters"
233
260
  require "isolator/adapters"
234
261
  require "isolator/railtie" if defined?(Rails)
235
262
  require "isolator/database_cleaner_support" if defined?(DatabaseCleaner)
263
+
264
+ # Built-in extensions
265
+ require "isolator/plugins/concurrent_database_transactions"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isolator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-16 00:00:00.000000000 Z
11
+ date: 2023-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sniffer
@@ -313,7 +313,9 @@ files:
313
313
  - lib/isolator/orm_adapters.rb
314
314
  - lib/isolator/orm_adapters/active_record.rb
315
315
  - lib/isolator/orm_adapters/active_support_subscriber.rb
316
+ - lib/isolator/orm_adapters/active_support_transaction_subscriber.rb
316
317
  - lib/isolator/orm_adapters/rom_active_support.rb
318
+ - lib/isolator/plugins/concurrent_database_transactions.rb
317
319
  - lib/isolator/railtie.rb
318
320
  - lib/isolator/simple_hashie.rb
319
321
  - lib/isolator/version.rb
@@ -335,14 +337,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
335
337
  requirements:
336
338
  - - ">="
337
339
  - !ruby/object:Gem::Version
338
- version: 2.6.0
340
+ version: 2.7.0
339
341
  required_rubygems_version: !ruby/object:Gem::Requirement
340
342
  requirements:
341
343
  - - ">="
342
344
  - !ruby/object:Gem::Version
343
345
  version: '0'
344
346
  requirements: []
345
- rubygems_version: 3.4.8
347
+ rubygems_version: 3.4.20
346
348
  signing_key:
347
349
  specification_version: 4
348
350
  summary: Detect non-atomic interactions within DB transactions