Use a different Redis key when PG failover sets site to readonly mode.

This commit is contained in:
Guo Xiang Tan 2017-01-11 16:38:07 +08:00
parent 77045eb1f1
commit cdd550e947
5 changed files with 56 additions and 36 deletions

View File

@ -95,7 +95,14 @@ class Admin::BackupsController < Admin::AdminController
def readonly def readonly
enable = params.fetch(:enable).to_s == "true" enable = params.fetch(:enable).to_s == "true"
enable ? Discourse.enable_readonly_mode(user_enabled: true) : Discourse.disable_readonly_mode(user_enabled: true) readonly_mode_key = Discourse::USER_READONLY_MODE_KEY
if enable
Discourse.enable_readonly_mode(readonly_mode_key)
else
Discourse.disable_readonly_mode(readonly_mode_key)
end
render nothing: true render nothing: true
end end

View File

@ -52,7 +52,7 @@ class PostgreSQLFallbackHandler
logger.warn "#{log_prefix}: Master server is active. Reconnecting..." logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
self.master_up(key) self.master_up(key)
Discourse.disable_readonly_mode Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
end end
rescue => e rescue => e
logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'" logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
@ -103,7 +103,7 @@ module ActiveRecord
})) }))
verify_replica(connection) verify_replica(connection)
Discourse.enable_readonly_mode Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
else else
begin begin
connection = postgresql_connection(config) connection = postgresql_connection(config)

View File

@ -113,24 +113,6 @@ module Discourse
end end
end end
def self.last_read_only
@last_read_only ||= {}
end
def self.recently_readonly?
read_only = last_read_only[$redis.namespace]
return false unless read_only
read_only > 15.seconds.ago
end
def self.received_readonly!
last_read_only[$redis.namespace] = Time.zone.now
end
def self.clear_readonly!
last_read_only[$redis.namespace] = nil
end
def self.disabled_plugin_names def self.disabled_plugin_names
plugins.select { |p| !p.enabled? }.map(&:name) plugins.select { |p| !p.enabled? }.map(&:name)
end end
@ -212,41 +194,67 @@ module Discourse
READONLY_MODE_KEY_TTL ||= 60 READONLY_MODE_KEY_TTL ||= 60
READONLY_MODE_KEY ||= 'readonly_mode'.freeze READONLY_MODE_KEY ||= 'readonly_mode'.freeze
PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres'.freeze
USER_READONLY_MODE_KEY ||= 'readonly_mode:user'.freeze USER_READONLY_MODE_KEY ||= 'readonly_mode:user'.freeze
def self.enable_readonly_mode(user_enabled: false) READONLY_KEYS = [
if user_enabled READONLY_MODE_KEY,
$redis.set(USER_READONLY_MODE_KEY, 1) PG_READONLY_MODE_KEY,
USER_READONLY_MODE_KEY
]
def self.enable_readonly_mode(key = READONLY_MODE_KEY)
if key == USER_READONLY_MODE_KEY
$redis.set(key, 1)
else else
$redis.setex(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL, 1) $redis.setex(key, READONLY_MODE_KEY_TTL, 1)
keep_readonly_mode keep_readonly_mode(key)
end end
MessageBus.publish(readonly_channel, true) MessageBus.publish(readonly_channel, true)
true true
end end
def self.keep_readonly_mode def self.keep_readonly_mode(key)
# extend the expiry by 1 minute every 30 seconds # extend the expiry by 1 minute every 30 seconds
unless Rails.env.test? unless Rails.env.test?
Thread.new do Thread.new do
while readonly_mode? while readonly_mode?
$redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL) $redis.expire(key, READONLY_MODE_KEY_TTL)
sleep 30.seconds sleep 30.seconds
end end
end end
end end
end end
def self.disable_readonly_mode(user_enabled: false) def self.disable_readonly_mode(key = READONLY_MODE_KEY)
key = user_enabled ? USER_READONLY_MODE_KEY : READONLY_MODE_KEY
$redis.del(key) $redis.del(key)
MessageBus.publish(readonly_channel, false) MessageBus.publish(readonly_channel, false)
true true
end end
def self.readonly_mode? def self.readonly_mode?
recently_readonly? || !!$redis.get(READONLY_MODE_KEY) || !!$redis.get(USER_READONLY_MODE_KEY) return true if recently_readonly?
READONLY_KEYS.each { |key| return true if !!$redis.get(key) }
false
end
def self.last_read_only
@last_read_only ||= {}
end
def self.recently_readonly?
read_only = last_read_only[$redis.namespace]
return false unless read_only
read_only > 15.seconds.ago
end
def self.received_readonly!
last_read_only[$redis.namespace] = Time.zone.now
end
def self.clear_readonly!
last_read_only[$redis.namespace] = nil
end end
def self.request_refresh! def self.request_refresh!

View File

@ -42,8 +42,13 @@ describe ActiveRecord::ConnectionHandling do
end end
after do after do
with_multisite_db(multisite_db) { Discourse.disable_readonly_mode } pg_readonly_mode_key = Discourse::PG_READONLY_MODE_KEY
Discourse.disable_readonly_mode
with_multisite_db(multisite_db) do
Discourse.disable_readonly_mode(pg_readonly_mode_key)
end
Discourse.disable_readonly_mode(pg_readonly_mode_key)
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
end end

View File

@ -118,7 +118,7 @@ describe Discourse do
context 'user enabled readonly mode' do context 'user enabled readonly mode' do
it "adds a key in redis and publish a message through the message bus" do it "adds a key in redis and publish a message through the message bus" do
expect($redis.get(user_readonly_mode_key)).to eq(nil) expect($redis.get(user_readonly_mode_key)).to eq(nil)
message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_enabled: true) }.first message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_readonly_mode_key) }.first
assert_readonly_mode(message, user_readonly_mode_key) assert_readonly_mode(message, user_readonly_mode_key)
end end
end end
@ -160,10 +160,10 @@ describe Discourse do
end end
it "returns true when user enabled readonly mode key is present in redis" do it "returns true when user enabled readonly mode key is present in redis" do
Discourse.enable_readonly_mode(user_enabled: true) Discourse.enable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(true) expect(Discourse.readonly_mode?).to eq(true)
Discourse.disable_readonly_mode(user_enabled: true) Discourse.disable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(false) expect(Discourse.readonly_mode?).to eq(false)
end end
end end