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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +32 -2
- data/lib/isolator/adapter_builder.rb +11 -5
- data/lib/isolator/adapters/base.rb +2 -2
- data/lib/isolator/adapters/http/sniffer.rb +6 -6
- data/lib/isolator/adapters/mailers/mail.rb +7 -7
- data/lib/isolator/callbacks.rb +24 -0
- data/lib/isolator/notifier.rb +14 -6
- data/lib/isolator/orm_adapters/active_record.rb +8 -2
- data/lib/isolator/orm_adapters/active_support_subscriber.rb +39 -7
- data/lib/isolator/orm_adapters/active_support_transaction_subscriber.rb +74 -0
- data/lib/isolator/orm_adapters/rom_active_support.rb +1 -1
- data/lib/isolator/plugins/concurrent_database_transactions.rb +25 -0
- data/lib/isolator/railtie.rb +23 -18
- data/lib/isolator/version.rb +1 -1
- data/lib/isolator.rb +42 -12
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc5300f95e1333baa45e8e10e1e13b1ebc03175b648791bfec5a87e160643e9e
|
4
|
+
data.tar.gz: e184eb7fc257b0ab5ae95878ea955769d9925b8b428dc6c469bcde5af89c69f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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` >=
|
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
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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?(
|
31
|
-
enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
+
}
|
data/lib/isolator/callbacks.rb
CHANGED
@@ -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
|
data/lib/isolator/notifier.rb
CHANGED
@@ -29,14 +29,22 @@ module Isolator
|
|
29
29
|
def log_exception
|
30
30
|
return unless Isolator.config.logger
|
31
31
|
|
32
|
-
|
33
|
-
"#{exception.message}"
|
32
|
+
separator = " ↳ "
|
34
33
|
|
35
|
-
|
36
|
-
msg
|
37
|
-
|
34
|
+
begin
|
35
|
+
msg = "[ISOLATOR EXCEPTION]\n" \
|
36
|
+
"#{exception.message}"
|
38
37
|
|
39
|
-
|
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 "
|
3
|
+
require_relative "active_support_subscriber"
|
4
4
|
|
5
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Isolator.
|
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
|
@@ -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
|
data/lib/isolator/railtie.rb
CHANGED
@@ -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
|
-
|
35
|
-
::ActiveRecord::TestFixtures.prepend(
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
data/lib/isolator/version.rb
CHANGED
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
|
-
|
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
|
-
|
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
|
-
|
204
|
-
source = extract_source_location(caller)
|
221
|
+
separator = " ↳ "
|
205
222
|
|
206
|
-
|
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.
|
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-
|
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.
|
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.
|
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
|