mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 09:44:22 +08:00
d9a02d1336
This reverts commit20780a1eee
. * SECURITY: re-adds accidentally reverted commit: 03d26cd6: ensure embed_url contains valid http(s) uri * when the merge commite62a85cf
was reverted, git chose the2660c2e2
parent to land on instead of the03d26cd6
parent (which contains security fixes)
248 lines
6.8 KiB
Ruby
248 lines
6.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
describe DiscourseRedis do
|
|
let(:slave_host) { 'testhost' }
|
|
let(:slave_port) { 1234 }
|
|
|
|
let(:config) do
|
|
DiscourseRedis.config.dup.merge(slave_host: 'testhost', slave_port: 1234, connector: DiscourseRedis::Connector)
|
|
end
|
|
|
|
let(:fallback_handler) { DiscourseRedis::FallbackHandler.instance }
|
|
|
|
it "ignore_readonly returns nil from a pure exception" do
|
|
result = DiscourseRedis.ignore_readonly { raise Redis::CommandError.new("READONLY") }
|
|
expect(result).to eq(nil)
|
|
end
|
|
|
|
describe 'redis commands' do
|
|
let(:raw_redis) { Redis.new(DiscourseRedis.config) }
|
|
|
|
before do
|
|
raw_redis.flushdb
|
|
end
|
|
|
|
after do
|
|
raw_redis.flushdb
|
|
end
|
|
|
|
describe 'when namespace is enabled' do
|
|
let(:redis) { DiscourseRedis.new }
|
|
|
|
it 'should append namespace to the keys' do
|
|
raw_redis.set('default:key', 1)
|
|
raw_redis.set('test:key2', 1)
|
|
|
|
expect(redis.keys).to include('key')
|
|
expect(redis.keys).to_not include('key2')
|
|
expect(redis.scan_each.to_a).to eq(['key'])
|
|
|
|
redis.scan_each.each do |key|
|
|
expect(key).to eq('key')
|
|
end
|
|
|
|
redis.del('key')
|
|
|
|
expect(raw_redis.get('default:key')).to eq(nil)
|
|
expect(redis.scan_each.to_a).to eq([])
|
|
|
|
raw_redis.set('default:key1', '1')
|
|
raw_redis.set('default:key2', '2')
|
|
|
|
expect(redis.mget('key1', 'key2')).to eq(['1', '2'])
|
|
expect(redis.scan_each.to_a).to contain_exactly('key1', 'key2')
|
|
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
|
|
raw_redis.set('default:key', 1)
|
|
raw_redis.set('test:key2', 1)
|
|
|
|
expect(redis.keys).to include('default:key', 'test:key2')
|
|
|
|
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
|
|
it 'should return the right config' do
|
|
slave_config = DiscourseRedis.slave_config(config)
|
|
expect(slave_config[:host]).to eq(slave_host)
|
|
expect(slave_config[:port]).to eq(slave_port)
|
|
end
|
|
end
|
|
|
|
context 'when redis connection is to a slave redis server' do
|
|
it 'should check the status of the master server' do
|
|
begin
|
|
fallback_handler.master = false
|
|
Discourse.redis.without_namespace.expects(:set).raises(Redis::CommandError.new("READONLY"))
|
|
fallback_handler.expects(:verify_master).once
|
|
Discourse.redis.set('test', '1')
|
|
ensure
|
|
fallback_handler.master = true
|
|
Discourse.redis.del('test')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe DiscourseRedis::Connector do
|
|
let(:connector) { DiscourseRedis::Connector.new(config) }
|
|
|
|
after do
|
|
fallback_handler.master = true
|
|
end
|
|
|
|
it 'should return the master config when master is up' do
|
|
expect(connector.resolve).to eq(config)
|
|
end
|
|
|
|
class BrokenRedis
|
|
def initialize(error)
|
|
@error = error
|
|
end
|
|
|
|
def call(*args)
|
|
raise @error
|
|
end
|
|
|
|
def disconnect
|
|
end
|
|
end
|
|
|
|
it 'should return the slave config when master is down' do
|
|
error = Redis::CannotConnectError
|
|
|
|
expect do
|
|
connector.resolve(BrokenRedis.new(error))
|
|
end.to raise_error(Redis::CannotConnectError)
|
|
|
|
config = connector.resolve
|
|
|
|
expect(config[:host]).to eq(slave_host)
|
|
expect(config[:port]).to eq(slave_port)
|
|
end
|
|
|
|
it "should return the slave config when master's hostname cannot be resolved" do
|
|
error = RuntimeError.new('Name or service not known')
|
|
|
|
expect do
|
|
connector.resolve(BrokenRedis.new(error))
|
|
end.to raise_error(error)
|
|
|
|
expect(fallback_handler.master).to eq(false)
|
|
|
|
config = connector.resolve
|
|
|
|
expect(config[:host]).to eq(slave_host)
|
|
expect(config[:port]).to eq(slave_port)
|
|
expect(fallback_handler.master).to eq(false)
|
|
end
|
|
|
|
it "should return the slave config when master is still loading data" do
|
|
Redis::Client.any_instance
|
|
.expects(:call)
|
|
.with([:info, :persistence])
|
|
.returns("
|
|
someconfig:haha\r
|
|
#{DiscourseRedis::FallbackHandler::MASTER_LOADING_STATUS}
|
|
")
|
|
|
|
config = connector.resolve
|
|
|
|
expect(config[:host]).to eq(slave_host)
|
|
expect(config[:port]).to eq(slave_port)
|
|
end
|
|
|
|
it "should raise the right error" do
|
|
error = RuntimeError.new('test')
|
|
|
|
2.times do
|
|
expect { connector.resolve(BrokenRedis.new(error)) }
|
|
.to raise_error(error)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe DiscourseRedis::FallbackHandler do
|
|
before do
|
|
@original_keepalive_interval = MessageBus.keepalive_interval
|
|
end
|
|
|
|
after do
|
|
fallback_handler.master = true
|
|
MessageBus.keepalive_interval = @original_keepalive_interval
|
|
end
|
|
|
|
describe '#initiate_fallback_to_master' do
|
|
it 'should return the right value if the master server is still down' do
|
|
fallback_handler.master = false
|
|
Redis::Client.any_instance.expects(:call).with([:info]).returns("Some other stuff")
|
|
|
|
expect(fallback_handler.initiate_fallback_to_master).to eq(false)
|
|
expect(MessageBus.keepalive_interval).to eq(0)
|
|
end
|
|
|
|
it 'should fallback to the master server once it is up' do
|
|
fallback_handler.master = false
|
|
master_conn = mock('master')
|
|
slave_conn = mock('slave')
|
|
|
|
Redis::Client.expects(:new)
|
|
.with(DiscourseRedis.config)
|
|
.returns(master_conn)
|
|
|
|
Redis::Client.expects(:new)
|
|
.with(DiscourseRedis.slave_config)
|
|
.returns(slave_conn)
|
|
|
|
master_conn.expects(:call)
|
|
.with([:info])
|
|
.returns("
|
|
#{DiscourseRedis::FallbackHandler::MASTER_ROLE_STATUS}\r\n
|
|
#{DiscourseRedis::FallbackHandler::MASTER_LOADED_STATUS}
|
|
")
|
|
|
|
DiscourseRedis::FallbackHandler::CONNECTION_TYPES.each do |connection_type|
|
|
slave_conn.expects(:call).with(
|
|
[:client, [:kill, 'type', connection_type]]
|
|
)
|
|
end
|
|
|
|
master_conn.expects(:disconnect)
|
|
slave_conn.expects(:disconnect)
|
|
|
|
expect(fallback_handler.initiate_fallback_to_master).to eq(true)
|
|
expect(fallback_handler.master).to eq(true)
|
|
expect(Discourse.recently_readonly?).to eq(false)
|
|
expect(MessageBus.keepalive_interval).to eq(-1)
|
|
end
|
|
end
|
|
end
|
|
end
|