tsukasaoishi / fresh_connection

FreshConnection provides access to one or more configured database replicas.
MIT License
58 stars 11 forks source link

Inconsistent ActiveRecord Query Caching Between Master and Replica Connections #17

Closed aks closed 6 years ago

aks commented 7 years ago

There is a problem with Rails caching and fresh_connection.

  1. fresh_connection maintains a replica connection pool, consisting of multiple connection instances, separate from the master connection pool, also one or more connection instances.

  2. The ActiveRecord Query Cache module inserts itself as class methods into the AbstractAdapter, but maintains the actual query cache as an instance variable on the connection instance.

These two facts means that the query caches for the "master" connections are separate from that of the "replica" connections. So, the AR::Base methods of update, insert, and delete methods, which clear the cache attached to a given connection, do not clear the corresponding cache in the other pool connections.

So, when a read-only query for an AR model object is diverted to a replica connection, it's cache is likely (guaranteed) to be out-of-date from a recent update to that same object on the master connection.

For example:

c = Contact.find_by login: 'my-email@sample.com'  # replica connection
c.update address: new_address   # update the address
c.save   # master connection query cache cleared, replica connection unchanged!
...
c = Contact.find_by login: 'my-email@sample.com' # out-of-date replica connection cache retrieved
c.address       # -- almost guaranteed to be incorrect!! 
aks commented 7 years ago

I have a proposal for addressing this cache-coherency problem: create a full proxy connection object that is returned to and used by FreshConnection to AR Base methods, and through which the decision to redirect SQL queries to either the master or replica is deferred to the last possible moment.

In other words, the ActiveRecord::Base.connection returns a FreshProxyConnection, which is subclassed from ConnectionAdapters::AbstractAdapter.

The FreshProxyConnection maintains two pools of connections: @master_pool and @replica_pool, and defers the choice of connection until the last possible moment -- just before executing the SQL, via execute.

This allows the QueryCache module to be effective at the ActiveRecord::Base level, unifying the caches for both master and replica connections, since the query cache instance is now attached to the FreshProxyConnection instead of either the original master connections or the replica connections.

By fully proxying the connection, and putting the master/replica switching at the lowest possible level, I believe that less method prepends/intercepts are needed, and only the lowest level methods need to be prepended/intercepted by FreshConnection.

Does this make sense?

tsukasaoishi commented 7 years ago

Thank you! I'll think way to resolve this problem. Please wait a while.

aks commented 7 years ago

Please see PR#19 which provides a fix with a test to confirm the problem and a test to confirm the solution.

tsukasaoishi commented 7 years ago

I fixed a problem of query cache by simple way. #20 I think that there is more better way it. I'm thinking other way to solve this problem.