isolator 0.9.0 → 0.11.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: 529433e0e838e663f335a5fb493f51d10f16fbde2168c89be72e92c968709593
4
- data.tar.gz: ef33db058e25204e615fae7ee945c738e516e8ae6d63bb93f9660e6e117cb632
3
+ metadata.gz: fb3ecc2b4b93f35241a5f03804bc89fab1ab2a6d9d4fe33753ad827bc8a06ea5
4
+ data.tar.gz: 8385b3b3654b25d1ec2ec178305cac41049523339da8d56c6ba7defb31996640
5
5
  SHA512:
6
- metadata.gz: 5cb63aa9a96f5931425267193590550cac0614dd15d272bd7d33f7e2aeaad0b7b37a19f5a6f7d8617dce992e946b92c42c3df5829177cbd51b41d8919abdc8c5
7
- data.tar.gz: b4997855a9d813a961491235097a42eee75d55598fb7d33d53bb4f7e3828f486ea0a2fb788edd028237e39518a2d8e962824b08a3f4cbed2ce3ac22acdfee9c3
6
+ metadata.gz: a18d105a45e116af3120e4f9eefd69f1c533b14f1ce26b8bdd9c284fd99f87ddd8e3650ae3e7a73cc54592329726f549d16b374763f5bae6c71163e36499b55a
7
+ data.tar.gz: cb72be0a0d3aa004dab07392b875e0d70472806517d7d89acdafd133f332a50f59232aed8c919c12ce15cd12399b29f7ef8e33f986e445046b9171765db4ae74
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.11.0 (2023-09-27)
6
+
7
+ - Use Rails new `transaction.active_record` event if available to better handle edge cases. ([@palkan][])
8
+
9
+ - Fix logging non-UTF8 strings. ([@palkan][])
10
+
11
+ Fixes [#66](https://github.com/palkan/isolator/issues/66)
12
+
13
+ ## 0.10.0 (2023-08-15)
14
+
15
+ - Support multiple databases with DatabaseCleaner. ([@palkan][])
16
+
17
+ - Fix query having invalid characters. ([@tagirahmad][])
18
+
19
+ Fixes [#43](https://github.com/palkan/isolator/issues/43).
20
+
5
21
  ## 0.9.0 (2023-05-18)
6
22
 
7
23
  - Support keyword arguments to isolated method in Ruby 3.0. ([@Mange][])
@@ -109,3 +125,5 @@ This, for example, makes Isolator compatible with Rails multi-database apps.
109
125
  [@mquan]: https://github.com/mquan
110
126
  [@bobbymcwho]: https://github.com/bobbymcwho
111
127
  [@Mange]: https://github.com/Mange
128
+ [@tomgi]: https://github.com/tomgi
129
+ [@tagirahmad]: https://github.com/tagirahmad
@@ -33,7 +33,8 @@ module Isolator
33
33
 
34
34
  Module.new do
35
35
  define_method method_name do |*args, **kwargs, &block|
36
- adapter.notify(caller, self, *args, **kwargs)
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)
37
38
  super(*args, **kwargs, &block)
38
39
  end
39
40
  end
@@ -7,5 +7,6 @@ adapter = Isolator.isolate :webmock,
7
7
  }
8
8
 
9
9
  WebMock.after_request do |*args|
10
- adapter.notify(caller, *args)
10
+ # check if we are even notifying before calling `caller`, which is well known to be slow
11
+ adapter.notify(caller, *args) if adapter.notify?(*args)
11
12
  end
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "database_cleaner/active_record/transaction"
4
-
5
4
  ::DatabaseCleaner::ActiveRecord::Transaction.prepend(
6
5
  Module.new do
7
6
  def start
8
7
  super
9
- Isolator.transactions_threshold += 1
8
+ connection_id = connection_class.connection.object_id
9
+ Isolator.set_connection_threshold(
10
+ Isolator.transactions_threshold(connection_id) + 1,
11
+ connection_id
12
+ )
10
13
  end
11
14
 
12
15
  def clean
13
- Isolator.transactions_threshold -= 1
16
+ connection_id = connection_class.connection.object_id
17
+ Isolator.set_connection_threshold(
18
+ Isolator.transactions_threshold(connection_id) - 1,
19
+ connection_id
20
+ )
14
21
  super
15
22
  end
16
23
  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
@@ -2,4 +2,10 @@
2
2
 
3
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,12 +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
- Isolator.incr_transactions!(connection_id) if START_PATTERN.match?(query[:sql])
14
- Isolator.decr_transactions!(connection_id) if FINISH_PATTERN.match?(query[:sql])
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))
15
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)
16
50
  end
17
51
  end
18
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
3
  module Isolator
4
- VERSION = "0.9.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/isolator.rb CHANGED
@@ -198,15 +198,23 @@ module Isolator
198
198
 
199
199
  def debug!(msg)
200
200
  return unless debug_enabled
201
- msg = "[ISOLATOR DEBUG] #{msg}"
202
201
 
203
- if backtrace_cleaner && backtrace_length.positive?
204
- source = extract_source_location(caller)
202
+ separator = " ↳ "
205
203
 
206
- msg = "#{msg}\n ↳ #{source.join("\n")}" unless source.empty?
204
+ begin
205
+ msg = "[ISOLATOR DEBUG] #{msg}"
206
+ if backtrace_cleaner && backtrace_length.positive?
207
+ source = extract_source_location(caller)
208
+
209
+ msg = "#{msg}\n #{separator}#{source.join("\n")}" unless source.empty?
210
+ end
211
+
212
+ $stdout.puts(colorize_debug(msg))
213
+ rescue Encoding::CompatibilityError
214
+ should_retry = separator != " - "
215
+ separator = " - "
216
+ retry if should_retry
207
217
  end
208
-
209
- $stdout.puts(colorize_debug(msg))
210
218
  end
211
219
 
212
220
  def extract_source_location(locations)
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.9.0
4
+ version: 0.11.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-05-18 00:00:00.000000000 Z
11
+ date: 2023-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sniffer
@@ -313,6 +313,7 @@ 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
317
318
  - lib/isolator/railtie.rb
318
319
  - lib/isolator/simple_hashie.rb