isolator 0.10.0 → 1.0.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: 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