# 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 describe '.exists' do it 'should return false when key is not present' do expect(Discourse.redis.exists('test')).to eq(false) end it 'should return false when keys are not present' do expect(Discourse.redis.exists('test', 'test2')).to eq(false) end it 'should return true when key is present' do Discourse.redis.set('test', 1) expect(Discourse.redis.exists('test')).to eq(true) end it 'should return true when any key is present' do Discourse.redis.set('test', 1) Discourse.redis.set('test2', 1) expect(Discourse.redis.exists('test')).to eq(true) expect(Discourse.redis.exists('test', 'test2')).to eq(true) expect(Discourse.redis.exists('test2', 'test3')).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