Fix Redis command errors when trying to start app with a readonly Redis.

This commit is contained in:
Guo Xiang Tan 2017-08-02 14:32:01 +09:00
parent 3538b63305
commit 9bc3038728
4 changed files with 86 additions and 11 deletions

View File

@ -48,6 +48,8 @@ class GlobalSetting
end end
token token
end end
rescue Redis::CommandError => e
@safe_secret_key_base = SecureRandom.hex(64) if e.message =~ /READONLY/
end end
def self.load_defaults def self.load_defaults

View File

@ -19,7 +19,9 @@ if defined?(Rack::MiniProfiler)
# raw_connection means results are not namespaced # raw_connection means results are not namespaced
# #
# namespacing gets complex, cause mini profiler is in the rack chain way before multisite # namespacing gets complex, cause mini profiler is in the rack chain way before multisite
Rack::MiniProfiler.config.storage_instance = Rack::MiniProfiler::RedisStore.new(connection: DiscourseRedis.raw_connection) Rack::MiniProfiler.config.storage_instance = Rack::MiniProfiler::RedisStore.new(
connection: DiscourseRedis.new(nil, namespace: false)
)
skip = [ skip = [
/^\/message-bus/, /^\/message-bus/,

View File

@ -135,9 +135,10 @@ class DiscourseRedis
options.dup.merge!(host: options[:slave_host], port: options[:slave_port]) options.dup.merge!(host: options[:slave_host], port: options[:slave_port])
end end
def initialize(config = nil) def initialize(config = nil, namespace: true)
@config = config || DiscourseRedis.config @config = config || DiscourseRedis.config
@redis = DiscourseRedis.raw_connection(@config) @redis = DiscourseRedis.raw_connection(@config)
@namespace = namespace
end end
def self.fallback_handler def self.fallback_handler
@ -183,29 +184,35 @@ class DiscourseRedis
:sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank, :sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank,
:zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore].each do |m| :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore].each do |m|
define_method m do |*args| define_method m do |*args|
args[0] = "#{namespace}:#{args[0]}" args[0] = "#{namespace}:#{args[0]}" if @namespace
DiscourseRedis.ignore_readonly { @redis.send(m, *args) } DiscourseRedis.ignore_readonly { @redis.send(m, *args) }
end end
end end
def mget(*args) def mget(*args)
args.map! { |a| "#{namespace}:#{a}" } args.map! { |a| "#{namespace}:#{a}" } if @namespace
DiscourseRedis.ignore_readonly { @redis.mget(*args) } DiscourseRedis.ignore_readonly { @redis.mget(*args) }
end end
def del(k) def del(k)
DiscourseRedis.ignore_readonly do DiscourseRedis.ignore_readonly do
k = "#{namespace}:#{k}" k = "#{namespace}:#{k}" if @namespace
@redis.del k @redis.del k
end end
end end
def keys(pattern = nil) def keys(pattern = nil)
DiscourseRedis.ignore_readonly do DiscourseRedis.ignore_readonly do
len = namespace.length + 1 pattern = pattern || '*'
@redis.keys("#{namespace}:#{pattern || '*'}").map { pattern = "#{namespace}:#{pattern}" if @namespace
|k| k[len..-1] keys = @redis.keys(pattern)
}
if @namespace
len = namespace.length + 1
keys.map! { |k| k[len..-1] }
end
keys
end end
end end

View File

@ -10,6 +10,70 @@ describe DiscourseRedis do
let(:fallback_handler) { DiscourseRedis::FallbackHandler.instance } let(:fallback_handler) { DiscourseRedis::FallbackHandler.instance }
describe 'redis commands' do
let(:raw_redis) { Redis.new(DiscourseRedis.config) }
before do
raw_redis.flushall
end
after do
raw_redis.flushall
end
describe 'when namespace is enabled' do
let(:redis) { DiscourseRedis.new }
it 'should append namespace to the keys' do
redis.set('key', 1)
expect(raw_redis.get('default:key')).to eq('1')
expect(redis.keys).to eq(['key'])
redis.del('key')
expect(raw_redis.get('default:key')).to eq(nil)
raw_redis.set('default:key1', '1')
raw_redis.set('default:key2', '2')
expect(redis.mget('key1', 'key2')).to eq(['1', '2'])
end
end
describe 'when namespace is disabled' do
let(:redis) { DiscourseRedis.new(nil, namespace: false) }
it 'should not append any namespace to the keys' do
redis.set('key', 1)
expect(raw_redis.get('key')).to eq('1')
expect(redis.keys).to eq(['key'])
redis.del('key')
expect(raw_redis.get('key')).to eq(nil)
raw_redis.set('key1', '1')
raw_redis.set('key2', '2')
expect(redis.mget('key1', 'key2')).to eq(['1', '2'])
end
it 'should noop a readonly redis' do
expect(Discourse.recently_readonly?).to eq(false)
redis.without_namespace
.expects(:set)
.raises(Redis::CommandError.new("READONLY"))
redis.set('key', 1)
expect(Discourse.recently_readonly?).to eq(true)
end
end
end
context '.slave_host' do context '.slave_host' do
it 'should return the right config' do it 'should return the right config' do
slave_config = DiscourseRedis.slave_config(config) slave_config = DiscourseRedis.slave_config(config)
@ -22,9 +86,9 @@ describe DiscourseRedis do
it 'should check the status of the master server' do it 'should check the status of the master server' do
begin begin
fallback_handler.master = false fallback_handler.master = false
$redis.without_namespace.expects(:get).raises(Redis::CommandError.new("READONLY")) $redis.without_namespace.expects(:set).raises(Redis::CommandError.new("READONLY"))
fallback_handler.expects(:verify_master).once fallback_handler.expects(:verify_master).once
$redis.get('test') $redis.set('test', '1')
ensure ensure
fallback_handler.master = true fallback_handler.master = true
end end