seamless-database-pool-1.0.17/0000755000175000017500000000000012673014540017415 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/MIT-LICENSE0000644000175000017500000000204012673014540021045 0ustar balasankarcbalasankarcCopyright (c) 2008 Brian Durand 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. seamless-database-pool-1.0.17/Rakefile0000644000175000017500000000517012673014540021065 0ustar balasankarcbalasankarcrequire 'rubygems' require 'rake' require 'yaml' desc 'Default: run unit tests.' task :default => :test begin require 'rspec' require 'rspec/core/rake_task' desc 'Run the unit tests' RSpec::Core::RakeTask.new(:test) namespace :test do desc "Run all tests including for all database adapters" task :all do save_val = ENV['TEST_ADAPTERS'] begin ENV['TEST_ADAPTERS'] = YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.join(' ') Rake::Task["test"].execute ensure ENV['TEST_ADAPTERS'] = save_val end end desc "Test all database adapters defined in database.yml or just the one specified in TEST_ADAPTERS" task :adapters do save_val = ENV['TEST_ADAPTERS'] begin ENV['TEST_ADAPTERS'] ||= YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.join(' ') Rake::Task["test:adapters:specified"].execute ensure ENV['TEST_ADAPTERS'] = save_val end end namespace :adapters do desc "Internal task to run database adapter tests" RSpec::Core::RakeTask.new(:specified) do |t| t.pattern = FileList.new('spec/connection_adapters_spec.rb') end YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.each do |adapter_name| desc "Test the #{adapter_name} database adapter" task adapter_name do save_val = ENV['TEST_ADAPTERS'] begin ENV['TEST_ADAPTERS'] = adapter_name Rake::Task["test:adapters:specified"].execute ensure ENV['TEST_ADAPTERS'] = save_val end end end end end rescue LoadError task :test do STDERR.puts "You must have rspec >= 2.0 to run the tests" end end begin require 'jeweler' Jeweler::Tasks.new do |gem| gem.name = "seamless_database_pool" gem.summary = "Add support for master/slave database clusters in ActiveRecord to improve performance." gem.email = "bbdurand@gmail.com" gem.homepage = "http://github.com/bdurand/seamless_database_pool" gem.authors = ["Brian Durand"] gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "MIT-LICENSE"].to_a gem.has_rdoc = true gem.extra_rdoc_files = ["README.rdoc", "MIT-LICENSE"] gem.add_dependency('activerecord', '>= 3.0.20') gem.add_development_dependency('rspec', '>= 2.0') gem.add_development_dependency('jeweler') gem.add_development_dependency('sqlite3') gem.add_development_dependency('mysql') gem.add_development_dependency('pg') end Jeweler::GemcutterTasks.new rescue LoadError endseamless-database-pool-1.0.17/seamless_database_pool.gemspec0000644000175000017500000000503312673014540025454 0ustar balasankarcbalasankarc######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: seamless_database_pool 1.0.17 ruby lib Gem::Specification.new do |s| s.name = "seamless_database_pool" s.version = "1.0.17" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.require_paths = ["lib"] s.authors = ["Brian Durand"] s.date = "2014-08-14" s.email = "bbdurand@gmail.com" s.extra_rdoc_files = ["MIT-LICENSE", "README.rdoc"] s.files = ["MIT-LICENSE", "README.rdoc", "Rakefile", "lib/active_record/connection_adapters/seamless_database_pool_adapter.rb", "lib/seamless_database_pool.rb", "lib/seamless_database_pool/arel_compiler.rb", "lib/seamless_database_pool/connection_statistics.rb", "lib/seamless_database_pool/controller_filter.rb", "lib/seamless_database_pool/railtie.rb", "spec/connection_adapters_spec.rb", "spec/connection_statistics_spec.rb", "spec/controller_filter_spec.rb", "spec/database.yml", "spec/seamless_database_pool_adapter_spec.rb", "spec/seamless_database_pool_spec.rb", "spec/spec_helper.rb", "spec/test_adapter/active_record/connection_adapters/read_only_adapter.rb", "spec/test_model.rb"] s.homepage = "http://github.com/bdurand/seamless_database_pool" s.rubygems_version = "2.5.1" s.summary = "Add support for master/slave database clusters in ActiveRecord to improve performance." if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, [">= 3.0.20"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 2.0"]) s.add_development_dependency(%q, [">= 0"]) else s.add_dependency(%q, [">= 3.0.20"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 2.0"]) s.add_dependency(%q, [">= 0"]) end else s.add_dependency(%q, [">= 3.0.20"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 2.0"]) s.add_dependency(%q, [">= 0"]) end end seamless-database-pool-1.0.17/spec/0000755000175000017500000000000012673014540020347 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/spec/test_model.rb0000644000175000017500000000346412673014540023042 0ustar balasankarcbalasankarcmodule SeamlessDatabasePool class TestModel < ActiveRecord::Base self.abstract_class = true class << self def database_configs adapters = ENV['TEST_ADAPTERS'].blank? ? [] : ENV['TEST_ADAPTERS'].split(/\s+/) configs = {} YAML.load_file(File.expand_path("../database.yml", __FILE__)).each do |adapter_name, adapter_config| configs[adapter_name] = adapter_config if adapters.include?(adapter_name.downcase) end configs end def use_database_connection(db_name) establish_connection(database_configs[db_name.to_s]) end def db_model(db_name) model_class_name = "#{db_name.classify}TestModel" unless const_defined?(model_class_name) klass = Class.new(self) const_set(model_class_name, klass) klass = const_get(model_class_name) klass.use_database_connection(db_name) end const_get(model_class_name) end def create_tables connection.create_table(table_name) do |t| t.column :name, :string t.column :value, :integer end unless table_exists? connection.clear_cache! if connection.respond_to?(:clear_cache!) undefine_attribute_methods if respond_to?(:undefine_attribute_methods) end def drop_tables connection.drop_table(table_name) connection.clear_cache! if connection.respond_to?(:clear_cache!) undefine_attribute_methods if respond_to?(:undefine_attribute_methods) end def cleanup_database! connection.disconnect! sqlite3_config = database_configs['sqlite3'] if sqlite3_config && File.exist?(sqlite3_config['database']) File.delete(sqlite3_config['database']) end end end end end seamless-database-pool-1.0.17/spec/controller_filter_spec.rb0000644000175000017500000001026212673014540025437 0ustar balasankarcbalasankarcrequire 'spec_helper' describe "SeamlessDatabasePool::ControllerFilter" do module SeamlessDatabasePool class TestApplicationController attr_reader :session def initialize(session) @session = session end def process(action, *args) send action end def redirect_to (options = {}, response_status = {}) options end def base_action ::SeamlessDatabasePool.read_only_connection_type end end class TestBaseController < TestApplicationController include ::SeamlessDatabasePool::ControllerFilter use_database_pool :read => :persistent def read ::SeamlessDatabasePool.read_only_connection_type end def other ::SeamlessDatabasePool.read_only_connection_type end end class TestOtherController < TestBaseController use_database_pool :all => :random, [:edit, :save, :redirect_master_action] => :master def edit ::SeamlessDatabasePool.read_only_connection_type end def save ::SeamlessDatabasePool.read_only_connection_type end def redirect_master_action redirect_to(:action => :read) end def redirect_read_action redirect_to(:action => :read) end end class TestRails2ApplicationController < TestApplicationController attr_reader :action_name def process(action, *args) @action_name = action perform_action end private def perform_action send action_name end end class TestRails2BaseController < TestRails2ApplicationController include ::SeamlessDatabasePool::ControllerFilter use_database_pool :read => :persistent def read ::SeamlessDatabasePool.read_only_connection_type end end end let(:session){Hash.new} let(:controller){SeamlessDatabasePool::TestOtherController.new(session)} it "should work with nothing set" do controller = SeamlessDatabasePool::TestApplicationController.new(session) controller.process('base_action').should == :master end it "should allow setting a connection type for a single action" do controller = SeamlessDatabasePool::TestBaseController.new(session) controller.process('read').should == :persistent end it "should allow setting a connection type for actions" do controller.process('edit').should == :master controller.process('save').should == :master end it "should allow setting a connection type for all actions" do controller.process('other').should == :random end it "should inherit the superclass' options" do controller.process('read').should == :persistent end it "should be able to force using the master connection on the next request" do # First request controller.process('read').should == :persistent controller.use_master_db_connection_on_next_request # Second request controller.process('read').should == :master # Third request controller.process('read').should == :persistent end it "should not break trying to force the master connection if sessions are not enabled" do controller.process('read').should == :persistent controller.use_master_db_connection_on_next_request # Second request session.clear controller.process('read').should == :persistent end it "should force the master connection on the next request for a redirect in master connection block" do controller = SeamlessDatabasePool::TestOtherController.new(session) controller.process('redirect_master_action').should == {:action => :read} controller.process('read').should == :master end it "should not force the master connection on the next request for a redirect not in master connection block" do controller.process('redirect_read_action').should == {:action => :read} controller.process('read').should == :persistent end it "should work with a Rails 2 controller" do controller = SeamlessDatabasePool::TestRails2BaseController.new(session) controller.process('read').should == :persistent end end seamless-database-pool-1.0.17/spec/database.yml0000644000175000017500000000165212673014540022642 0ustar balasankarcbalasankarc# This file contains the databases that the test suite will be run against if you run rake:test:adapters or set the # environment variable ADAPTER (use commas to test multiple adapters). If you want, you can add your own adapter below # and it will be added to the test suite. sqlite3: adapter: seamless_database_pool database: test.sqlite3 master: adapter: sqlite3 pool_weight: 0 read_pool: - adapter: read_only real_adapter: sqlite3 postgresql: adapter: seamless_database_pool database: seamless_database_pool_test username: postgres password: postgres master: adapter: postgresql pool_weight: 0 read_pool: - adapter: read_only real_adapter: postgresql mysql: adapter: seamless_database_pool database: seamless_database_pool_test username: root password: master: adapter: mysql2 pool_weight: 0 read_pool: - adapter: read_only real_adapter: mysql2 seamless-database-pool-1.0.17/spec/seamless_database_pool_adapter_spec.rb0000755000175000017500000004222312673014540030105 0ustar balasankarcbalasankarcrequire 'spec_helper' module SeamlessDatabasePool class MockConnection < ActiveRecord::ConnectionAdapters::AbstractAdapter def initialize (name) @name = name end def inspect "#{@name} connection" end def reconnect! sleep(0.1) end def active? true end def begin_db_transaction end def commit_db_transaction end end class MockMasterConnection < MockConnection def insert (sql, name = nil); end def update (sql, name = nil); end def execute (sql, name = nil); end def columns (table_name, name = nil); end end end describe "SeamlessDatabasePoolAdapter ActiveRecord::Base extension" do it "should establish the connections in the pool merging global options into the connection options" do options = { :adapter => 'seamless_database_pool', :pool_adapter => 'reader', :username => 'user', :master => { 'adapter' => 'writer', 'host' => 'master_host' }, :read_pool => [ {'host' => 'read_host_1'}, {'host' => 'read_host_2', 'pool_weight' => '2'}, {'host' => 'read_host_3', 'pool_weight' => '0'} ] } pool_connection = double(:connection) master_connection = SeamlessDatabasePool::MockConnection.new("master") read_connection_1 = SeamlessDatabasePool::MockConnection.new("read_1") read_connection_2 = SeamlessDatabasePool::MockConnection.new("read_2") logger = ActiveRecord::Base.logger weights = {master_connection => 1, read_connection_1 => 1, read_connection_2 => 2} ActiveRecord::Base.should_receive(:writer_connection).with('adapter' => 'writer', 'host' => 'master_host', 'username' => 'user', 'pool_weight' => 1).and_return(master_connection) ActiveRecord::Base.should_receive(:reader_connection).with('adapter' => 'reader', 'host' => 'read_host_1', 'username' => 'user', 'pool_weight' => 1).and_return(read_connection_1) ActiveRecord::Base.should_receive(:reader_connection).with('adapter' => 'reader', 'host' => 'read_host_2', 'username' => 'user', 'pool_weight' => 2).and_return(read_connection_2) klass = double(:class) ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.should_receive(:adapter_class).with(master_connection).and_return(klass) klass.should_receive(:new).with(nil, logger, master_connection, [read_connection_1, read_connection_2], weights).and_return(pool_connection) ActiveRecord::Base.should_receive(:establish_adapter).with('writer') ActiveRecord::Base.should_receive(:establish_adapter).with('reader').twice ActiveRecord::Base.seamless_database_pool_connection(options).should == pool_connection end it "should raise an error if the adapter would be recursive" do lambda{ActiveRecord::Base.seamless_database_pool_connection('seamless_database_pool').should_raise(ActiveRecord::AdapterNotFound)} end end describe "SeamlessDatabasePoolAdapter" do let(:master_connection){ SeamlessDatabasePool::MockMasterConnection.new("master") } let(:read_connection_1){ SeamlessDatabasePool::MockConnection.new("read_1") } let(:read_connection_2){ SeamlessDatabasePool::MockConnection.new("read_2") } let(:pool_connection) do weights = {master_connection => 1, read_connection_1 => 1, read_connection_2 => 2} connection_class = ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection) connection_class.new(nil, nil, master_connection, [read_connection_1, read_connection_2], weights) end it "should be able to be converted to a string" do pool_connection.to_s.should =~ /\A#\z/ pool_connection.inspect.should == pool_connection.to_s end context "selecting a connection from the pool" do it "should initialize the connection pool" do pool_connection.master_connection.should == master_connection pool_connection.read_connections.should == [read_connection_1, read_connection_2] pool_connection.all_connections.should == [master_connection, read_connection_1, read_connection_2] pool_connection.pool_weight(master_connection).should == 1 pool_connection.pool_weight(read_connection_1).should == 1 pool_connection.pool_weight(read_connection_2).should == 2 end it "should return the current read connection" do SeamlessDatabasePool.should_receive(:read_only_connection).with(pool_connection).and_return(:current) pool_connection.current_read_connection.should == :current end it "should select a random read connection" do mock_connection = double(:connection) mock_connection.stub(:active? => true) pool_connection.should_receive(:available_read_connections).and_return([:fake1, :fake2, mock_connection]) pool_connection.should_receive(:rand).with(3).and_return(2) pool_connection.random_read_connection.should == mock_connection end it "should select the master connection if the read pool is empty" do pool_connection.should_receive(:available_read_connections).and_return([]) pool_connection.random_read_connection.should == master_connection end it "should use the master connection in a block" do connection_class = ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection) connection = connection_class.new(nil, double(:logger), master_connection, [read_connection_1], {read_connection_1 => 1}) connection.random_read_connection.should == read_connection_1 connection.use_master_connection do connection.random_read_connection.should == master_connection end connection.random_read_connection.should == read_connection_1 end it "should use the master connection inside a transaction" do connection_class = ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection) connection = connection_class.new(nil, double(:logger), master_connection, [read_connection_1], {read_connection_1 => 1}) master_connection.should_receive(:begin_db_transaction) master_connection.should_receive(:commit_db_transaction) master_connection.should_receive(:select).with('Transaction SQL', nil) read_connection_1.should_receive(:select).with('SQL 1', nil) read_connection_1.should_receive(:select).with('SQL 2', nil) SeamlessDatabasePool.use_persistent_read_connection do connection.send(:select, 'SQL 1', nil) connection.transaction do connection.send(:select, 'Transaction SQL', nil) end connection.send(:select, 'SQL 2', nil) end end end context "read connection methods" do it "should proxy select methods to a read connection" do pool_connection.should_receive(:current_read_connection).and_return(read_connection_1) read_connection_1.should_receive(:select).with('SQL').and_return(:retval) pool_connection.send(:select, 'SQL').should == :retval end it "should proxy execute methods to a read connection" do pool_connection.should_receive(:current_read_connection).and_return(read_connection_1) read_connection_1.should_receive(:execute).with('SQL').and_return(:retval) pool_connection.execute('SQL').should == :retval end it "should proxy select_rows methods to a read connection" do pool_connection.should_receive(:current_read_connection).and_return(read_connection_1) read_connection_1.should_receive(:select_rows).with('SQL').and_return(:retval) pool_connection.select_rows('SQL').should == :retval end end context "master connection methods" do it "should proxy insert method to the master connection" do master_connection.should_receive(:insert).with('SQL').and_return(:retval) pool_connection.insert('SQL').should == :retval end it "should proxy update method to the master connection" do master_connection.should_receive(:update).with('SQL').and_return(:retval) pool_connection.update('SQL').should == :retval end it "should proxy columns method to the master connection" do master_connection.should_receive(:columns).with(:table).and_return(:retval) pool_connection.columns(:table).should == :retval end end context "fork to all connections" do it "should fork active? to all connections and return true if all are up" do master_connection.should_receive(:active?).and_return(true) read_connection_1.should_receive(:active?).and_return(true) read_connection_2.should_receive(:active?).and_return(true) pool_connection.active?.should == true end it "should fork active? to all connections and return false if one is down" do master_connection.should_receive(:active?).and_return(true) read_connection_1.should_receive(:active?).and_return(true) read_connection_2.should_receive(:active?).and_return(false) pool_connection.active?.should == false end it "should fork verify! to all connections" do master_connection.should_receive(:verify!).with(5) read_connection_1.should_receive(:verify!).with(5) read_connection_2.should_receive(:verify!).with(5) pool_connection.verify!(5) end it "should fork disconnect! to all connections" do master_connection.should_receive(:disconnect!) read_connection_1.should_receive(:disconnect!) read_connection_2.should_receive(:disconnect!) pool_connection.disconnect! end it "should fork reconnect! to all connections" do master_connection.should_receive(:reconnect!) read_connection_1.should_receive(:reconnect!) read_connection_2.should_receive(:reconnect!) pool_connection.reconnect! end it "should fork reset_runtime to all connections" do master_connection.should_receive(:reset_runtime).and_return(1) read_connection_1.should_receive(:reset_runtime).and_return(2) read_connection_2.should_receive(:reset_runtime).and_return(3) pool_connection.reset_runtime.should == 6 end end context "reconnection" do it "should proxy requests to a connection" do args = [:arg1, :arg2] block = Proc.new{} master_connection.should_receive(:select_value).with(*args, &block) master_connection.should_not_receive(:active?) master_connection.should_not_receive(:reconnect!) pool_connection.send(:proxy_connection_method, master_connection, :select_value, :master, *args, &block) end it "should try to reconnect dead connections when they become available again" do master_connection.stub(:select).and_raise("SQL ERROR") master_connection.should_receive(:active?).and_return(false, false, true) master_connection.should_receive(:reconnect!) now = Time.now lambda{pool_connection.select_value("SQL")}.should raise_error("SQL ERROR") Time.stub(:now => now + 31) lambda{pool_connection.select_value("SQL")}.should raise_error("SQL ERROR") end it "should not try to reconnect live connections" do args = [:arg1, :arg2] block = Proc.new{} master_connection.should_receive(:select).with(*args, &block).twice.and_raise("SQL ERROR") master_connection.should_receive(:active?).and_return(true) master_connection.should_not_receive(:reconnect!) lambda{pool_connection.send(:proxy_connection_method, master_connection, :select, :read, *args, &block)}.should raise_error("SQL ERROR") end it "should not try to reconnect a connection during a retry" do args = [:arg1, :arg2] block = Proc.new{} master_connection.should_receive(:select).with(*args, &block).and_raise("SQL ERROR") master_connection.should_not_receive(:active?) master_connection.should_not_receive(:reconnect!) lambda{pool_connection.send(:proxy_connection_method, master_connection, :select, :retry, *args, &block)}.should raise_error("SQL ERROR") end it "should try to execute a read statement again after a connection error" do connection_error = ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter::DatabaseConnectionError.new pool_connection.should_receive(:current_read_connection).and_return(read_connection_1) read_connection_1.should_receive(:select).with('SQL').and_raise(connection_error) read_connection_1.should_receive(:active?).and_return(true) pool_connection.should_not_receive(:suppress_read_connection) SeamlessDatabasePool.should_not_receive(:set_persistent_read_connection) read_connection_1.should_receive(:select).with('SQL').and_return(:results) pool_connection.send(:select, 'SQL').should == :results end it "should not try to execute a read statement again after a connection error if the master connection must be used" do master_connection.should_receive(:select).with('SQL').and_raise("Fail") pool_connection.use_master_connection do lambda{pool_connection.send(:select, 'SQL')}.should raise_error("Fail") end end it "should not try to execute a read statement again after a non-connection error" do pool_connection.should_receive(:current_read_connection).and_return(read_connection_1) pool_connection.should_receive(:proxy_connection_method).with(read_connection_1, :select, :read, 'SQL').and_raise("SQL Error") lambda{pool_connection.send(:select, 'SQL')}.should raise_error("SQL Error") end it "should use a different connection on a retry if the original connection could not be reconnected" do pool_connection.should_receive(:current_read_connection).and_return(read_connection_1, read_connection_2) read_connection_1.should_receive(:select).with('SQL').and_raise("Fail") read_connection_1.should_receive(:active?).and_return(false) pool_connection.should_receive(:suppress_read_connection).with(read_connection_1, 30) SeamlessDatabasePool.should_receive(:set_persistent_read_connection).with(pool_connection, read_connection_2) read_connection_2.should_receive(:select).with('SQL').and_return(:results) pool_connection.send(:select, 'SQL').should == :results end it "should keep track of read connections that can't be reconnected for a set period" do pool_connection.available_read_connections.should include(read_connection_1) pool_connection.suppress_read_connection(read_connection_1, 30) pool_connection.available_read_connections.should_not include(read_connection_1) end it "should return dead connections to the pool after the timeout has expired" do pool_connection.available_read_connections.should include(read_connection_1) pool_connection.suppress_read_connection(read_connection_1, 0.2) pool_connection.available_read_connections.should_not include(read_connection_1) sleep(0.3) pool_connection.available_read_connections.should include(read_connection_1) end it "should not return a connection to the pool until it can be reconnected" do pool_connection.available_read_connections.should include(read_connection_1) pool_connection.suppress_read_connection(read_connection_1, 0.2) pool_connection.available_read_connections.should_not include(read_connection_1) sleep(0.3) read_connection_1.should_receive(:reconnect!) read_connection_1.should_receive(:active?).and_return(false) pool_connection.available_read_connections.should_not include(read_connection_1) end it "should try all connections again if none of them can be reconnected" do stack = pool_connection.instance_variable_get(:@available_read_connections) available = pool_connection.available_read_connections available.should include(read_connection_1) available.should include(read_connection_2) available.should include(master_connection) stack.size.should == 1 pool_connection.suppress_read_connection(read_connection_1, 30) available = pool_connection.available_read_connections available.should_not include(read_connection_1) available.should include(read_connection_2) available.should include(master_connection) stack.size.should == 2 pool_connection.suppress_read_connection(master_connection, 30) available = pool_connection.available_read_connections available.should_not include(read_connection_1) available.should include(read_connection_2) available.should_not include(master_connection) stack.size.should == 3 pool_connection.suppress_read_connection(read_connection_2, 30) available = pool_connection.available_read_connections available.should include(read_connection_1) available.should include(read_connection_2) available.should include(master_connection) stack.size.should == 1 end it "should not try to suppress a read connection that wasn't available in the read pool" do stack = pool_connection.instance_variable_get(:@available_read_connections) stack.size.should == 1 pool_connection.suppress_read_connection(read_connection_1, 30) stack.size.should == 2 pool_connection.suppress_read_connection(read_connection_1, 30) stack.size.should == 2 end end end seamless-database-pool-1.0.17/spec/spec_helper.rb0000644000175000017500000000111212673014540023160 0ustar balasankarcbalasankarcrequire 'rubygems' active_record_version = ENV["ACTIVE_RECORD_VERSION"] || [">= 2.2.2"] active_record_version = [active_record_version] unless active_record_version.is_a?(Array) gem 'activerecord', *active_record_version require 'active_record' puts "Testing Against ActiveRecord #{ActiveRecord::VERSION::STRING}" if defined?(ActiveRecord::VERSION) require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'seamless_database_pool')) require File.expand_path(File.join(File.dirname(__FILE__), 'test_model')) $LOAD_PATH << File.expand_path("../test_adapter", __FILE__) seamless-database-pool-1.0.17/spec/connection_adapters_spec.rb0000644000175000017500000002335112673014540025734 0ustar balasankarcbalasankarcrequire 'spec_helper' require 'active_record/connection_adapters/read_only_adapter' describe "Test connection adapters" do if SeamlessDatabasePool::TestModel.database_configs.empty? puts "No adapters specified for testing. Specify the adapters with TEST_ADAPTERS variable" else SeamlessDatabasePool::TestModel.database_configs.keys.each do |adapter| context adapter do let(:model){ SeamlessDatabasePool::TestModel.db_model(adapter) } let(:connection){ model.connection } let(:read_connection){ connection.available_read_connections.first } let(:master_connection){ connection.master_connection } before(:all) do if ActiveRecord::VERSION::MAJOR < 3 || (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0) ActiveRecord::Base.configurations = {'adapter' => "sqlite3", 'database' => ":memory:"} else ActiveRecord::Base.configurations = {"test" => {'adapter' => "sqlite3", 'database' => ":memory:"}} end ActiveRecord::Base.establish_connection('adapter' => "sqlite3", 'database' => ":memory:") ActiveRecord::Base.connection SeamlessDatabasePool::TestModel.db_model(adapter).create_tables end after(:all) do SeamlessDatabasePool::TestModel.db_model(adapter).drop_tables SeamlessDatabasePool::TestModel.db_model(adapter).cleanup_database! end before(:each) do model.create!(:name => 'test', :value => 1) SeamlessDatabasePool.use_persistent_read_connection end after(:each) do model.delete_all SeamlessDatabasePool.use_master_connection end it "should force the master connection on reload" do record = model.first SeamlessDatabasePool.should_not_receive(:current_read_connection) record.reload end it "should quote table names properly" do connection.quote_table_name("foo").should == master_connection.quote_table_name("foo") end it "should quote column names properly" do connection.quote_column_name("foo").should == master_connection.quote_column_name("foo") end it "should quote string properly" do connection.quote_string("foo").should == master_connection.quote_string("foo") end it "should quote booleans properly" do connection.quoted_true.should == master_connection.quoted_true connection.quoted_false.should == master_connection.quoted_false end it "should quote dates properly" do date = Date.today time = Time.now connection.quoted_date(date).should == master_connection.quoted_date(date) connection.quoted_date(time).should == master_connection.quoted_date(time) end it "should query for records" do record = model.find_by_name("test") record.name.should == "test" end it "should work with query caching" do record_id = model.first.id model.cache do found = model.find(record_id) found.value.should == 1 connection.master_connection.update("UPDATE #{model.table_name} SET value = 0 WHERE id = #{record_id}") model.find(record_id).value.should == 1 end end it "should work bust the query cache on update" do record_id = model.first.id model.cache do found = model.find(record_id) found.name = "new value" found.save! model.find(record_id).name.should == "new value" end end context "read connection" do let(:sample_sql){"SELECT #{connection.quote_column_name('name')} FROM #{connection.quote_table_name(model.table_name)}"} it "should not include the master connection in the read pool for these tests" do connection.available_read_connections.should_not include(master_connection) connection.current_read_connection.should_not == master_connection end it "should send select to the read connection" do results = connection.send(:select, sample_sql) results.to_a.should == [{"name" => "test"}] results.to_a.should == master_connection.send(:select, sample_sql).to_a results.should be_read_only end it "should send select_rows to the read connection" do results = connection.select_rows(sample_sql) results.should == [["test"]] results.should == master_connection.select_rows(sample_sql) results.should be_read_only end it "should send execute to the read connection" do results = connection.execute(sample_sql) results.should be_read_only end it "should send columns to the read connection" do results = connection.columns(model.table_name) columns = results.collect{|c| c.name}.sort.should columns.should == ["id", "name", "value"] columns.should == master_connection.columns(model.table_name).collect{|c| c.name}.sort results.should be_read_only end it "should send tables to the read connection" do results = connection.tables results.should == [model.table_name] results.should == master_connection.tables results.should be_read_only end it "should reconnect dead connections in the read pool" do read_connection.disconnect! read_connection.should_not be_active results = connection.select_all(sample_sql) results.should be_read_only read_connection.should be_active end end context "methods not overridden" do let(:sample_sql){"SELECT #{connection.quote_column_name('name')} FROM #{connection.quote_table_name(model.table_name)}"} it "should use select_all" do results = connection.select_all(sample_sql) results.to_a.should == [{"name" => "test"}].to_a results.to_a.should == master_connection.select_all(sample_sql).to_a end it "should use select_one" do results = connection.select_one(sample_sql) results.should == {"name" => "test"} results.should == master_connection.select_one(sample_sql) end it "should use select_values" do results = connection.select_values(sample_sql) results.should == ["test"] results.should == master_connection.select_values(sample_sql) end it "should use select_value" do results = connection.select_value(sample_sql) results.should == "test" results.should == master_connection.select_value(sample_sql) end end context "master connection" do let(:insert_sql){ "INSERT INTO #{connection.quote_table_name(model.table_name)} (#{connection.quote_column_name('name')}) VALUES ('new')" } let(:update_sql){ "UPDATE #{connection.quote_table_name(model.table_name)} SET #{connection.quote_column_name('value')} = 2" } let(:delete_sql){ "DELETE FROM #{connection.quote_table_name(model.table_name)}" } it "should blow up if a master connection method is sent to the read only connection" do lambda{read_connection.update(update_sql)}.should raise_error(NotImplementedError) lambda{read_connection.update(insert_sql)}.should raise_error(NotImplementedError) lambda{read_connection.update(delete_sql)}.should raise_error(NotImplementedError) lambda{read_connection.transaction{}}.should raise_error(NotImplementedError) lambda{read_connection.create_table(:test)}.should raise_error(NotImplementedError) end it "should send update to the master connection" do connection.update(update_sql) model.first.value.should == 2 end it "should send insert to the master connection" do connection.update(insert_sql) model.find_by_name("new").should_not == nil end it "should send delete to the master connection" do connection.update(delete_sql) model.first.should == nil end it "should send transaction to the master connection" do connection.transaction do connection.update(update_sql) end model.first.value.should == 2 end it "should send schema altering statements to the master connection" do SeamlessDatabasePool.use_master_connection do begin connection.create_table(:foo) do |t| t.string :name end connection.add_index(:foo, :name) ensure connection.remove_index(:foo, :name) connection.drop_table(:foo) end end end it "should properly dump the schema" do with_driver = StringIO.new ActiveRecord::SchemaDumper.dump(connection, with_driver) without_driver = StringIO.new ActiveRecord::SchemaDumper.dump(master_connection, without_driver) with_driver.string.should == without_driver.string end end end end end end seamless-database-pool-1.0.17/spec/seamless_database_pool_spec.rb0000644000175000017500000001726112673014540026406 0ustar balasankarcbalasankarcrequire 'spec_helper' describe "SeamlessDatabasePool" do before(:each) do SeamlessDatabasePool.clear_read_only_connection end after(:each) do SeamlessDatabasePool.clear_read_only_connection end it "should use the master connection by default" do connection = double(:connection, :master_connection => :master_db_connection, :using_master_connection? => false) SeamlessDatabasePool.read_only_connection_type.should == :master SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end it "should be able to set using persistent read connections" do connection = double(:connection) connection.should_receive(:random_read_connection).once.and_return(:read_db_connection) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.use_persistent_read_connection SeamlessDatabasePool.read_only_connection_type.should == :persistent SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection end it "should be able to set using random read connections" do connection = double(:connection) connection.should_receive(:random_read_connection).and_return(:read_db_connection_1, :read_db_connection_2) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.use_random_read_connection SeamlessDatabasePool.read_only_connection_type.should == :random SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection_1 SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection_2 end it "should use the master connection if the connection is forcing it" do connection = double(:connection, :master_connection => :master_db_connection) connection.should_receive(:using_master_connection?).and_return(true) SeamlessDatabasePool.use_persistent_read_connection SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end it "should be able to set using the master connection" do connection = double(:connection, :master_connection => :master_db_connection) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.use_master_connection SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end it "should be able to use persistent read connections within a block" do connection = double(:connection, :master_connection => :master_db_connection) connection.should_receive(:random_read_connection).once.and_return(:read_db_connection) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection SeamlessDatabasePool.use_persistent_read_connection do SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection :test_val end.should == :test_val SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end it "should be able to use random read connections within a block" do connection = double(:connection, :master_connection => :master_db_connection) connection.should_receive(:random_read_connection).and_return(:read_db_connection_1, :read_db_connection_2) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection SeamlessDatabasePool.use_random_read_connection do SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection_1 SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection_2 :test_val end.should == :test_val SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end it "should be able to use the master connection within a block" do connection = double(:connection, :master_connection => :master_db_connection) connection.should_receive(:random_read_connection).once.and_return(:read_db_connection) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.use_persistent_read_connection SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.use_master_connection do SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection :test_val end.should == :test_val SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.clear_read_only_connection end it "should be able to use connection blocks within connection blocks" do connection = double(:connection, :master_connection => :master_db_connection) connection.stub(:random_read_connection => :read_db_connection) connection.stub(:using_master_connection? => false) SeamlessDatabasePool.use_persistent_read_connection do SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.use_master_connection do SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection SeamlessDatabasePool.use_random_read_connection do SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection end SeamlessDatabasePool.read_only_connection(connection).should == :master_db_connection end end SeamlessDatabasePool.clear_read_only_connection end it "should be able to change the persistent connection" do connection = double(:connection) connection.stub(:random_read_connection => :read_db_connection, :using_master_connection? => false) SeamlessDatabasePool.use_persistent_read_connection SeamlessDatabasePool.read_only_connection_type.should == :persistent SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.set_persistent_read_connection(connection, :another_db_connection) SeamlessDatabasePool.read_only_connection(connection).should == :another_db_connection SeamlessDatabasePool.use_random_read_connection SeamlessDatabasePool.read_only_connection_type.should == :random SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection SeamlessDatabasePool.set_persistent_read_connection(connection, :another_db_connection) SeamlessDatabasePool.read_only_connection(connection).should == :read_db_connection end it "should be able to specify a default read connection type instead of :master" do SeamlessDatabasePool.read_only_connection_type.should == :master SeamlessDatabasePool.read_only_connection_type(nil).should == nil end it "should pull out the master configurations for compatibility with rake db:* tasks" do config = { 'development' => { 'adapter' => 'seamless_database_pool', 'pool_adapter' => 'mysql2', 'database' => 'development', 'username' => 'root', 'master' => { 'host' => 'localhost', 'pool_weight' => 2 }, 'read_pool' => { 'host' => 'slavehost', 'pool_weight' => 5 } }, 'test' => { 'adapter' => 'mysql2', 'database' => 'test' } } SeamlessDatabasePool.master_database_configuration(config).should == { 'development' => { 'adapter' => 'mysql2', 'database' => 'development', 'username' => 'root', 'host' => 'localhost' }, 'test' => { 'adapter' => 'mysql2', 'database' => 'test' } } end end seamless-database-pool-1.0.17/spec/test_adapter/0000755000175000017500000000000012673014540023026 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/spec/test_adapter/active_record/0000755000175000017500000000000012673014540025637 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/spec/test_adapter/active_record/connection_adapters/0000755000175000017500000000000012673014540031661 5ustar balasankarcbalasankarc././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootseamless-database-pool-1.0.17/spec/test_adapter/active_record/connection_adapters/read_only_adapter.rbseamless-database-pool-1.0.17/spec/test_adapter/active_record/connection_adapters/read_only_adapter.0000644000175000017500000000332212673014540035336 0ustar balasankarcbalasankarcmodule ActiveRecord class Base def self.read_only_connection(config) real_adapter = config.delete("real_adapter") connection = send("#{real_adapter}_connection", config.merge("adapter" => real_adapter)) ConnectionAdapters::ReadOnlyAdapter.new(connection) end end module ConnectionAdapters class ReadOnlyAdapter < AbstractAdapter %w(select select_rows execute tables columns).each do |read_method| class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{read_method} (*args, &block) raise "Not Connected" unless @connected result = @connection.send(:#{read_method}, *args, &block) def result.read_only? true end result end EOS %w(update insert delete reload create_table drop_table add_index remove_index transaction).each do |write_method| class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{write_method} (*args, &block) raise NotImplementedError.new("Master method '#{write_method}' called on read only connection") end EOS end end def initialize(connection) @connection = connection @connected = true super end def test_select @connection.select_all('SELECT "test_models".* FROM "test_models" LIMIT 1') end def visitor @connection.visitor end def visitor=(v) @connection.visitor = v end def reconnect! @connected = true end def disconnect! @connected = false end def active? @connected end end end end seamless-database-pool-1.0.17/spec/connection_statistics_spec.rb0000644000175000017500000000525012673014540026321 0ustar balasankarcbalasankarcrequire 'spec_helper' describe SeamlessDatabasePool::ConnectionStatistics do module SeamlessDatabasePool class ConnectionStatisticsTester def insert (sql, name = nil) "INSERT #{sql}/#{name}" end def update (sql, name = nil) execute(sql) "UPDATE #{sql}/#{name}" end def execute (sql, name = nil) "EXECUTE #{sql}/#{name}" end protected def select (sql, name = nil, binds = []) "SELECT #{sql}/#{name}" end include ::SeamlessDatabasePool::ConnectionStatistics end end it "should increment statistics on update" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.update('SQL', 'name').should == "UPDATE SQL/name" connection.connection_statistics.should == {:update => 1} connection.update('SQL 2').should == "UPDATE SQL 2/" connection.connection_statistics.should == {:update => 2} end it "should increment statistics on insert" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.insert('SQL', 'name').should == "INSERT SQL/name" connection.connection_statistics.should == {:insert => 1} connection.insert('SQL 2').should == "INSERT SQL 2/" connection.connection_statistics.should == {:insert => 2} end it "should increment statistics on execute" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.execute('SQL', 'name').should == "EXECUTE SQL/name" connection.connection_statistics.should == {:execute => 1} connection.execute('SQL 2').should == "EXECUTE SQL 2/" connection.connection_statistics.should == {:execute => 2} end it "should increment statistics on select" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.send(:select, 'SQL', 'name').should == "SELECT SQL/name" connection.connection_statistics.should == {:select => 1} connection.send(:select, 'SQL 2').should == "SELECT SQL 2/" connection.connection_statistics.should == {:select => 2} end it "should increment counts only once within a block" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.should_receive(:execute).with('SQL') connection.update('SQL') connection.connection_statistics.should == {:update => 1} end it "should be able to clear the statistics" do connection = SeamlessDatabasePool::ConnectionStatisticsTester.new connection.update('SQL') connection.connection_statistics.should == {:update => 1} connection.reset_connection_statistics connection.connection_statistics.should == {} end end seamless-database-pool-1.0.17/lib/0000755000175000017500000000000012673014540020163 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/lib/seamless_database_pool.rb0000644000175000017500000001536712673014540025215 0ustar balasankarcbalasankarcrequire File.join(File.dirname(__FILE__), 'seamless_database_pool', 'connection_statistics.rb') require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'controller_filter.rb') require File.join(File.dirname(__FILE__), 'active_record', 'connection_adapters', 'seamless_database_pool_adapter.rb') require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'railtie.rb') if defined?(Rails::Railtie) $LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__)) # This module allows setting the read pool connection type. Generally you will use one of # # - use_random_read_connection # - use_persistent_read_connection # - use_master_connection # # Each of these methods can take an optional block. If they are called with a block, they # will set the read connection type only within the block. Otherwise they will set the default # read connection type. If none is ever called, the read connection type will be :master. module SeamlessDatabasePool # Adapter name to class name map. This exists because there isn't an obvious way to translate things like # sqlite3 to SQLite3. The adapters that ship with ActiveRecord are defined here. If you use # an adapter that doesn't translate directly to camel case, then add the mapping here in an initializer. ADAPTER_TO_CLASS_NAME_MAP = {"sqlite" => "SQLite", "sqlite3" => "SQLite3", "postgresql" => "PostgreSQL"} READ_CONNECTION_METHODS = [:master, :persistent, :random] class << self # Call this method to use a random connection from the read pool for every select statement. # This method is good if your replication is very fast. Otherwise there is a chance you could # get inconsistent results from one request to the next. This can result in mysterious failures # if your code selects a value in one statement and then uses in another statement. You can wind # up trying to use a value from one server that hasn't been replicated to another one yet. # This method is best if you have few processes which generate a lot of queries and you have # fast replication. def use_random_read_connection if block_given? set_read_only_connection_type(:random){yield} else Thread.current[:read_only_connection] = :random end end # Call this method to pick a random connection from the read pool and use it for all subsequent # select statements. This provides consistency from one select statement to the next. This # method should always be called with a block otherwise you can end up with an imbalanced read # pool. This method is best if you have lots of processes which have a relatively few select # statements or a slow replication mechanism. Generally this is the best method to use for web # applications. def use_persistent_read_connection if block_given? set_read_only_connection_type(:persistent){yield} else Thread.current[:read_only_connection] = {} end end # Call this method to use the master connection for all subsequent select statements. This # method is most useful when you are doing lots of updates since it guarantees consistency # if you do a select immediately after an update or insert. # # The master connection will also be used for selects inside any transaction blocks. It will # also be used if you pass :readonly => false to any ActiveRecord.find method. def use_master_connection if block_given? set_read_only_connection_type(:master){yield} else Thread.current[:read_only_connection] = :master end end # Set the read only connection type to either :master, :random, or :persistent. def set_read_only_connection_type(connection_type) saved_connection = Thread.current[:read_only_connection] retval = nil begin connection_type = {} if connection_type == :persistent Thread.current[:read_only_connection] = connection_type retval = yield if block_given? ensure Thread.current[:read_only_connection] = saved_connection end return retval end # Get the read only connection type currently in use. Will be one of :master, :random, or :persistent. def read_only_connection_type(default = :master) connection_type = Thread.current[:read_only_connection] || default connection_type = :persistent if connection_type.kind_of?(Hash) return connection_type end # Get a read only connection from a connection pool. def read_only_connection(pool_connection) return pool_connection.master_connection if pool_connection.using_master_connection? connection_type = Thread.current[:read_only_connection] if connection_type.kind_of?(Hash) connection = connection_type[pool_connection] unless connection connection = pool_connection.random_read_connection connection_type[pool_connection] = connection end return connection elsif connection_type == :random return pool_connection.random_read_connection else return pool_connection.master_connection end end # This method is provided as a way to change the persistent connection when it fails and a new one is substituted. def set_persistent_read_connection(pool_connection, read_connection) connection_type = Thread.current[:read_only_connection] connection_type[pool_connection] = read_connection if connection_type.kind_of?(Hash) end def clear_read_only_connection Thread.current[:read_only_connection] = nil end # Get the connection adapter class for an adapter name. The class will be loaded from # ActiveRecord::ConnectionAdapters::NameAdapter where Name is the camelized version of the name. # If the adapter class does not fit this pattern (i.e. sqlite3 => SQLite3Adapter), then add # the mapping to the +ADAPTER_TO_CLASS_NAME_MAP+ Hash. def adapter_class_for(name) name = name.to_s class_name = ADAPTER_TO_CLASS_NAME_MAP[name] || name.camelize "ActiveRecord::ConnectionAdapters::#{class_name}Adapter".constantize end # Pull out the master configuration for compatibility with such things as the Rails' rake db:* # tasks which only support known adapters. def master_database_configuration(database_configs) configs = {} database_configs.each do |key, values| if values['adapter'] == 'seamless_database_pool' values['adapter'] = values.delete('pool_adapter') values = values.merge(values['master']) if values['master'].is_a?(Hash) values.delete('pool_weight') values.delete('master') values.delete('read_pool') end configs[key] = values end configs end end end seamless-database-pool-1.0.17/lib/seamless_database_pool/0000755000175000017500000000000012673014540024654 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/lib/seamless_database_pool/arel_compiler.rb0000644000175000017500000000206212673014540030016 0ustar balasankarcbalasankarcmodule Arel module SqlCompiler # Hook into arel to use the compiler used by the master connection. class Seamless_Database_PoolCompiler < GenericCompiler def self.new(relation) @compiler_classes ||= {} master_adapter = relation.engine.connection.master_connection.adapter_name compiler_class = @compiler_classes[master_adapter] unless compiler_class begin require "arel/engines/sql/compilers/#{master_adapter.downcase}_compiler" rescue LoadError begin # try to load an externally defined compiler, in case this adapter has defined the compiler on its own. require "#{master_adapter.downcase}/arel_compiler" rescue LoadError raise "#{master_adapter} is not supported by Arel." end end compiler_class = Arel::SqlCompiler.const_get("#{master_adapter}Compiler") @compiler_classes[master_adapter] = compiler_class end compiler_class.new(relation) end end end end seamless-database-pool-1.0.17/lib/seamless_database_pool/controller_filter.rb0000644000175000017500000001221112673014540030726 0ustar balasankarcbalasankarcmodule SeamlessDatabasePool # This module provides a simple method of declaring which read pool connection type should # be used for various ActionController actions. To use it, you must first mix it into # you controller and then call use_database_pool to configure the connection types. Generally # you should just do this in ApplicationController and call use_database_pool in your controllers # when you need different connection types. # # Example: # # ApplicationController < ActionController::Base # include SeamlessDatabasePool::ControllerFilter # use_database_pool :all => :persistent, [:save, :delete] => :master # ... module ControllerFilter def self.included(base) unless base.respond_to?(:use_database_pool) base.extend(ClassMethods) base.class_eval do if base.method_defined?(:perform_action) || base.private_method_defined?(:perform_action) alias_method_chain :perform_action, :seamless_database_pool else alias_method_chain :process, :seamless_database_pool end alias_method_chain :redirect_to, :seamless_database_pool end end end module ClassMethods def seamless_database_pool_options return @seamless_database_pool_options if @seamless_database_pool_options @seamless_database_pool_options = superclass.seamless_database_pool_options.dup if superclass.respond_to?(:seamless_database_pool_options) @seamless_database_pool_options ||= {} end # Call this method to set up the connection types that will be used for your actions. # The configuration is given as a hash where the key is the action name and the value is # the connection type (:master, :persistent, or :random). You can specify :all as the action # to define a default connection type. You can also specify the action names in an array # to easily map multiple actions to one connection type. # # The configuration is inherited from parent controller classes, so if you have default # behavior, you should simply specify it in ApplicationController to have it available # globally. def use_database_pool(options) remapped_options = seamless_database_pool_options options.each_pair do |actions, connection_method| unless SeamlessDatabasePool::READ_CONNECTION_METHODS.include?(connection_method) raise "Invalid read pool method: #{connection_method}; should be one of #{SeamlessDatabasePool::READ_CONNECTION_METHODS.inspect}" end actions = [actions] unless actions.kind_of?(Array) actions.each do |action| remapped_options[action.to_sym] = connection_method end end @seamless_database_pool_options = remapped_options end end # Force the master connection to be used on the next request. This is very useful for the Post-Redirect pattern # where you post a request to your save action and then redirect the user back to the edit action. By calling # this method, you won't have to worry if the replication engine is slower than the redirect. Normally you # won't need to call this method yourself as it is automatically called when you perform a redirect from within # a master connection block. It is made available just in case you have special needs that don't quite fit # into this module's default logic. def use_master_db_connection_on_next_request session[:next_request_db_connection] = :master if session end def seamless_database_pool_options self.class.seamless_database_pool_options end # Rails 3.x hook for setting the read connection for the request. def process_with_seamless_database_pool(action, *args) set_read_only_connection_for_block(action) do process_without_seamless_database_pool(action, *args) end end def redirect_to_with_seamless_database_pool(options = {}, response_status = {}) if SeamlessDatabasePool.read_only_connection_type(nil) == :master use_master_db_connection_on_next_request end redirect_to_without_seamless_database_pool(options, response_status) end private # Rails 2.x hook for setting the read connection for the request. def perform_action_with_seamless_database_pool(*args) set_read_only_connection_for_block(action_name) do perform_action_without_seamless_database_pool(*args) end end # Set the read only connection for a block. Used to set the connection for a controller action. def set_read_only_connection_for_block(action) read_pool_method = nil if session read_pool_method = session[:next_request_db_connection] session.delete(:next_request_db_connection) if session[:next_request_db_connection] end read_pool_method ||= seamless_database_pool_options[action.to_sym] || seamless_database_pool_options[:all] if read_pool_method SeamlessDatabasePool.set_read_only_connection_type(read_pool_method) do yield end else yield end end end end seamless-database-pool-1.0.17/lib/seamless_database_pool/railtie.rb0000644000175000017500000000071212673014540026632 0ustar balasankarcbalasankarcmodule SeamlessDatabasePool class Railtie < ::Rails::Railtie rake_tasks do namespace :db do task :load_config do # Override seamless_database_pool configuration so db:* rake tasks work as expected. original_config = Rails.application.config.database_configuration ActiveRecord::Base.configurations = SeamlessDatabasePool.master_database_configuration(original_config) end end end end end seamless-database-pool-1.0.17/lib/seamless_database_pool/connection_statistics.rb0000644000175000017500000000355112673014540031616 0ustar balasankarcbalasankarcmodule SeamlessDatabasePool # This module is included for testing. Mix it into each of your database pool connections # and it will keep track of how often each connection calls update, insert, execute, # or select. module ConnectionStatistics def self.included(base) base.alias_method_chain(:update, :connection_statistics) base.alias_method_chain(:insert, :connection_statistics) base.alias_method_chain(:execute, :connection_statistics) base.alias_method_chain(:select, :connection_statistics) end # Get the connection statistics def connection_statistics @connection_statistics ||= {} end def reset_connection_statistics @connection_statistics = {} end def update_with_connection_statistics(sql, name = nil) increment_connection_statistic(:update) do update_without_connection_statistics(sql, name) end end def insert_with_connection_statistics(sql, name = nil) increment_connection_statistic(:insert) do insert_without_connection_statistics(sql, name) end end def execute_with_connection_statistics(sql, name = nil) increment_connection_statistic(:execute) do execute_without_connection_statistics(sql, name) end end protected def select_with_connection_statistics(sql, name = nil, *args) increment_connection_statistic(:select) do select_without_connection_statistics(sql, name, *args) end end def increment_connection_statistic(method) if @counting_pool_statistics yield else begin @counting_pool_statistics = true stat = connection_statistics[method] || 0 @connection_statistics[method] = stat + 1 yield ensure @counting_pool_statistics = false end end end end end seamless-database-pool-1.0.17/lib/active_record/0000755000175000017500000000000012673014540022774 5ustar balasankarcbalasankarcseamless-database-pool-1.0.17/lib/active_record/connection_adapters/0000755000175000017500000000000012673014540027016 5ustar balasankarcbalasankarc././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootseamless-database-pool-1.0.17/lib/active_record/connection_adapters/seamless_database_pool_adapter.rbseamless-database-pool-1.0.17/lib/active_record/connection_adapters/seamless_database_pool_adapter.r0000755000175000017500000003416312673014540035404 0ustar balasankarcbalasankarcmodule ActiveRecord class Base class << self def seamless_database_pool_connection(config) pool_weights = {} config = config.with_indifferent_access default_config = {:pool_weight => 1}.merge(config.merge(:adapter => config[:pool_adapter])).with_indifferent_access default_config.delete(:master) default_config.delete(:read_pool) default_config.delete(:pool_adapter) master_config = default_config.merge(config[:master]).with_indifferent_access establish_adapter(master_config[:adapter]) master_connection = send("#{master_config[:adapter]}_connection".to_sym, master_config) pool_weights[master_connection] = master_config[:pool_weight].to_i if master_config[:pool_weight].to_i > 0 read_connections = [] config[:read_pool].each do |read_config| read_config = default_config.merge(read_config).with_indifferent_access read_config[:pool_weight] = read_config[:pool_weight].to_i if read_config[:pool_weight] > 0 begin establish_adapter(read_config[:adapter]) conn = send("#{read_config[:adapter]}_connection".to_sym, read_config) read_connections << conn pool_weights[conn] = read_config[:pool_weight] rescue Exception => e if logger logger.error("Error connecting to read connection #{read_config.inspect}") logger.error(e) end end end end if config[:read_pool] klass = ::ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection) klass.new(nil, logger, master_connection, read_connections, pool_weights) end def establish_adapter(adapter) raise AdapterNotSpecified.new("database configuration does not specify adapter") unless adapter raise AdapterNotFound.new("database pool must specify adapters") if adapter == 'seamless_database_pool' begin require 'rubygems' gem "activerecord-#{adapter}-adapter" require "active_record/connection_adapters/#{adapter}_adapter" rescue LoadError begin require "active_record/connection_adapters/#{adapter}_adapter" rescue LoadError raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})" end end adapter_method = "#{adapter}_connection" if !respond_to?(adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{adapter} adapter" end end end module SeamlessDatabasePoolBehavior def self.included(base) base.alias_method_chain(:reload, :seamless_database_pool) end # Force reload to use the master connection since it's probably being called for a reason. def reload_with_seamless_database_pool(*args) SeamlessDatabasePool.use_master_connection do reload_without_seamless_database_pool(*args) end end end include(SeamlessDatabasePoolBehavior) unless include?(SeamlessDatabasePoolBehavior) end module ConnectionAdapters class SeamlessDatabasePoolAdapter < AbstractAdapter attr_reader :read_connections, :master_connection class << self # Create an anonymous class that extends this one and proxies methods to the pool connections. def adapter_class(master_connection) adapter_class_name = master_connection.adapter_name.classify return const_get(adapter_class_name) if const_defined?(adapter_class_name, false) # Define methods to proxy to the appropriate pool read_only_methods = [:select, :select_rows, :execute, :tables, :columns] clear_cache_methods = [:insert, :update, :delete] # Get a list of all methods redefined by the underlying adapter. These will be # proxied to the master connection. master_methods = [] override_classes = (master_connection.class.ancestors - AbstractAdapter.ancestors) override_classes.each do |connection_class| master_methods.concat(connection_class.public_instance_methods(false)) master_methods.concat(connection_class.protected_instance_methods(false)) end master_methods = master_methods.collect{|m| m.to_sym}.uniq master_methods -= public_instance_methods(false) + protected_instance_methods(false) + private_instance_methods(false) master_methods -= read_only_methods master_methods -= [:select_all, :select_one, :select_value, :select_values] master_methods -= clear_cache_methods klass = Class.new(self) master_methods.each do |method_name| klass.class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{method_name}(*args, &block) use_master_connection do return proxy_connection_method(master_connection, :#{method_name}, :master, *args, &block) end end EOS end clear_cache_methods.each do |method_name| klass.class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{method_name}(*args, &block) clear_query_cache if query_cache_enabled use_master_connection do return proxy_connection_method(master_connection, :#{method_name}, :master, *args, &block) end end EOS end read_only_methods.each do |method_name| klass.class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{method_name}(*args, &block) connection = @use_master ? master_connection : current_read_connection proxy_connection_method(connection, :#{method_name}, :read, *args, &block) end EOS end klass.send :protected, :select const_set(adapter_class_name, klass) return klass end # Set the arel visitor on the connections. def visitor_for(pool) # This is ugly, but then again, so is the code in ActiveRecord for setting the arel # visitor. There is a note in the code indicating the method signatures should be updated. config = pool.spec.config.with_indifferent_access adapter = config[:master][:adapter] || config[:pool_adapter] SeamlessDatabasePool.adapter_class_for(adapter).visitor_for(pool) end end def initialize(connection, logger, master_connection, read_connections, pool_weights) @master_connection = master_connection @read_connections = read_connections.dup.freeze super(connection, logger) @weighted_read_connections = [] pool_weights.each_pair do |conn, weight| weight.times{@weighted_read_connections << conn} end @available_read_connections = [AvailableConnections.new(@weighted_read_connections)] end def adapter_name #:nodoc: 'Seamless_Database_Pool' end # Returns an array of the master connection and the read pool connections def all_connections [@master_connection] + @read_connections end # Get the pool weight of a connection def pool_weight(connection) return @weighted_read_connections.select{|conn| conn == connection}.size end def requires_reloading? false end def transaction(options = {}) use_master_connection do super end end def visitor=(visitor) all_connections.each{|conn| conn.visitor = visitor} end def visitor master_connection.visitor end def active? active = true do_to_connections {|conn| active &= conn.active?} return active end def reconnect! do_to_connections {|conn| conn.reconnect!} end def disconnect! do_to_connections {|conn| conn.disconnect!} end def reset! do_to_connections {|conn| conn.reset!} end def verify!(*ignored) do_to_connections {|conn| conn.verify!(*ignored)} end def reset_runtime total = 0.0 do_to_connections {|conn| total += conn.reset_runtime} total end # Get a random read connection from the pool. If the connection is not active, it will attempt to reconnect # to the database. If that fails, it will be removed from the pool for one minute. def random_read_connection weighted_read_connections = available_read_connections if @use_master || weighted_read_connections.empty? return master_connection else weighted_read_connections[rand(weighted_read_connections.length)] end end # Get the current read connection def current_read_connection return SeamlessDatabasePool.read_only_connection(self) end def using_master_connection? !!@use_master end # Force using the master connection in a block. def use_master_connection save_val = @use_master begin @use_master = true yield if block_given? ensure @use_master = save_val end end def to_s "#<#{self.class.name}:0x#{object_id.to_s(16)} #{all_connections.size} connections>" end def inspect to_s end class DatabaseConnectionError < StandardError end # This simple class puts an expire time on an array of connections. It is used so the a connection # to a down database won't try to reconnect over and over. class AvailableConnections attr_reader :connections, :failed_connection attr_writer :expires def initialize(connections, failed_connection = nil, expires = nil) @connections = connections @failed_connection = failed_connection @expires = expires end def expired? @expires ? @expires <= Time.now : false end def reconnect! failed_connection.reconnect! raise DatabaseConnectionError.new unless failed_connection.active? end end # Get the available weighted connections. When a connection is dead and cannot be reconnected, it will # be temporarily removed from the read pool so we don't keep trying to reconnect to a database that isn't # listening. def available_read_connections available = @available_read_connections.last if available.expired? begin @logger.info("Adding dead database connection back to the pool") if @logger available.reconnect! rescue => e # Couldn't reconnect so try again in a little bit if @logger @logger.warn("Failed to reconnect to database when adding connection back to the pool") @logger.warn(e) end available.expires = 30.seconds.from_now return available.connections end # If reconnect is successful, the connection will have been re-added to @available_read_connections list, # so let's pop this old version of the connection @available_read_connections.pop # Now we'll try again after either expiring our bad connection or re-adding our good one return available_read_connections else return available.connections end end def reset_available_read_connections @available_read_connections.slice!(1, @available_read_connections.length) @available_read_connections.first.connections.each do |connection| unless connection.active? connection.reconnect! rescue nil end end end # Temporarily remove a connection from the read pool. def suppress_read_connection(conn, expire) available = available_read_connections connections = available.reject{|c| c == conn} # This wasn't a read connection so don't suppress it return if connections.length == available.length if connections.empty? @logger.warn("All read connections are marked dead; trying them all again.") if @logger # No connections available so we might as well try them all again reset_available_read_connections else @logger.warn("Removing #{conn.inspect} from the connection pool for #{expire} seconds") if @logger # Available connections will now not include the suppressed connection for a while @available_read_connections.push(AvailableConnections.new(connections, conn, expire.seconds.from_now)) end end private def proxy_connection_method(connection, method, proxy_type, *args, &block) begin connection.send(method, *args, &block) rescue => e # If the statement was a read statement and it wasn't forced against the master connection # try to reconnect if the connection is dead and then re-run the statement. if proxy_type == :read && !using_master_connection? unless connection.active? suppress_read_connection(connection, 30) connection = current_read_connection SeamlessDatabasePool.set_persistent_read_connection(self, connection) end proxy_connection_method(connection, method, :retry, *args, &block) else raise e end end end # Yield a block to each connection in the pool. If the connection is dead, ignore the error # unless it is the master connection def do_to_connections all_connections.each do |conn| begin yield(conn) rescue => e raise e if conn == master_connection end end nil end end end end seamless-database-pool-1.0.17/README.rdoc0000755000175000017500000001472612673014540021240 0ustar balasankarcbalasankarcSeamless Database Pool provides a simple way in which to add support for a master/slave database cluster to ActiveRecord to allow massive scalability and automatic failover. The guiding design principle behind this code is to make it absolutely trivial to add to an existing, complex application. That way when you have a big, nasty application which needs to scale the database you won't have to stop all feature development just to refactor your database connection code. Let's face it, when the database is having scaling problems, you are in for a world of hurt and the faster you can fix the problem the better. This code is available as both a Rails plugin and a gem so it will work with any ActiveRecord application. = Database Clusters In a master/slave cluster you have one master database server which uses replication to feed all changes to one or more slave databases which are set up to only handle reads. Since most applications put most of the load on the server with reads, this setup can scale out an application quite well. You'll need to work with your database of choice to get replication set up. This plugin has an connection adapter which will handle proxying database requests to the right server. = Simple Integration You can convert a standard Rails application (i.e. one that follows the scaffold conventions) to use a database cluster with three simple steps: 1. Set up the database cluster (OK maybe this one isn't simple) 2. Update database.yml settings to point to the servers in the cluster 3. Add this code to ApplicationController: include SeamlessDatabasePool::ControllerFilter use_database_pool :all => :persistent, [:create, :update, :destroy] => :master If needed you can control how the connection pool is utilized by wrapping your code in some simple blocks. = Failover One of the other main advantages of using any sort of cluster is that one node can fail without bringing down your application. This plugin automatically handles failing over dead database connections in the read pool. That is if it tries to use a read connection and it is found to be inactive, the connector will try to reconnect. If that fails, it will try another connection in the read pool. After thirty seconds it will try to reconnect the dead connection again. One limitation on failover is when database servers are down when the pool is being initialized during startup. In this case, the connections cannot be initialized and are not added to the pool. If this happens, you will need to restart your processes once the database servers are back online. = Configuration == The pool configuration The cluster connections are configured in database.yml using the seamless_database_pool adapter. Any properties you configure for the connection will be inherited by all connections in the pool. In this way, you can configure ports, usernames, etc. once instead of for each connection. One exception is that you can set the pool_adapter property which each connection will inherit as the adapter property. Each connection in the pool uses all the same configuration properties as normal for the adapters. == The read pool The read pool is specified with a read_pool property in the pool connection definition in database.yml. This should be specified as an array of hashes where each hash is the configuration for each read connection you'd like to use (see below for an example). As noted above, the configuration for the entire pool will be merged in with the options for each connection. Each connection can be assigned an additional option of pool_weight. This value should be number which indicates the relative weight that the connection should be given in the pool. If no value is specified, it will default to one. Setting the value to zero will keep the connection out of the pool. If possible, you should set the permissions on the database user for the read connections to one that only has select permission. This can be especially useful in development and testing to ensure that the read connection never have writes sent to them. == The master connection The master connection is specified with a master_connection property in the pool connection definition in database.yml (see below for an example). The master connection will be used for all non-select statements against the database (i.e. insert, update, delete, etc.). It will also be used for all statements inside a transaction or any reload commands. By default, the master connection will be included in the read pool. If you would like to dedicate this connection only for write operations, you should set the pool weight to zero. Do not duplicate the master connection in the read pool as this will result in the additional overhead of two connections to the database. == Example configuration development: adapter: seamless_database_pool database: mydb_development username: read_user password: abc123 pool_adapter: mysql2 port: 3306 master: host: master-db.example.com port: 6000 username: master_user password: 567pass read_pool: - host: read-db-1.example.com pool_weight: 2 - host: read-db-2.example.com In this configuration, the master connection will be a mysql connection to master-db.example.com:6000 using the username master_user and the password 567pass. The read pool will use three mysql connections to master-db, read-db-1, and read-db-2. The master connection will use a different port, username, password for the connection. The read connections will use the same values. Further, the connection read-db-1 will get half the traffic as the other two connections, so presumably it's on a more powerful box. You must use compatible database adapters for both the master and the read connections. For example, you cannot use an Oracle server as your master and PostgreSQL servers as you read slaves. = Using the read pool By default, the master connection will be used for everything. This is not terribly useful, so you should really specify a method of using the read pool for the actions that need it. Read connections will only be used for select statements against the database. This is done with static methods on SeamlessDatabasePool. = Controller Filters To ease integration into a Ruby on Rails application, several controller filters are provided to invoke the above connection methods in a block. These are not implemented as standard controller filters so that the connection methods can be in effect for other filters. See SeamlessDatabasePool::ControllerFilter for more details.