From f63df2bddac43d9716d3f96f89da1c79d583b04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Fran=C3=A7a?= Date: Fri, 16 Aug 2019 13:43:32 -0400 Subject: [PATCH] Merge pull request #36949 from 97jaz/thread-local-prepared-statements Make prepared statement status thread and instance-specific --- .../connection_adapters/abstract_adapter.rb | 16 +++--- .../connection_adapters/mysql2_adapter.rb | 4 +- .../cases/prepared_statement_status_test.rb | 50 +++++++++++++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 activerecord/test/cases/prepared_statement_status_test.rb diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c2d9027e8ebd0..19c7f3d1effd0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -11,6 +11,7 @@ require "arel/collectors/composite" require "arel/collectors/sql_string" require "arel/collectors/substitute_binds" +require "concurrent/atomic/thread_local_var" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -78,7 +79,7 @@ class AbstractAdapter SIMPLE_INT = /\A\d+\z/ attr_accessor :pool - attr_reader :visitor, :owner, :logger, :lock, :prepared_statements + attr_reader :visitor, :owner, :logger, :lock alias :in_use? :owner set_callback :checkin, :after, :enable_lazy_transactions! @@ -129,10 +130,10 @@ def initialize(connection, logger = nil, config = {}) # :nodoc: @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true + @prepared_statement_status = Concurrent::ThreadLocalVar.new(true) @visitor.extend(DetermineIfPreparableVisitor) else - @prepared_statements = false + @prepared_statement_status = Concurrent::ThreadLocalVar.new(false) end @advisory_locks_enabled = self.class.type_cast_config_to_boolean( @@ -175,6 +176,10 @@ def schema_migration # :nodoc: end end + def prepared_statements + @prepared_statement_status.value + end + class Version include Comparable @@ -259,10 +264,7 @@ def seconds_idle # :nodoc: end def unprepared_statement - old_prepared_statements, @prepared_statements = @prepared_statements, false - yield - ensure - @prepared_statements = old_prepared_statements + @prepared_statement_status.bind(false) { yield } end # Returns the human-readable name of the adapter. Use mixed case - one diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 3ad68fbb6b6f6..c834796f30828 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -39,8 +39,8 @@ class Mysql2Adapter < AbstractMysqlAdapter include MySQL::DatabaseStatements def initialize(connection, logger, connection_options, config) - super - @prepared_statements = false unless config.key?(:prepared_statements) + superclass_config = config.reverse_merge(prepared_statements: false) + super(connection, logger, connection_options, superclass_config) configure_connection end diff --git a/activerecord/test/cases/prepared_statement_status_test.rb b/activerecord/test/cases/prepared_statement_status_test.rb new file mode 100644 index 0000000000000..1f0d6402537f0 --- /dev/null +++ b/activerecord/test/cases/prepared_statement_status_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/course" +require "models/entrant" + +module ActiveRecord + class PreparedStatementStatusTest < ActiveRecord::TestCase + def test_prepared_statement_status_is_thread_and_instance_specific + course_conn = Course.connection + entrant_conn = Entrant.connection + + inside = Concurrent::Event.new + preventing = Concurrent::Event.new + finished = Concurrent::Event.new + + assert_not_same course_conn, entrant_conn + + if current_adapter?(:Mysql2Adapter) + # The mysql adapter does not use prepared + # statements by default. + assert_not course_conn.prepared_statements + assert_not entrant_conn.prepared_statements + else + t1 = Thread.new do + course_conn.unprepared_statement do + inside.set + preventing.wait + assert_not course_conn.prepared_statements + assert entrant_conn.prepared_statements + finished.set + end + end + + t2 = Thread.new do + entrant_conn.unprepared_statement do + inside.wait + assert course_conn.prepared_statements + assert_not entrant_conn.prepared_statements + preventing.set + finished.wait + end + end + + t1.join + t2.join + end + end + end +end