From e8a30431297a5a908e5bd2fc4e63e515412efa60 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Thu, 10 Nov 2016 23:44:51 +0800 Subject: [PATCH 1/2] Spawn a single thread that checks for PostgreSQL fallback. --- .../postgresql_fallback_adapter.rb | 126 +++++++----------- lib/discourse.rb | 10 +- .../postgresql_fallback_adapter_spec.rb | 31 ++--- 3 files changed, 64 insertions(+), 103 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index b52a6c12ac0..03932d6cc75 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -6,23 +6,46 @@ class PostgreSQLFallbackHandler include Singleton def initialize - @master = {} - @running = {} - @mutex = {} - @last_check = {} - - setup! + @masters_down = {} + @mutex = Mutex.new end def verify_master - @mutex[namespace].synchronize do - return if running || recently_checked? - @running[namespace] = true - end + synchronize { return if @thread && @thread.alive? } - current_namespace = namespace - Thread.new do - RailsMultisite::ConnectionManagement.with_connection(current_namespace) do + @thread = Thread.new do + while true do + begin + thread = Thread.new { initiate_fallback_to_master } + thread.join + break if synchronize { @masters_down.empty? } + sleep 10 + ensure + thread.kill + end + end + end + end + + def master_down? + synchronize { @masters_down[namespace] } + end + + def master_down=(args) + synchronize { @masters_down[namespace] = args } + end + + def master_up(namespace) + synchronize { @masters_down.delete(namespace) } + end + + def running? + synchronize { @thread.alive? } + end + + def initiate_fallback_to_master + @masters_down.keys.each do |key| + RailsMultisite::ConnectionManagement.with_connection(key) do begin logger.warn "#{log_prefix}: Checking master server..." connection = ActiveRecord::Base.postgresql_connection(config) @@ -32,54 +55,20 @@ class PostgreSQLFallbackHandler ActiveRecord::Base.clear_all_connections! logger.warn "#{log_prefix}: Master server is active. Reconnecting..." - if namespace == RailsMultisite::ConnectionManagement::DEFAULT - ActiveRecord::Base.establish_connection(config) - else - RailsMultisite::ConnectionManagement.establish_connection(db: namespace) - end - + self.master_up(key) Discourse.disable_readonly_mode - self.master = true end rescue => e - if e.message.include?("could not connect to server") - logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'" - else - raise e - end - ensure - @mutex[namespace].synchronize do - @last_check[namespace] = Time.zone.now - @running[namespace] = false - end + byebug + logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'" end end end end - def master - @master[namespace] - end - - def master=(args) - @master[namespace] = args - end - - def running - @running[namespace] - end - + # Use for testing def setup! - RailsMultisite::ConnectionManagement.all_dbs.each do |db| - @master[db] = true - @running[db] = false - @mutex[db] = Mutex.new - @last_check[db] = nil - end - end - - def verify? - !master && !running + @masters_down = {} end private @@ -96,17 +85,13 @@ class PostgreSQLFallbackHandler "#{self.class} [#{namespace}]" end - def recently_checked? - if @last_check[namespace] - Time.zone.now <= (@last_check[namespace] + 5.seconds) - else - false - end - end - def namespace RailsMultisite::ConnectionManagement.current_db end + + def synchronize + @mutex.synchronize { yield } + end end module ActiveRecord @@ -115,7 +100,7 @@ module ActiveRecord fallback_handler = ::PostgreSQLFallbackHandler.instance config = config.symbolize_keys - if fallback_handler.verify? + if fallback_handler.master_down? connection = postgresql_connection(config.dup.merge({ host: config[:replica_host], port: config[:replica_port] })) @@ -126,7 +111,8 @@ module ActiveRecord begin connection = postgresql_connection(config) rescue PG::ConnectionBad => e - fallback_handler.master = false + fallback_handler.master_down = true + fallback_handler.verify_master raise e end end @@ -141,20 +127,4 @@ module ActiveRecord raise "Replica database server is not in recovery mode." if value == 'f' end end - - module ConnectionAdapters - class PostgreSQLAdapter - set_callback :checkout, :before, :switch_back? - - private - - def fallback_handler - @fallback_handler ||= ::PostgreSQLFallbackHandler.instance - end - - def switch_back? - fallback_handler.verify_master if fallback_handler.verify? - end - end - end end diff --git a/lib/discourse.rb b/lib/discourse.rb index 1ac6e1b00d1..f28ad2593cb 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -228,10 +228,12 @@ module Discourse def self.keep_readonly_mode # extend the expiry by 1 minute every 30 seconds - Thread.new do - while readonly_mode? - $redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL) - sleep 30.seconds + unless Rails.env.test? + Thread.new do + while readonly_mode? + $redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL) + sleep 30.seconds + end end end end diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 05e6b68cd9e..a9e61dbea15 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -48,8 +48,9 @@ describe ActiveRecord::ConnectionHandling do end it 'should failover to a replica server' do + current_threads = Thread.list + RailsMultisite::ConnectionManagement.stubs(:all_dbs).returns(['default', multisite_db]) - ::PostgreSQLFallbackHandler.instance.setup! [config, multisite_config].each do |configuration| ActiveRecord::Base.expects(:postgresql_connection).with(configuration).raises(PG::ConnectionBad) @@ -60,7 +61,7 @@ describe ActiveRecord::ConnectionHandling do })).returns(@replica_connection) end - expect(postgresql_fallback_handler.master).to eq(true) + expect(postgresql_fallback_handler.master_down?).to eq(nil) expect { ActiveRecord::Base.postgresql_fallback_connection(config) } .to raise_error(PG::ConnectionBad) @@ -68,10 +69,10 @@ describe ActiveRecord::ConnectionHandling do expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } .to change{ Discourse.readonly_mode? }.from(false).to(true) - expect(postgresql_fallback_handler.master).to eq(false) + expect(postgresql_fallback_handler.master_down?).to eq(true) with_multisite_db(multisite_db) do - expect(postgresql_fallback_handler.master).to eq(true) + expect(postgresql_fallback_handler.master_down?).to eq(nil) expect { ActiveRecord::Base.postgresql_fallback_connection(multisite_config) } .to raise_error(PG::ConnectionBad) @@ -79,30 +80,18 @@ describe ActiveRecord::ConnectionHandling do expect{ ActiveRecord::Base.postgresql_fallback_connection(multisite_config) } .to change{ Discourse.readonly_mode? }.from(false).to(true) - expect(postgresql_fallback_handler.master).to eq(false) + expect(postgresql_fallback_handler.master_down?).to eq(true) end + postgresql_fallback_handler.master_up(multisite_db) + ActiveRecord::Base.unstub(:postgresql_connection) - current_threads = Thread.list - - expect{ ActiveRecord::Base.connection_pool.checkout } - .to change{ Thread.list.size }.by(1) - - # Ensure that we don't try to connect back to the replica when a thread - # is running - begin - ActiveRecord::Base.postgresql_fallback_connection(config) - rescue PG::ConnectionBad => e - # This is expected if the thread finishes before the above is called. - end - - # Wait for the thread to finish execution - (Thread.list - current_threads).each(&:join) + postgresql_fallback_handler.initiate_fallback_to_master expect(Discourse.readonly_mode?).to eq(false) - expect(PostgreSQLFallbackHandler.instance.master).to eq(true) + expect(postgresql_fallback_handler.master_down?).to eq(nil) expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) From 759feef3f03c2f2f524b9acbdc10df21a506205c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Mon, 14 Nov 2016 11:39:19 +0800 Subject: [PATCH 2/2] FIX: No loggers may have been chained. --- config/initializers/100-logster.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb index c39494f6a05..d913051336c 100644 --- a/config/initializers/100-logster.rb +++ b/config/initializers/100-logster.rb @@ -87,5 +87,6 @@ RailsMultisite::ConnectionManagement.each_connection do end if Rails.configuration.multisite - Rails.logger.instance_variable_get(:@chained).first.formatter = RailsMultisite::Formatter.new + chained = Rails.logger.instance_variable_get(:@chained) + chained && chained.first.formatter = RailsMultisite::Formatter.new end