Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Staubo committed May 18, 2011
0 parents commit 9af101f
Show file tree
Hide file tree
Showing 13 changed files with 454 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
/pkg
4 changes: 4 additions & 0 deletions Gemfile
@@ -0,0 +1,4 @@
source 'http://rubygems.org/'

gem 'activesupport', '>= 2.2'
gem 'activerecord', '>= 2.2'
20 changes: 20 additions & 0 deletions LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2011 [name of plugin creator]

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.
125 changes: 125 additions & 0 deletions README.markdown
@@ -0,0 +1,125 @@
Multidb
=======

A simple, no-nonsense ActiveRecord extension which allows the application to switch
between multiple database connections, such as in a master/slave environment. For example:

Multidb.use(:slave) do
@posts = Post.all
end

The extension was developed in order to support PostgreSQL 9.0's new hot standby
support in a production environment.

Randomized balancing of multiple connections within a group is supported. In the
future, some kind of automatic balancing of read/write queries might be implemented.

Testet with Rails 2.3.11. No guarantees about Rails 3.


Comparison to other ActiveRecord extensions
===========================================

Unlike other, more full-featured extensions such as Octopus and Seamless Database Pool,
Multidb strives to be:

* Implemented using a minimal amount of
monkeypatching magic. The only part of ActiveRecord that is overriden is
`ActiveRecord::Base#connection`.

* Non-invasive. Very small amounts of configuration and changes to the client
application are required.

* Orthogonal. Unlike Octopus, for example, connections follow context:

Multidb.use(:master) do
@post = Post.find(1)
Multidb.use(:slave) do
@post.authors # This will use the slave
end
end

* Low-overhead. Since `connection` is called on every single
database operation, it needs to be fast. Which it is: Multidb's implementation of
`connection` incurs only a single hash lookup in `Thread.current`.

However, Multidb also has fewer features. At the moment it will _not_ automatically
split reads and writes between database backends.


Getting started
===============

In Rails 2.x applications without a `Gemfile`, add this to `environment.rb`:

config.gem 'ar-multidb'

In Bundler-based on Rails apps, add this to your `Gemfile`:

gem 'ar-multidb', :require => 'multidb'

You may also install it as a plugin:

script/plugin install git://github.com/alexstaubo/multidb.git

All that is needed is to set up your `database.yml` file:

production:
adapter: postgresql
database: myapp_production
username: ohoh
password: mymy
host: db1
multidb:
databases:
slave:
host: db-slave

Each database entry may be a hash or an array. So this also works:

production:
adapter: postgresql
database: myapp_production
username: ohoh
password: mymy
host: db1
multidb:
databases:
slave:
- host: db-slave1
- host: db-slave2
The database hashes follow the same format as the top-level adapter configuration. In
other words, each database connection may override the adapter, database name, username
and so on.

To use the connection, modify your code by wrapping database access logic in blocks:

Multidb.use(:slave) do
@posts = Post.all
end

To wrap entire controller requests, for example:

class PostsController < ApplicationController
around_filter :run_using_slave

def run_using_slave(&block)
Multidb.use(:slave, &block)
end
end

You can also set the current connection for the remainder of the thread's execution:

Multidb.use(:slave)
# Do work
Multidb.use(:master)

Note that the symbol `:default` will (unless you override it) refer to the default
top-level ActiveRecord configuration.


Legal
=====

Copyright (c) 2011 Alexander Staubo. Released under the MIT license. See the file LICENSE.
38 changes: 38 additions & 0 deletions Rakefile
@@ -0,0 +1,38 @@
# encoding: utf-8

require 'rubygems'
require 'rake'
require 'rake/rdoctask'

begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = 'ar-multidb'
gem.summary = gem.description = %Q{Multidb is an ActiveRecord extension for switching between multiple database connections, such as master/slave setups.}
gem.email = "alex@bengler.no"
gem.homepage = "http://github.com/alexstaubo/multidb"
gem.authors = ["Alexander Staubo"]
gem.has_rdoc = true
gem.require_paths = ["lib"]
gem.files = FileList[%W(
README.markdown
VERSION
LICENSE*
lib/**/*
)]
gem.add_dependency 'activesupport', '>= 2.2'
gem.add_dependency 'activerecord', '>= 2.2'
end
Jeweler::GemcutterTasks.new
rescue LoadError
$stderr << "Warning: Gem-building tasks are not included as Jeweler (or a dependency) not available. Install it with: `gem install jeweler`.\n"
end

Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""

rdoc.rdoc_dir = 'rdoc'
rdoc.title = "ruby-hdfs #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
1 change: 1 addition & 0 deletions VERSION
@@ -0,0 +1 @@
0.1.0
54 changes: 54 additions & 0 deletions ar-multidb.gemspec
@@ -0,0 +1,54 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = %q{ar-multidb}
s.version = "0.1.0"

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Alexander Staubo"]
s.date = %q{2011-05-18}
s.description = %q{Multidb is an ActiveRecord extension for switching between multiple database connections, such as master/slave setups.}
s.email = %q{alex@bengler.no}
s.extra_rdoc_files = [
"LICENSE",
"README.markdown"
]
s.files = [
"LICENSE",
"README.markdown",
"VERSION",
"lib/multidb.rb",
"lib/multidb/balancer.rb",
"lib/multidb/configuration.rb",
"lib/multidb/model_extensions.rb"
]
s.homepage = %q{http://github.com/alexstaubo/multidb}
s.require_paths = ["lib"]
s.rubygems_version = %q{1.5.0}
s.summary = %q{Multidb is an ActiveRecord extension for switching between multiple database connections, such as master/slave setups.}

if s.respond_to? :specification_version then
s.specification_version = 3

if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<activesupport>, [">= 2.2"])
s.add_runtime_dependency(%q<activerecord>, [">= 2.2"])
s.add_runtime_dependency(%q<activesupport>, [">= 2.2"])
s.add_runtime_dependency(%q<activerecord>, [">= 2.2"])
else
s.add_dependency(%q<activesupport>, [">= 2.2"])
s.add_dependency(%q<activerecord>, [">= 2.2"])
s.add_dependency(%q<activesupport>, [">= 2.2"])
s.add_dependency(%q<activerecord>, [">= 2.2"])
end
else
s.add_dependency(%q<activesupport>, [">= 2.2"])
s.add_dependency(%q<activerecord>, [">= 2.2"])
s.add_dependency(%q<activesupport>, [">= 2.2"])
s.add_dependency(%q<activerecord>, [">= 2.2"])
end
end

2 changes: 2 additions & 0 deletions init.rb
@@ -0,0 +1,2 @@
# Init file for running as Rails plugin.
require 'multidb'
25 changes: 25 additions & 0 deletions lib/multidb.rb
@@ -0,0 +1,25 @@
require 'multidb/configuration'
require 'multidb/model_extensions'
require 'multidb/balancer'

module Multidb
class << self

def install!
configure!
if @configuration and @configuration.raw_configuration[:databases].any?
ActiveRecord::Base.class_eval do
include Multidb::ModelExtensions
end
@balancer = Balancer.new(@configuration)
end
end

attr_reader :balancer

delegate :use, :get, :to => :balancer

end
end

Multidb.install!
81 changes: 81 additions & 0 deletions lib/multidb/balancer.rb
@@ -0,0 +1,81 @@
module Multidb

class Candidate
def initialize(config)
adapter = config[:adapter]
begin
require "active_record/connection_adapters/#{adapter}_adapter"
rescue LoadError
raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
end
@connection_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(
ActiveRecord::Base::ConnectionSpecification.new(config, "#{adapter}_connection"))
end

def connection
@connection_pool.connection
end
end

class Balancer

def initialize(configuration)
@candidates = {}.with_indifferent_access
@configuration = configuration
@configuration.raw_configuration[:databases].each_pair do |name, config|
configs = config.is_a?(Array) ? config : [config]
configs.each do |config|
candidate = Candidate.new(@configuration.default_adapter.merge(config))
@candidates[name] ||= []
@candidates[name].push(candidate)
end
end
unless @candidates.include?(:default)
@candidates[:default] = [Candidate.new(@configuration.default_adapter)]
end
end

def get(name, &block)
candidates = @candidates[name] || []
raise ArgumentError, "No such database connection '#{name}'" if candidates.blank?
candidate = candidates.sample
block_given? ? yield(candidate) : candidate
end

def use(name, &block)
result = nil
get(name) do |candidate|
connection = candidate.connection
if block_given?
previous_connection, Thread.current[:multidb_connection] =
Thread.current[:multidb_connection], connection
begin
result = yield
ensure
Thread.current[:multidb_connection] = previous_connection
end
result
else
result = Thread.current[:multidb_connection] = connection
end
end
result
end

def current_connection
Thread.current[:multidb_connection] ||= ActiveRecord::Base.connection_pool.connection
end

class << self
def use(name, &block)
Multidb.balancer.use(name, &block)
end

def current_connection
Multidb.balancer.current_connection
end
end

end

end
25 changes: 25 additions & 0 deletions lib/multidb/configuration.rb
@@ -0,0 +1,25 @@
module Multidb

class << self

def configure!
activerecord_config = ActiveRecord::Base.connection_pool.connection.instance_variable_get(:@config).dup.with_indifferent_access
default_adapter, configuration_hash = activerecord_config, activerecord_config.delete(:multidb)
@configuration = Configuration.new(default_adapter, configuration_hash)
end

attr_reader :configuration

end

class Configuration
def initialize(default_adapter, configuration_hash)
@default_adapter = default_adapter
@raw_configuration = configuration_hash
end

attr_reader :default_adapter
attr_reader :raw_configuration
end

end

0 comments on commit 9af101f

Please sign in to comment.