Module: AutoReplica
- Defined in:
- lib/activerecord_autoreplica.rb
Overview
The idea is this: we can have ActiveRecord::Base.connection return an Adapter object (that executes the actual SQL queries) and inside that adapter we can redirect all SELECT queries to the read replica instead of the main database. This is however slightly more involved than it looks, because if we keep calling establish_connection willy-nilly we are going to disconnect from the master DB, connect to the read DB, disconnect again and so on.
The right approach for this is to maintain a separate connection pool instad just for connections to the read slave. This is exactly what we are doing within AutoReplica.
The setup to make it work is a bit involved. If you want to play with how ActiveRecord uses connections, the only actual "official" hook you can use is replacing the connection handler it uses. When a SQL request needs to be run AR walks the following dependency chain to arrive at the actual connection:
- SomeRecord.connection calls ActiveRecord::Base.connection_handler
- It then asks the connection handler for a connection for this specific ActiveRecord subclass, or barring that
- for the connection for one of it's ancestors, ending with ActiveRecord::Base itself
- The ConnectionHandler, in turn, asks one of its managed ConnectionPool objects to give it a connection for use.
- The connection is returned and then the query is ran against it.
This is why the only integration point for this is ++ActiveRecord::Base.connection_handler=++ To make our trick work, here is what we do:
First we wrap the original ConnectionHandler used by ActiveRecord in our own proxy. That proxy will ask the original ConnectionHandler for a connection to use, but it will also maintain it's own ConnectionPool just for the read replica connections.
When a connection is returned by the original Handler, it is wrapped into a Adapter together with the read connection obtained from the special read pool. That proxy will intercept all of the SQL methods for SELECT and redirect them to the read connection instead. All of the other methods (including transactions) are still going to be executed on the master database.
Once the block exits, the original connection handler is reassigned to the AR connection_pool.
Defined Under Namespace
Classes: AdHocConnectionHandler, Adapter, ConnectionHandler
Constant Summary collapse
- CONNECTION_SWITCHING_MUTEX =
Mutex.new
- ConnectionSpecification =
The first one is used in ActiveRecord 3+, the second one in 4+
begin ActiveRecord::Base::ConnectionSpecification rescue ActiveRecord::ConnectionAdapters::ConnectionSpecification end
Class Method Summary collapse
- .in_replica_context(handler_params, handler_class = ConnectionHandler) ⇒ Object
-
.using_read_replica_at(replica_connection_spec_hash_or_url) ⇒ void
Runs a given block with all SELECT statements being executed against the read slave database.
-
.using_read_replica_pool(replica_connection_pool) ⇒ void
Runs a given block with all SELECT statements being executed using the read slave connection pool.
Class Method Details
.in_replica_context(handler_params, handler_class = ConnectionHandler) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/activerecord_autoreplica.rb', line 72 def self.in_replica_context(handler_params, handler_class=ConnectionHandler) return yield if Thread.current[:autoreplica] # This method should not be reentrant original_connection_handler = ActiveRecord::Base.connection_handler custom_handler = handler_class.new(original_connection_handler, handler_params) begin CONNECTION_SWITCHING_MUTEX.synchronize do Thread.current[:autoreplica] = true ActiveRecord::Base.connection_handler = custom_handler end yield ensure CONNECTION_SWITCHING_MUTEX.synchronize do Thread.current[:autoreplica] = false ActiveRecord::Base.connection_handler = original_connection_handler end custom_handler.finish end end |
.using_read_replica_at(replica_connection_spec_hash_or_url) ⇒ void
This method returns an undefined value.
Runs a given block with all SELECT statements being executed against the read slave database.
AutoReplica.using_read_replica_at(:adapter => 'mysql2', :database => 'read_replica', ...) do
customer = Customer.find(3) # Will SELECT from the replica database at the connection spec passed to the block
customer.register_complaint! # Will UPDATE to the master database connection
end
53 54 55 |
# File 'lib/activerecord_autoreplica.rb', line 53 def self.using_read_replica_at(replica_connection_spec_hash_or_url) in_replica_context(replica_connection_spec_hash_or_url, AdHocConnectionHandler){ yield } end |
.using_read_replica_pool(replica_connection_pool) ⇒ void
This method returns an undefined value.
Runs a given block with all SELECT statements being executed using the read slave connection pool.
read_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(:adapter => 'mysql2', :database => 'read_replica', ...)
AutoReplica.using_read_replica_pool(read_pool) do
customer = Customer.find(3) # Will SELECT from the replica database picked off the read pool
customer.register_complaint! # Will UPDATE to the master database connection
end
68 69 70 |
# File 'lib/activerecord_autoreplica.rb', line 68 def self.using_read_replica_pool(replica_connection_pool) in_replica_context(replica_connection_pool){ yield } end |