isolator 0.9.0 → 0.11.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: 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