From a8028bdca895fc2cbfaf876297a9a03a91e91721 Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 19:26:07 -0800 Subject: [PATCH 1/6] Allow mysql_master_slave as the yml adapter Various Rails rake tasks make assumptions on the adapter absed on whether or not it starts with 'mysql' --- .../connection_adapters/mysql_master_slave_adapter.rb | 1 + lib/master_slave_adapter.rb | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 lib/active_record/connection_adapters/mysql_master_slave_adapter.rb diff --git a/lib/active_record/connection_adapters/mysql_master_slave_adapter.rb b/lib/active_record/connection_adapters/mysql_master_slave_adapter.rb new file mode 100644 index 0000000..120513d --- /dev/null +++ b/lib/active_record/connection_adapters/mysql_master_slave_adapter.rb @@ -0,0 +1 @@ +require 'master_slave_adapter' diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index 895cdbf..319b997 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -42,6 +42,10 @@ def master_slave_connection(config) ConnectionAdapters::MasterSlaveAdapter.new(config, logger) end + def mysql_master_slave_connection(config) + master_slave_connection(config) + end + private def massage(config) From 9cd85f86d11b7449067ca758afed56877eb2c58a Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 19:29:27 -0800 Subject: [PATCH 2/6] Allows rake db:create and db:migrate db:create passes in nil for the database connection and db:migrate uses the idnexes method --- lib/master_slave_adapter.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index 319b997..089fd13 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -55,6 +55,7 @@ def massage(config) reject { |k,_| skip.include?(k) }. merge(:adapter => config.fetch(:connection_adapter)) ([config.fetch(:master)] + config.fetch(:slaves, [])).map do |cfg| + cfg[:database] = defaults[:database] if defaults.has_key?(:database) cfg.symbolize_keys!.reverse_merge!(defaults) end config @@ -273,6 +274,7 @@ def execute(*args) :to => :master_connection }]) # no clear interface contract: delegate :tables, # commented in SchemaStatements + :indexes, # migrations :truncate_table, # monkeypatching database_cleaner gem :primary_key, # is Base#primary_key meant to be the contract? :to => :master_connection From edd0b3340992fcccb3a947c8e3ad5a40c1a34b5b Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 19:32:34 -0800 Subject: [PATCH 3/6] Allow adapter to work on databases that do not have a real cluster For example, this will allow local development with a readonly user against the same database. --- lib/master_slave_adapter.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index 089fd13..de16224 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -344,17 +344,27 @@ def current_clock=(clock) def master_clock conn = master_connection + out = nil if status = conn.uncached { conn.select_one("SHOW MASTER STATUS") } - Clock.new(status['File'], status['Position']) + out = Clock.new(status['File'], status['Position']) end + if Rails.env.production? + out ||= Clock.infinity + else + out ||= Clock.zero + end + out end def slave_clock(conn) + out = nil if status = conn.uncached { conn.select_one("SHOW SLAVE STATUS") } - Clock.new(status['Relay_Master_Log_File'], status['Exec_Master_Log_Pos']).tap do |c| + out = Clock.new(status['Relay_Master_Log_File'], status['Exec_Master_Log_Pos']).tap do |c| set_last_seen_slave_clock(conn, c) end end + out ||= Clock.zero + out end def slave_consistent?(conn, clock) From c6a2b3100d3049025f746a6e3076e2facbda0253 Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 19:40:28 -0800 Subject: [PATCH 4/6] Allows initial connection to be to master --- lib/master_slave_adapter.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index de16224..1893202 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -130,7 +130,11 @@ def initialize(config, logger) @disable_connection_test = config.delete(:disable_connection_test) == 'true' - self.current_connection = slave_connection! + if config.delete(:initial_connection) == 'master' + self.current_connection = master_connection + else + self.current_connection = slave_connection! + end end # MASTER SLAVE ADAPTER INTERFACE ======================================== From bc442e757e49d642ef4bcc83972ba9efd6e89a19 Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 19:42:42 -0800 Subject: [PATCH 5/6] Adds some fault tolerance to the slave connections - Actually leaving off slaves in the yml supported - A slave will be dropped if unable to connect - Adapter will fall back on the master connection in that case Possible TODO: Refresh the list and try to reconnect every so often, maybe at exponential intervals. --- lib/master_slave_adapter.rb | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index 1893202..e4381ea 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -126,7 +126,17 @@ def initialize(config, logger) @connections = {} @connections[:master] = connect(config.fetch(:master), :master) - @connections[:slaves] = config.fetch(:slaves).map { |cfg| connect(cfg, :slave) } + + @connections[:slaves] = [] + if config[:slaves] + config.fetch(:slaves).each do |cfg| + begin + @connections[:slaves] << connect(cfg, :slave) + rescue StandardError => e + Rails.logger.error "Slave can not connect ::: #{cfg.inspect}" + end + end + end @disable_connection_test = config.delete(:disable_connection_test) == 'true' @@ -223,7 +233,16 @@ def active? end def reconnect! - self.connections.each { |c| c.reconnect! } + @connections[:slaves].delete_if do |slave| + begin + slave.reconnect! + false + rescue StandardError => e + Rails.logger.error "Slave can not reconnect! ::: #{slave}" + true + end + end + @connections[:master].reconnect! end def disconnect! @@ -323,7 +342,7 @@ def master_connection # Returns a random slave connection # Note: the method is not referentially transparent, hence the bang def slave_connection! - @connections[:slaves].sample + @connections[:slaves].sample || master_connection end def connections @@ -331,7 +350,7 @@ def connections end def current_connection - connection_stack.first + connection_stack.first || master_connection end def current_connection=(conn) From 46a794ae071ed0374a241dfa61aab8d9778bf668 Mon Sep 17 00:00:00 2001 From: Brian Leonard Date: Sat, 21 Jan 2012 20:41:09 -0800 Subject: [PATCH 6/6] Use master connection for obj.reload --- lib/master_slave_adapter.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/master_slave_adapter.rb b/lib/master_slave_adapter.rb index e4381ea..60613b5 100644 --- a/lib/master_slave_adapter.rb +++ b/lib/master_slave_adapter.rb @@ -78,6 +78,21 @@ def load_adapter(adapter_name) end end end + + module MasterSlaveBehavior + def self.included(base) + base.alias_method_chain(:reload, :master_slave) + end + + # Force reload to use the master connection since it's probably being called for a reason. + def reload_with_master_slave(*args) + self.class.with_master do + reload_without_master_slave(*args) + end + end + end + + include(MasterSlaveBehavior) unless include?(MasterSlaveBehavior) end module ConnectionAdapters