Merge pull request #4049 from tgxworld/support_multisite_in_postgresql_fallback

FEATURE: Support multisite in PostgreSQL fallback adapter.
This commit is contained in:
Sam 2016-03-07 14:01:15 +11:00
commit 77ecaba43a
4 changed files with 143 additions and 50 deletions

View file

@ -5,65 +5,103 @@ require 'discourse'
class PostgreSQLFallbackHandler class PostgreSQLFallbackHandler
include Singleton include Singleton
attr_reader :running
attr_accessor :master
def initialize def initialize
@master = true @master = {}
@running = false @running = {}
@mutex = Mutex.new @mutex = {}
@last_check = {}
setup!
end end
def verify_master def verify_master
@mutex.synchronize do @mutex[namespace].synchronize do
return if @running || recently_checked? return if running || recently_checked?
@running = true @running[namespace] = true
end end
current_namespace = namespace
Thread.new do Thread.new do
RailsMultisite::ConnectionManagement.with_connection(current_namespace) do
begin begin
logger.warn "#{self.class}: Checking master server..." logger.warn "#{log_prefix}: Checking master server..."
connection = ActiveRecord::Base.postgresql_connection(config) connection = ActiveRecord::Base.postgresql_connection(config)
if connection.active? if connection.active?
connection.disconnect! connection.disconnect!
logger.warn "#{self.class}: Master server is active. Reconnecting..." ActiveRecord::Base.clear_all_connections!
logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
if namespace == RailsMultisite::ConnectionManagement::DEFAULT
ActiveRecord::Base.establish_connection(config) ActiveRecord::Base.establish_connection(config)
else
RailsMultisite::ConnectionManagement.establish_connection(db: namespace)
end
Discourse.disable_readonly_mode Discourse.disable_readonly_mode
@master = true master = true
end end
rescue => e rescue => e
if e.message.include?("could not connect to server") if e.message.include?("could not connect to server")
logger.warn "#{self.class}: Connection to master PostgreSQL server failed with '#{e.message}'" logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
else else
raise e raise e
end end
ensure ensure
@mutex.synchronize do @mutex[namespace].synchronize do
@last_check = Time.zone.now @last_check[namespace] = Time.zone.now
@running = false @running[namespace] = false
end end
end end
end end
end end
end
def master
@master[namespace]
end
def master=(args)
@master[namespace] = args
end
def running
@running[namespace]
end
def setup!
RailsMultisite::ConnectionManagement.all_dbs.each do |db|
@master[db] = true
@running[db] = false
@mutex[db] = Mutex.new
end
end
private private
def config def config
ActiveRecord::Base.configurations[Rails.env] ActiveRecord::Base.connection_config
end end
def logger def logger
Rails.logger Rails.logger
end end
def log_prefix
"#{self.class} [#{namespace}]"
end
def recently_checked? def recently_checked?
if @last_check if @last_check[namespace]
Time.zone.now <= (@last_check + 5.seconds) Time.zone.now <= (@last_check[namespace] + 5.seconds)
else else
false false
end end
end end
def namespace
RailsMultisite::ConnectionManagement.current_db
end
end end
module ActiveRecord module ActiveRecord

View file

@ -112,17 +112,22 @@ module Discourse
end end
end end
def self.last_read_only
@last_read_only ||= {}
end
def self.recently_readonly? def self.recently_readonly?
return false unless @last_read_only read_only = last_read_only[$redis.namespace]
@last_read_only > 15.seconds.ago return false unless read_only
read_only > 15.seconds.ago
end end
def self.received_readonly! def self.received_readonly!
@last_read_only = Time.now last_read_only[$redis.namespace] = Time.zone.now
end end
def self.clear_readonly! def self.clear_readonly!
@last_read_only = nil last_read_only[$redis.namespace] = nil
end end
def self.disabled_plugin_names def self.disabled_plugin_names

View file

@ -3,10 +3,10 @@ require_dependency 'active_record/connection_adapters/postgresql_fallback_adapte
describe ActiveRecord::ConnectionHandling do describe ActiveRecord::ConnectionHandling do
let(:replica_host) { "1.1.1.1" } let(:replica_host) { "1.1.1.1" }
let(:replica_port) { "6432" } let(:replica_port) { 6432 }
let(:config) do let(:config) do
ActiveRecord::Base.configurations["test"].merge({ ActiveRecord::Base.configurations[Rails.env].merge({
"adapter" => "postgresql_fallback", "adapter" => "postgresql_fallback",
"replica_host" => replica_host, "replica_host" => replica_host,
"replica_port" => replica_port "replica_port" => replica_port
@ -14,8 +14,7 @@ describe ActiveRecord::ConnectionHandling do
end end
after do after do
Discourse.disable_readonly_mode ::PostgreSQLFallbackHandler.instance.setup!
::PostgreSQLFallbackHandler.instance.master = true
end end
describe "#postgresql_fallback_connection" do describe "#postgresql_fallback_connection" do
@ -25,17 +24,39 @@ describe ActiveRecord::ConnectionHandling do
end end
context 'when master server is down' do context 'when master server is down' do
let(:multisite_db) { "database_2" }
let(:multisite_config) do
{
host: 'localhost1',
port: 5432,
replica_host: replica_host,
replica_port: replica_port
}
end
before do before do
@replica_connection = mock('replica_connection') @replica_connection = mock('replica_connection')
end end
after do
with_multisite_db(multisite_db) { Discourse.disable_readonly_mode }
Discourse.disable_readonly_mode
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
end
it 'should failover to a replica server' do it 'should failover to a replica server' do
ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad) 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)
ActiveRecord::Base.expects(:verify_replica).with(@replica_connection) ActiveRecord::Base.expects(:verify_replica).with(@replica_connection)
ActiveRecord::Base.expects(:postgresql_connection).with(config.merge({ ActiveRecord::Base.expects(:postgresql_connection).with(configuration.merge({
host: replica_host, port: replica_port host: replica_host, port: replica_port
})).returns(@replica_connection) })).returns(@replica_connection)
end
expect { ActiveRecord::Base.postgresql_fallback_connection(config) } expect { ActiveRecord::Base.postgresql_fallback_connection(config) }
.to raise_error(PG::ConnectionBad) .to raise_error(PG::ConnectionBad)
@ -43,6 +64,14 @@ describe ActiveRecord::ConnectionHandling do
expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } expect{ ActiveRecord::Base.postgresql_fallback_connection(config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true) .to change{ Discourse.readonly_mode? }.from(false).to(true)
with_multisite_db(multisite_db) do
expect { ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to raise_error(PG::ConnectionBad)
expect{ ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true)
end
ActiveRecord::Base.unstub(:postgresql_connection) ActiveRecord::Base.unstub(:postgresql_connection)
current_threads = Thread.list current_threads = Thread.list
@ -59,7 +88,7 @@ describe ActiveRecord::ConnectionHandling do
end end
# Wait for the thread to finish execution # Wait for the thread to finish execution
threads = (Thread.list - current_threads).each(&:join) (Thread.list - current_threads).each(&:join)
expect(Discourse.readonly_mode?).to eq(false) expect(Discourse.readonly_mode?).to eq(false)
@ -72,7 +101,11 @@ describe ActiveRecord::ConnectionHandling do
context 'when both master and replica server is down' do context 'when both master and replica server is down' do
it 'should raise the right error' do it 'should raise the right error' do
ActiveRecord::Base.expects(:postgresql_connection).raises(PG::ConnectionBad).twice ActiveRecord::Base.expects(:postgresql_connection).with(config).raises(PG::ConnectionBad).once
ActiveRecord::Base.expects(:postgresql_connection).with(config.dup.merge({
host: replica_host, port: replica_port
})).raises(PG::ConnectionBad).once
2.times do 2.times do
expect { ActiveRecord::Base.postgresql_fallback_connection(config) } expect { ActiveRecord::Base.postgresql_fallback_connection(config) }
@ -81,4 +114,10 @@ describe ActiveRecord::ConnectionHandling do
end end
end end
end end
def with_multisite_db(dbname)
RailsMultisite::ConnectionManagement.expects(:current_db).returns(dbname).at_least_once
yield
RailsMultisite::ConnectionManagement.unstub(:current_db)
end
end end

View file

@ -111,8 +111,12 @@ describe Discourse do
end end
it "returns true when the key is present in redis" do it "returns true when the key is present in redis" do
$redis.expects(:get).with(Discourse.readonly_mode_key).returns("1") begin
$redis.set(Discourse.readonly_mode_key, 1)
expect(Discourse.readonly_mode?).to eq(true) expect(Discourse.readonly_mode?).to eq(true)
ensure
$redis.del(Discourse.readonly_mode_key)
end
end end
it "returns true when Discourse is recently read only" do it "returns true when Discourse is recently read only" do
@ -121,6 +125,13 @@ describe Discourse do
end end
end end
context ".received_readonly!" do
it "sets the right time" do
time = Discourse.received_readonly!
expect(Discourse.last_read_only['default']).to eq(time)
end
end
context "#handle_exception" do context "#handle_exception" do
class TempSidekiqLogger < Sidekiq::ExceptionHandler::Logger class TempSidekiqLogger < Sidekiq::ExceptionHandler::Logger