spec/mongo/retryable_spec.rb in mongo-2.5.3 vs spec/mongo/retryable_spec.rb in mongo-2.6.0

- old
+ new

@@ -1,43 +1,76 @@ require 'spec_helper' -describe Mongo::Retryable do +class RetryableTestConsumer + include Mongo::Retryable - let(:klass) do - Class.new do - include Mongo::Retryable + attr_reader :cluster + attr_reader :operation - attr_reader :cluster - attr_reader :operation + def initialize(operation, cluster) + @operation = operation + @cluster = cluster + end - def initialize(operation, cluster) - @operation = operation - @cluster = cluster - end + def max_read_retries + cluster.max_read_retries + end - def max_read_retries - cluster.max_read_retries - end + def read_retry_interval + cluster.read_retry_interval + end - def read_retry_interval - cluster.read_retry_interval - end + def read + read_with_retry do + operation.execute + end + end - def read - read_with_retry do - operation.execute - end - end + def write + # This passes a nil session and therefore triggers + # legacy_write_with_retry code path + write_with_retry(session, write_concern) do + operation.execute + end + end - def write - write_with_retry(nil, nil) do - operation.execute - end - end + def retry_write_allowed_as_configured? + retry_write_allowed?(session, write_concern) + end +end + +class LegacyRetryableTestConsumer < RetryableTestConsumer + def session + nil + end + + def write_concern + nil + end +end + +class ModernRetryableTestConsumer < LegacyRetryableTestConsumer + include RSpec::Mocks::ExampleMethods + + def session + double('session').tap do |session| + expect(session).to receive(:retry_writes?).and_return(true) + + # mock everything else that is in the way + i = 1 + allow(session).to receive(:next_txn_num) { i += 1 } + allow(session).to receive(:in_transaction?).and_return(false) end end + def write_concern + nil + end +end + +describe Mongo::Retryable do + let(:operation) do double('operation') end let(:cluster) do @@ -47,11 +80,11 @@ let(:server_selector) do double('server_selector', select_server: double('server')) end let(:retryable) do - klass.new(operation, cluster) + LegacyRetryableTestConsumer.new(operation, cluster) end describe '#read_with_retry' do context 'when no exception occurs' do @@ -130,11 +163,11 @@ end context 'when the operation failure is retryable' do let(:error) do - Mongo::Error::OperationFailure.new('no master') + Mongo::Error::OperationFailure.new('not master') end context 'when the retry succeeds' do before do @@ -171,12 +204,17 @@ end end end end - describe '#write_with_retry' do + describe '#write_with_retry - legacy' do + before do + # Quick sanity check that the expected code path is being exercised + expect(retryable.retry_write_allowed_as_configured?).to be false + end + context 'when no exception occurs' do before do expect(operation).to receive(:execute).and_return(true) end @@ -184,45 +222,225 @@ it 'executes the operation once' do expect(retryable.write).to be true end end + shared_examples 'executes the operation twice' do + it 'executes the operation twice' do + expect(retryable.write).to be true + end + end + context 'when a not master error occurs' do before do - expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new('not master')).ordered + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('not master')).ordered expect(cluster).to receive(:scan!).and_return(true).ordered expect(operation).to receive(:execute).and_return(true).ordered end - it 'executes the operation twice' do - expect(retryable.write).to be true + it_behaves_like 'executes the operation twice' + end + + context 'when a node is recovering error occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('node is recovering')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered end + + it_behaves_like 'executes the operation twice' end - context 'when a not primary error occurs' do + context 'when a retryable error occurs with a code' do before do - expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new('Not primary')).ordered + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('message missing', nil, + :code => 91, :code_name => 'ShutdownInProgress')).ordered expect(cluster).to receive(:scan!).and_return(true).ordered expect(operation).to receive(:execute).and_return(true).ordered end + it_behaves_like 'executes the operation twice' + end + + context 'when a normal operation failure occurs' do + + before do + expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure).ordered + end + + it 'raises an exception' do + expect { + retryable.write + }.to raise_error(Mongo::Error::OperationFailure) + end + end + + context 'when a socket error occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::SocketError.new('socket error')).ordered + end + + it 'raises an exception' do + expect { + retryable.write + }.to raise_error(Mongo::Error::SocketError) + end + end + + context 'when a socket timeout occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::SocketTimeoutError.new('socket timeout')).ordered + end + + it 'raises an exception' do + expect { + retryable.write + }.to raise_error(Mongo::Error::SocketTimeoutError) + end + end + + context 'when a non-retryable exception occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::UnsupportedCollation.new('unsupported collation')).ordered + end + + it 'raises an exception' do + expect { + retryable.write + }.to raise_error(Mongo::Error::UnsupportedCollation) + end + end + + end + + describe '#write_with_retry - modern' do + + let(:retryable) do + ModernRetryableTestConsumer.new(operation, cluster) + end + + before do + # Quick sanity check that the expected code path is being exercised + expect(retryable.retry_write_allowed_as_configured?).to be true + + allow(server_selector).to receive(:retry_writes?).and_return(true) + allow(cluster).to receive(:scan!) + end + + context 'when no exception occurs' do + + before do + expect(operation).to receive(:execute).and_return(true) + end + + it 'executes the operation once' do + expect(retryable.write).to be true + end + end + + shared_examples 'executes the operation twice' do it 'executes the operation twice' do expect(retryable.write).to be true end end + context 'when a not master error occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('not master')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered + end + + it_behaves_like 'executes the operation twice' + end + + context 'when a node is recovering error occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('node is recovering')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered + end + + it_behaves_like 'executes the operation twice' + end + + context 'when a retryable error occurs with a code' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::OperationFailure.new('message missing', nil, + :code => 91, :code_name => 'ShutdownInProgress')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered + end + + it_behaves_like 'executes the operation twice' + end + context 'when a normal operation failure occurs' do before do expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure).ordered end it 'raises an exception' do expect { retryable.write }.to raise_error(Mongo::Error::OperationFailure) + end + end + + context 'when a socket error occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::SocketError.new('socket error')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered + end + + it_behaves_like 'executes the operation twice' + end + + context 'when a socket timeout occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::SocketTimeoutError.new('socket timeout')).ordered + expect(cluster).to receive(:scan!).and_return(true).ordered + expect(operation).to receive(:execute).and_return(true).ordered + end + + it_behaves_like 'executes the operation twice' + end + + context 'when a non-retryable exception occurs' do + + before do + expect(operation).to receive(:execute).and_raise( + Mongo::Error::UnsupportedCollation.new('unsupported collation')).ordered + end + + it 'raises an exception' do + expect { + retryable.write + }.to raise_error(Mongo::Error::UnsupportedCollation) end end end end