let EXPLAIN use a thread locals registry [John J. Wang & Xavier Noria]

Closes #10198.
This commit is contained in:
Xavier Noria 2013-04-16 22:46:55 +02:00
parent 0513f6ca9c
commit ef7a48df75
5 changed files with 70 additions and 46 deletions

View File

@ -1,22 +1,22 @@
require 'active_support/lazy_load_hooks' require 'active_support/lazy_load_hooks'
require 'active_record/explain_registry'
module ActiveRecord module ActiveRecord
module Explain module Explain
# Relation#explain needs to be able to collect the queries. # Executes the block with the collect flag enabled. Queries are collected
# asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc: def collecting_queries_for_explain # :nodoc:
current = Thread.current ExplainRegistry.collect = true
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
yield yield
return current[:available_queries_for_explain] ExplainRegistry.queries
ensure ensure
# Note that the return value above does not depend on this assignment. ExplainRegistry.reset
current[:available_queries_for_explain] = original
end end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged. # Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc: def exec_explain(queries) # :nodoc:
str = queries && queries.map do |sql, bind| str = queries.map do |sql, bind|
[].tap do |msg| [].tap do |msg|
msg << "EXPLAIN for: #{sql}" msg << "EXPLAIN for: #{sql}"
unless bind.empty? unless bind.empty?
@ -31,6 +31,7 @@ module ActiveRecord
def str.inspect def str.inspect
self self
end end
str str
end end
end end

View File

@ -0,0 +1,30 @@
require 'active_support/per_thread_registry'
module ActiveRecord
# This is a thread locals registry for EXPLAIN. For example
#
# ActiveRecord::ExplainRegistry.queries
#
# returns the collected queries local to the current thread.
#
# See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
# for further details.
class ExplainRegistry
extend ActiveSupport::PerThreadRegistry
attr_accessor :queries, :collect
def initialize
reset
end
def collect?
@collect
end
def reset
@collect = false
@queries = []
end
end
end

View File

@ -1,4 +1,5 @@
require 'active_support/notifications' require 'active_support/notifications'
require 'active_record/explain_registry'
module ActiveRecord module ActiveRecord
class ExplainSubscriber # :nodoc: class ExplainSubscriber # :nodoc:
@ -7,8 +8,8 @@ module ActiveRecord
end end
def finish(name, id, payload) def finish(name, id, payload)
if queries = Thread.current[:available_queries_for_explain] if ExplainRegistry.collect? && !ignore_payload?(payload)
queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) ExplainRegistry.queries << payload.values_at(:sql, :binds)
end end
end end

View File

@ -22,13 +22,6 @@ module ActiveRecord
assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
assert_match %(Seq Scan on audit_logs), explain assert_match %(Seq Scan on audit_logs), explain
end end
def test_dont_explain_for_set_search_path
queries = Thread.current[:available_queries_for_explain] = []
ActiveRecord::Base.connection.schema_search_path = "public"
assert queries.empty?
end
end end
end end
end end

View File

@ -1,55 +1,54 @@
require 'cases/helper' require 'cases/helper'
require 'active_record/explain_subscriber'
require 'active_record/explain_registry'
if ActiveRecord::Base.connection.supports_explain? if ActiveRecord::Base.connection.supports_explain?
class ExplainSubscriberTest < ActiveRecord::TestCase class ExplainSubscriberTest < ActiveRecord::TestCase
SUBSCRIBER = ActiveRecord::ExplainSubscriber.new SUBSCRIBER = ActiveRecord::ExplainSubscriber.new
def test_collects_nothing_if_available_queries_for_explain_is_nil def setup
with_queries(nil) do ActiveRecord::ExplainRegistry.reset
SUBSCRIBER.finish(nil, nil, {}) ActiveRecord::ExplainRegistry.collect = true
assert_nil Thread.current[:available_queries_for_explain]
end
end end
def test_collects_nothing_if_the_payload_has_an_exception def test_collects_nothing_if_the_payload_has_an_exception
with_queries([]) do |queries| SUBSCRIBER.finish(nil, nil, exception: Exception.new)
SUBSCRIBER.finish(nil, nil, :exception => Exception.new) assert queries.empty?
assert queries.empty?
end
end end
def test_collects_nothing_for_ignored_payloads def test_collects_nothing_for_ignored_payloads
with_queries([]) do |queries| ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| SUBSCRIBER.finish(nil, nil, name: ip)
SUBSCRIBER.finish(nil, nil, :name => ip)
end
assert queries.empty?
end end
assert queries.empty?
end
def test_collects_nothing_if_collect_is_false
ActiveRecord::ExplainRegistry.collect = false
SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2])
assert queries.empty?
end end
def test_collects_pairs_of_queries_and_binds def test_collects_pairs_of_queries_and_binds
sql = 'select 1 from users' sql = 'select 1 from users'
binds = [1, 2] binds = [1, 2]
with_queries([]) do |queries| SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: sql, binds: binds)
SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) assert_equal 1, queries.size
assert_equal 1, queries.size assert_equal sql, queries[0][0]
assert_equal sql, queries[0][0] assert_equal binds, queries[0][1]
assert_equal binds, queries[0][1]
end
end end
def test_collects_nothing_if_unexplained_sqls def test_collects_nothing_if_the_statement_is_not_whitelisted
with_queries([]) do |queries| SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length')
SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length') assert queries.empty?
assert queries.empty?
end
end end
def with_queries(queries) def teardown
Thread.current[:available_queries_for_explain] = queries ActiveRecord::ExplainRegistry.reset
yield queries end
ensure
Thread.current[:available_queries_for_explain] = nil def queries
ActiveRecord::ExplainRegistry.queries
end end
end end
end end