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

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

Parameters:

  • replica_connection_spec_hash_or_url (String, Hash)

    an ActiveRecord connection specification or a DSN URL



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

Parameters:

  • replica_connection_pool (ActiveRecord::ConnectionAdapters::ConnectionPool)

    an ActiveRecord connection pool instance



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