DataMapper Master/Slave Adapter (for MySQL/PostgreSQL replication etc)
This DataMapper adapter provides a thin layer infront of at least two real DataMaper adapters, splitting reads and writes between a 'master' and a 'slave'. It has been tested to work with both PostgreSQL and MySQL but it should work with anything that uses the master/slave ideology.
The adapter comes in two parts:
- The MasterSlaveAdapter, which knows of only two 'real' adapters
- A ReaderPoolAdapter, which knows of any number of 'real' adapters to use as readers. You can set the ReaderPoolAdapter as the reader for the MasterSlaveAdapter.
Installation
Via rubygems:
gem install dm-master-slave-adapter
Usage
The adapter is configured, at a basic level in the following way:
DataMapper.setup(:default, {
:adapter => :master_slave,
:master => {
:adapter => :mysql,
:host => "master.db.site.com",
:username => "root",
:password => ""
},
:slave => {
:adapter => :mysql,
:host => "slave.db.site.com",
:username => "root",
:password => ""
}
})
Here we create a repository named :default, which uses MySQL adapters for the master and the slave.
In YAML, this looks like this:
default:
adapter: master_slave
master:
adapter: mysql
host: "master.db.site.com"
username: root
password:
slave:
adapter: mysql
host: "slave.db.site.com"
username: root
password:
Both the master and the slave are named :default, but you cannot access them directly with DataMapper.repository( ... ); you can only access the MasterSlaveAdapter.
It is possible to access both the master and the slave using accessors on the MasterSlaveAdapter, however:
DataMapper.repository(:default).adapter.master
DataMapper.repository(:default).adapter.slave
Bind to master on first write
It is important to note one particular behaviour with this adapter. By design, after the first write operation has occurred, all subsquent queries, including reads, will be sent directly to the master. This is almost always the desirable behaviour, since you will undoubtedly experience race conditions due to reader-lag if not.
You can force the binding to the master at any time, using:
DataMapper.repository(:default).adapter.bind_to_master
This is a state changing method and will remain in effect until you reset the binding with:
DataMapper.repository(:default).adapter.reset_binding
In a web application, you'll typically want to reset the binding to master at the end of each request, to ensure subsquent requests are not permanently bound to the master. A Rack middleware is provided to do this automatically. The easiest way to use this in a Rails application, is to mount it inside your ApplicationController:
class ApplicationController < ActionController::Base
use DataMapper::MasterSlaveAdapter::Middleware::WriteUnbinding, :default
end
You can use the middleware anywhere a Rack middleware can be used, however, but it must be executed after DataMapper has been initialized.
Note that accessing the master directly, (again, by design) will not cause all subsquent queries to be sent to the master in the same way implicit querying does. This is useful when logic is isolated to a specific part of your application and you know other parts of the application need not query the same storage backend. I personally do this for session storage.
Lastly, you can force all queries to be implicitlty sent to the master in the context of a block, simply by passing a block to #bind_to_master, like so:
DataMapper.repository(:default).adapter.bind_to_master do
...
end
Once the block has completed, the adapter will be restored to its original state, regardless of what writes may have occurred. Note that if the adapter was already implictly bound to master before the block was invoked, this will have no effect.
Suggestion for Rails apps
We, at Flippa.com, have found it useful to preemptively bind to the master on all
#create
, #update
and #destroy
actions inside of our controllers. We follow fairly
strict resourceful routing, so this makes a great deal of sense for us. The benefit to
doing this is that if your reader pool ever lags behind (such as after execessive write
activity) you don't risk performing write operations on the basis of out-of-date data
selected from a reader.
class ApplicationController < ActionController::Base
before_filter :bind_to_master, :only => [:create, :update, :destroy]
def bind_to_master
DataMapper.repository(:default).adapter.bind_to_master
end
end
Using the ReaderPoolAdapter
The ReaderPoolAdapter simply allows you to use more than one adapter as the 'slave' when configuring the MasterSlaveAdapter. For every read query it receives, it picks a random adapter from its pool.
It is configured like so:
DataMapper.setup(:default, {
:adapter => :master_slave,
:master => {
...
},
:slave => {
:adapter => :reader_pool,
:pool => [
{
:adapter => :mysql
:host => "slave1.db.site.com",
:username => "root",
:password => ""
},
{
:adapter => :mysql
:host => "slave2.db.site.com",
:username => "root",
:password => ""
}
]
}
})
In the above setup, we simply have two MySQL hosts specified as available slaves to the MasterSlaveAdapter. In YAML, that looks like this:
default:
adapter: master_slave
master:
...
slave:
adapter: reader_pool
pool:
- adapter: mysql
host: "slave1.db.site.com"
username: root
password:
- adapter: mysql
host: "slave2.db.site.com"
username: root
password:
Reporting Issues
Please file any issues in the issue tracker at GitHub:
Potential TODOs
- Raise an exception for #create, #update and #delete on the reader
- Enhanced logging to include the details of the adapter being used
Copyright and Licensing
Copyright © 2011 Chris Corbyn
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.