active_record_query_counter 3.0.0 → 3.1.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: 89a919244edbce9bf36bdaa232078b04470f1bacc952469eab4a4fffc2416e08
4
- data.tar.gz: 97a66d47fda85ad15b832da3e390f8cc035a374983c64f53e4d673094b54d8b9
3
+ metadata.gz: 1966d4714e8fc7a3d77b86be07c8c930470d2af298dbd8d88d4bee4043a4da84
4
+ data.tar.gz: 8ad2b534042042259038f72daff078b6302b552f6a742b4f38f0504afe9a8919
5
5
  SHA512:
6
- metadata.gz: c5c657cf312deb55e21063b5d08f824cd762e42050704b06fc0610e00f938aacac8fc37050c60d293452521841ca614430cb50cd3bf10d3bca794df9d0dee6d0
7
- data.tar.gz: d81cb599eea68c911fad417a902848f1b1f2d0f29f4d095b70d5af8ef2f6366dac0e2b2de0fe534ed51fcbffb3d696147edfc53e64b549862a4dfe78a156a54d
6
+ metadata.gz: 13a2c8c037ea0b47ea11384e133c294d047a74b34f89564a49adb0c339e2e489a1d61ef8a48c76d31b7a7abbd9afca6caf9ebfea011dcd703dbfec19fa65e46f
7
+ data.tar.gz: 6ba58ac495883fa98d20e7cda7ae363444f279db7a543016a6ffcdacf1f4f2d7404c8f7f7d06425c9f159d004788c73bde3fb857ce3e63a218380d6d00e359fd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 3.1.0
8
+
9
+ ### Changed
10
+
11
+ - Transaction time now ends when the COMMIT or ROLLBACK statement completes rather than before it is sent, so it includes the commit itself. Time spent in after commit and after rollback callbacks is not included since those run after the database transaction is over.
12
+ - The counter is now stored in `ActiveSupport::IsolatedExecutionState` when available (Rails 7+) so that query counting follows the application's configured thread or fiber isolation level. On Rails 6.x the counter remains fiber-local.
13
+
14
+ ### Fixed
15
+
16
+ - A transaction whose COMMIT statement fails (e.g. a deadlock or serialization failure detected at commit time) is now counted as a rollback. Previously it was recorded as a successful commit and the rollback count was not incremented.
17
+ - `ActiveRecordQueryCounter.last_transaction_end_time` now returns the latest transaction end time when transactions on multiple connections overlap. Previously it returned the end time of the transaction that started last.
18
+ - Cached queries for ignored statements (`SCHEMA`, `EXPLAIN`) are no longer counted in the cached query count.
19
+ - Setting up the query cache subscription in `ActiveRecordQueryCounter.enable!` is now thread safe, so concurrent calls can no longer create duplicate subscriptions that would double count cached queries.
20
+
7
21
  ## 3.0.0
8
22
 
9
23
  ### Changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.0
1
+ 3.1.0
@@ -52,13 +52,13 @@ module ActiveRecordQueryCounter
52
52
  end
53
53
 
54
54
  module ExecQuery
55
- def exec_query(sql, name = nil, binds = [], *args, **kwargs)
55
+ def exec_query(sql, name = nil, binds = [], ...)
56
56
  ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
57
57
  end
58
58
  end
59
59
 
60
60
  module InternalExecQuery
61
- def internal_exec_query(sql, name = nil, binds = [], *args, **kwargs)
61
+ def internal_exec_query(sql, name = nil, binds = [], ...)
62
62
  ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
63
63
  end
64
64
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordQueryCounter
4
+ # Extension to ActiveRecord::ConnectionAdapters::RealTransaction to count transactions.
5
+ # Real transactions are always the outermost database transaction; savepoint transactions
6
+ # nested inside them are considered part of the transaction and are not counted separately.
7
+ module TransactionExtension
8
+ class << self
9
+ def inject(transaction_class)
10
+ unless transaction_class.include?(self)
11
+ transaction_class.prepend(self)
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize(...)
17
+ super
18
+ @active_record_query_counter_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
20
+
21
+ def commit(...)
22
+ # The transaction is only recorded after the COMMIT succeeds. If it raises, the start
23
+ # time is left in place so the rollback that Rails performs next is counted as a
24
+ # rollback rather than a successful commit. Recording here rather than in the
25
+ # transaction manager keeps time spent in after commit callbacks (which run after this
26
+ # method returns) out of the transaction time.
27
+ retval = super
28
+ active_record_query_counter_record_transaction
29
+ retval
30
+ end
31
+
32
+ def rollback(...)
33
+ super
34
+ ensure
35
+ # Recorded even if the ROLLBACK itself raises (e.g. the connection died) since the
36
+ # transaction is over either way.
37
+ active_record_query_counter_record_transaction(rollback: true)
38
+ end
39
+
40
+ private
41
+
42
+ def active_record_query_counter_record_transaction(rollback: false)
43
+ start_time = @active_record_query_counter_start_time
44
+ return unless start_time
45
+
46
+ @active_record_query_counter_start_time = nil
47
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
48
+ ActiveRecordQueryCounter.add_transaction(start_time, end_time)
49
+ ActiveRecordQueryCounter.increment_rollbacks if rollback
50
+ end
51
+ end
52
+ end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  require_relative "active_record_query_counter/connection_adapter_extension"
4
6
  require_relative "active_record_query_counter/counter"
5
7
  require_relative "active_record_query_counter/thresholds"
6
8
  require_relative "active_record_query_counter/transaction_info"
7
- require_relative "active_record_query_counter/transaction_manager_extension"
9
+ require_relative "active_record_query_counter/transaction_extension"
8
10
 
9
11
  # Everything you need to count ActiveRecord queries and row counts within a block.
10
12
  #
@@ -16,13 +18,17 @@ require_relative "active_record_query_counter/transaction_manager_extension"
16
18
  # puts ActiveRecordQueryCounter.row_count
17
19
  # end
18
20
  module ActiveRecordQueryCounter
21
+ VERSION = File.read(File.join(__dir__, "..", "VERSION")).strip
22
+
19
23
  autoload :RackMiddleware, "active_record_query_counter/rack_middleware"
20
24
  autoload :SidekiqMiddleware, "active_record_query_counter/sidekiq_middleware"
21
- autoload :VERSION, "active_record_query_counter/version"
22
25
 
23
26
  IGNORED_STATEMENTS = %w[SCHEMA EXPLAIN].freeze
24
27
  private_constant :IGNORED_STATEMENTS
25
28
 
29
+ @lock = Mutex.new
30
+ @default_thresholds = Thresholds.new
31
+
26
32
  class << self
27
33
  # Enable query counting within a block.
28
34
  #
@@ -37,8 +43,8 @@ module ActiveRecordQueryCounter
37
43
 
38
44
  transaction_count = counter.transaction_count
39
45
  if transaction_count > 0
40
- transaction_threshold = (counter.thresholds.transaction_count || -1)
41
- if transaction_threshold >= 0 && transaction_count >= transaction_threshold
46
+ transaction_threshold = counter.thresholds.transaction_count || -1
47
+ if transaction_threshold.between?(0, transaction_count)
42
48
  send_notification("transaction_count", counter.first_transaction_start_time, counter.last_transaction_end_time, transactions: counter.transactions)
43
49
  end
44
50
  end
@@ -98,15 +104,15 @@ module ActiveRecordQueryCounter
98
104
  notification_end_time = start_time + query_time
99
105
 
100
106
  trace = nil
101
- query_time_threshold = (counter.thresholds.query_time || -1)
102
- if query_time_threshold >= 0 && query_time >= query_time_threshold
107
+ query_time_threshold = counter.thresholds.query_time || -1
108
+ if query_time_threshold.between?(0, query_time)
103
109
  trace = backtrace
104
110
  payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time)
105
111
  send_notification("query_time", start_time, notification_end_time, **payload)
106
112
  end
107
113
 
108
- row_count_threshold = (counter.thresholds.row_count || -1)
109
- if row_count_threshold >= 0 && row_count >= row_count_threshold
114
+ row_count_threshold = counter.thresholds.row_count || -1
115
+ if row_count_threshold.between?(0, row_count)
110
116
  trace ||= backtrace
111
117
  payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time)
112
118
  send_notification("row_count", start_time, notification_end_time, **payload)
@@ -126,8 +132,8 @@ module ActiveRecordQueryCounter
126
132
  trace = backtrace
127
133
  counter.add_transaction(trace: trace, start_time: start_time, end_time: end_time)
128
134
 
129
- transaction_time_threshold = (counter.thresholds.transaction_time || -1)
130
- if transaction_time_threshold >= 0 && end_time - start_time >= transaction_time_threshold
135
+ transaction_time_threshold = counter.thresholds.transaction_time || -1
136
+ if transaction_time_threshold.between?(0, end_time - start_time)
131
137
  send_notification("transaction_time", start_time, end_time, trace: backtrace)
132
138
  end
133
139
  end
@@ -213,7 +219,7 @@ module ActiveRecordQueryCounter
213
219
  # @return [Float, nil] the monotonic time when the last transaction ended,
214
220
  def last_transaction_end_time
215
221
  counter = current_counter
216
- counter.transactions.last&.end_time if counter.is_a?(Counter)
222
+ counter.last_transaction_end_time if counter.is_a?(Counter)
217
223
  end
218
224
 
219
225
  # Return an array of transaction information for any transactions that have been counted
@@ -259,9 +265,7 @@ module ActiveRecordQueryCounter
259
265
  # thresholds are used as the default values.
260
266
  #
261
267
  # @return [ActiveRecordQueryCounter::Thresholds]
262
- def default_thresholds
263
- @default_thresholds ||= Thresholds.new
264
- end
268
+ attr_reader :default_thresholds
265
269
 
266
270
  # Get the current local notification thresholds. These thresholds are only used within
267
271
  # the current `count_queries` block.
@@ -276,25 +280,39 @@ module ActiveRecordQueryCounter
276
280
  def enable!(connection_class)
277
281
  ActiveSupport.on_load(:active_record) do
278
282
  ConnectionAdapterExtension.inject(connection_class)
279
- TransactionManagerExtension.inject(ActiveRecord::ConnectionAdapters::TransactionManager)
283
+ TransactionExtension.inject(ActiveRecord::ConnectionAdapters::RealTransaction)
280
284
  end
281
285
 
282
- @cache_subscription ||= ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start_time, _end_time, _id, payload|
283
- if payload[:cached]
284
- counter = current_counter
285
- counter.cached_query_count += 1 if counter
286
+ @lock.synchronize do
287
+ @cache_subscription ||= ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start_time, _end_time, _id, payload|
288
+ if payload[:cached] && !IGNORED_STATEMENTS.include?(payload[:name])
289
+ counter = current_counter
290
+ counter.cached_query_count += 1 if counter.is_a?(Counter)
291
+ end
286
292
  end
287
293
  end
288
294
  end
289
295
 
290
296
  private
291
297
 
298
+ # The counter is stored in ActiveSupport::IsolatedExecutionState when available so that
299
+ # it follows the application's configured isolation level (thread or fiber). The fallback
300
+ # for ActiveSupport 6.x uses Thread.current, which is fiber-local, so on those versions
301
+ # queries executed in a fiber spawned inside the block are not counted.
292
302
  def current_counter
293
- Thread.current[:active_record_query_counter]
303
+ if defined?(ActiveSupport::IsolatedExecutionState)
304
+ ActiveSupport::IsolatedExecutionState[:active_record_query_counter]
305
+ else
306
+ Thread.current[:active_record_query_counter]
307
+ end
294
308
  end
295
309
 
296
310
  def current_counter=(counter)
297
- Thread.current[:active_record_query_counter] = counter
311
+ if defined?(ActiveSupport::IsolatedExecutionState)
312
+ ActiveSupport::IsolatedExecutionState[:active_record_query_counter] = counter
313
+ else
314
+ Thread.current[:active_record_query_counter] = counter
315
+ end
298
316
  end
299
317
 
300
318
  def send_notification(name, start_time, end_time, payload = {})
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_query_counter
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -54,9 +54,8 @@ files:
54
54
  - lib/active_record_query_counter/rack_middleware.rb
55
55
  - lib/active_record_query_counter/sidekiq_middleware.rb
56
56
  - lib/active_record_query_counter/thresholds.rb
57
+ - lib/active_record_query_counter/transaction_extension.rb
57
58
  - lib/active_record_query_counter/transaction_info.rb
58
- - lib/active_record_query_counter/transaction_manager_extension.rb
59
- - lib/active_record_query_counter/version.rb
60
59
  homepage: https://github.com/bdurand/active_record_query_counter
61
60
  licenses:
62
61
  - MIT
@@ -78,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
77
  - !ruby/object:Gem::Version
79
78
  version: '0'
80
79
  requirements: []
81
- rubygems_version: 3.6.9
80
+ rubygems_version: 4.0.3
82
81
  specification_version: 4
83
82
  summary: Provides detailed insights into how your code interacts with the database
84
83
  by hooking into ActiveRecord.
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveRecordQueryCounter
4
- # Extension to ActiveRecord::ConnectionAdapters::TransactionManager to count transactions.
5
- module TransactionManagerExtension
6
- class << self
7
- def inject(transaction_manager_class)
8
- unless transaction_manager_class.include?(self)
9
- transaction_manager_class.prepend(self)
10
- end
11
- end
12
- end
13
-
14
- def begin_transaction(*args, **kwargs)
15
- if open_transactions == 0
16
- @active_record_query_counter_transaction_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
- end
18
- super
19
- end
20
-
21
- def commit_transaction(*args)
22
- if @active_record_query_counter_transaction_start_time && open_transactions == 1
23
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
- ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
25
- @active_record_query_counter_transaction_start_time = nil
26
- end
27
- super
28
- end
29
-
30
- def rollback_transaction(*args)
31
- if @active_record_query_counter_transaction_start_time && open_transactions == 1
32
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
- ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
34
- ActiveRecordQueryCounter.increment_rollbacks
35
- @active_record_query_counter_transaction_start_time = nil
36
- end
37
- super
38
- end
39
- end
40
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveRecordQueryCounter
4
- VERSION = File.read(File.join(__dir__, "..", "..", "VERSION")).strip
5
- end