# frozen_string_literal: true # encoding: utf-8 require 'spec_helper' # The tests raise OperationFailure in socket reads. This is done for # convenience to make the tests uniform between socket errors and operation # failures; in reality a socket read will never raise OperationFailure as # wire protocol parsing code raises this exception. For the purposes of # testing retryable writes, it is acceptable to raise OperationFailure in # socket reads because both exceptions end up getting handled in the same # place by retryable writes code. The SDAM error handling test specifically # checks server state (i.e. being marked unknown) and scanning behavior # that is performed by the wire protocol code; this test omits scan assertions # as otherwise it quickly becomes unwieldy. describe 'Retryable writes integration tests' do include PrimarySocket require_wired_tiger_on_36 # These tests override server selector, which fails if there are multiple # eligible servers as would be the case in a multi-shard sharded cluster require_no_multi_mongos # Note: these tests are deprecated in favor of the tests in the file # spec/integration/retryable_writes/retryable_writes_40_and_newer_spec.rb # If you are changing functionality in the driver that only impacts server # versions 4.0 or newer, test that functionality in the other test file. max_server_fcv '3.6' before do authorized_collection.delete_many end let(:check_collection) do # Verify data in the collection using another client instance to avoid # having the verification read trigger cluster scans on the writing client root_authorized_client[TEST_COLL] end let(:primary_connection) do client.database.command(ping: 1) expect(primary_server.pool.size).to eq(1) expect(primary_server.pool.available_count).to eq(1) primary_server.pool.instance_variable_get('@available_connections').last end shared_examples_for 'an operation that is retried' do context 'when the operation fails on the first attempt and succeeds on the second attempt' do before do wait_for_all_servers(client.cluster) allow(primary_socket).to receive(:do_write).and_raise(error.dup) end context 'when the error is retryable' do before do expect(Mongo::Logger.logger).to receive(:warn).once.and_call_original end context 'when the error is a socket error' do let(:error) do IOError.new('first error') end it 'retries writes' do operation expect(expectation).to eq(successful_retry_value) end end context 'when the error is a socket timeout error' do let(:error) do Errno::ETIMEDOUT.new end it 'retries writes' do operation expect(expectation).to eq(successful_retry_value) end end context 'when the error is a retryable OperationFailure' do let(:error) do Mongo::Error::OperationFailure.new('not master') end let(:reply) do make_not_master_reply end it 'retries writes' do operation expect(expectation).to eq(successful_retry_value) end end end context 'when the error is not retryable' do context 'when the error is a non-retryable OperationFailure' do let(:error) do Mongo::Error::OperationFailure.new('other error', code: 123) end it 'does not retry writes' do expect do operation end.to raise_error(Mongo::Error::OperationFailure, /other error/) expect(expectation).to eq(unsuccessful_retry_value) end it 'indicates server used for operation' do expect do operation end.to raise_error(Mongo::Error::OperationFailure, /on #{ClusterConfig.instance.primary_address_str}/) end it 'indicates first attempt' do expect do operation end.to raise_error(Mongo::Error::OperationFailure, /attempt 1/) end end end end context 'when the operation fails on the first attempt and again on the second attempt' do before do allow(primary_socket).to receive(:do_write).and_raise(error.dup) end context 'when the selected server does not support retryable writes' do before do legacy_primary = double('legacy primary', :retry_writes? => false) expect(collection).to receive(:select_server).and_return(primary_server, legacy_primary) expect(primary_socket).to receive(:do_write).and_raise(error.dup) end context 'when the error is a socket error' do let(:error) do IOError.new('first error') end let(:exposed_error_class) do Mongo::Error::SocketError end it 'does not retry writes and raises the original error' do expect do operation end.to raise_error(exposed_error_class, /first error/) expect(expectation).to eq(unsuccessful_retry_value) end end context 'when the error is a socket timeout error' do let(:error) do Errno::ETIMEDOUT.new('first error') end it 'does not retry writes and raises the original error' do expect do operation # The exception message is different because of added diagnostics. end.to raise_error(Mongo::Error::SocketTimeoutError, /first error/) expect(expectation).to eq(unsuccessful_retry_value) end end context 'when the error is a retryable OperationFailure' do let(:error) do Mongo::Error::OperationFailure.new('not master') end it 'does not retry writes and raises the original error' do expect do operation end.to raise_error(Mongo::Error::OperationFailure, /not master/) expect(expectation).to eq(unsuccessful_retry_value) end end end [ [IOError, 'first error', Mongo::Error::SocketError], [Errno::ETIMEDOUT, 'first error', Mongo::Error::SocketTimeoutError], [Mongo::Error::OperationFailure, 'first error: not master', Mongo::Error::OperationFailure], [Mongo::Error::OperationFailure, 'first error: node is recovering', Mongo::Error::OperationFailure], ].each do |error_cls, error_msg, exposed_first_error_class| # Note: actual exception instances must be different between tests context "when the first error is a #{error_cls}/#{error_msg}" do let(:error) do error_cls.new(error_msg) end before do wait_for_all_servers(client.cluster) bad_socket = primary_connection.address.socket(primary_connection.socket_timeout, primary_connection.send(:ssl_options)) good_socket = primary_connection.address.socket(primary_connection.socket_timeout, primary_connection.send(:ssl_options)) allow(bad_socket).to receive(:do_write).and_raise(second_error.dup) allow(primary_connection.address).to receive(:socket).and_return(bad_socket, good_socket) end context 'when the second error is a socket error' do let(:second_error) do IOError.new('second error') end let(:exposed_error_class) do Mongo::Error::SocketError end it 'raises the second error' do expect do operation end.to raise_error(exposed_error_class, /second error/) expect(expectation).to eq(unsuccessful_retry_value) end it 'indicates server used for operation' do expect do operation end.to raise_error(Mongo::Error, /on #{ClusterConfig.instance.primary_address_str}/) end it 'indicates second attempt' do expect do operation end.to raise_error(Mongo::Error, /attempt 2/) end end context 'when the second error is a socket timeout error' do let(:second_error) do Errno::ETIMEDOUT.new('second error') end let(:exposed_error_class) do Mongo::Error::SocketTimeoutError end it 'raises the second error' do expect do operation end.to raise_error(exposed_error_class, /second error/) expect(expectation).to eq(unsuccessful_retry_value) end end context 'when the second error is a retryable OperationFailure' do let(:second_error) do Mongo::Error::OperationFailure.new('second error: not master') end it 'raises the second error' do expect do operation end.to raise_error(Mongo::Error, /second error: not master/) expect(expectation).to eq(unsuccessful_retry_value) end end context 'when the second error is a non-retryable OperationFailure' do let(:second_error) do Mongo::Error::OperationFailure.new('other error') end it 'does not retry writes and raises the first error' do expect do operation end.to raise_error(exposed_first_error_class, /first error/) expect(expectation).to eq(unsuccessful_retry_value) end end # The driver shouldn't be producing non-Mongo::Error derived errors, # but if those are produced (like ArgumentError), they would be # immediately propagated to the application. context 'when the second error is another error' do let(:second_error) do StandardError.new('second error') end it 'raises the second error' do expect do operation end.to raise_error(StandardError, /second error/) expect(expectation).to eq(unsuccessful_retry_value) end end end end end end shared_examples_for 'an operation that is not retried' do let!(:client) do authorized_client_without_retry_writes end before do expect(primary_socket).to receive(:do_write).exactly(:once).and_raise(Mongo::Error::SocketError) end it 'does not retry writes' do expect do operation end.to raise_error(Mongo::Error::SocketError) expect(expectation).to eq(unsuccessful_retry_value) end end shared_examples_for 'an operation that does not support retryable writes' do let!(:client) do authorized_client_with_retry_writes end let!(:collection) do client[TEST_COLL] end before do expect(primary_socket).to receive(:do_write).and_raise(Mongo::Error::SocketError) end it 'does not retry writes' do expect do operation end.to raise_error(Mongo::Error::SocketError) expect(expectation).to eq(unsuccessful_retry_value) end end shared_examples_for 'operation that is retried when server supports retryable writes' do context 'when the server supports retryable writes' do min_server_fcv '3.6' before do allow(primary_server).to receive(:retry_writes?).and_return(true) end context 'standalone' do require_topology :single it_behaves_like 'an operation that is not retried' end context 'replica set or sharded cluster' do require_topology :replica_set, :sharded it_behaves_like 'an operation that is retried' end end context 'when the server does not support retryable writes' do before do allow(primary_server).to receive(:retry_writes?).and_return(false) end it_behaves_like 'an operation that is not retried' end end shared_examples_for 'supported retryable writes' do context 'when the client has retry_writes set to true' do let!(:client) do authorized_client_with_retry_writes end context 'when the collection has write concern acknowledged' do let!(:collection) do client[TEST_COLL, write: {w: :majority}] end it_behaves_like 'operation that is retried when server supports retryable writes' end context 'when the collection has write concern unacknowledged' do let!(:collection) do client[TEST_COLL, write: { w: 0 }] end it_behaves_like 'an operation that is not retried' end end context 'when the client has retry_writes set to false' do let!(:client) do authorized_client_without_retry_writes end context 'when the collection has write concern acknowledged' do let!(:collection) do client[TEST_COLL, write: {w: :majority}] end it_behaves_like 'an operation that is not retried' end context 'when the collection has write concern unacknowledged' do let!(:collection) do client[TEST_COLL, write: { w: 0 }] end it_behaves_like 'an operation that is not retried' end context 'when the collection has write concern not set' do let!(:collection) do client[TEST_COLL] end it_behaves_like 'an operation that is not retried' end end end context 'when the operation is insert_one' do let(:operation) do collection.insert_one(a:1) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 1 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'supported retryable writes' end context 'when the operation is update_one' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) end let(:operation) do collection.update_one({ a: 0 }, { '$set' => { a: 1 } }) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 1 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'supported retryable writes' end context 'when the operation is replace_one' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) end let(:operation) do collection.replace_one({ a: 0 }, { a: 1 }) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 1 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'supported retryable writes' end context 'when the operation is delete_one' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:1) end let(:operation) do collection.delete_one(a:1) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 0 end let(:unsuccessful_retry_value) do 1 end it_behaves_like 'supported retryable writes' end context 'when the operation is find_one_and_update' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) end let(:operation) do collection.find_one_and_update({ a: 0 }, { '$set' => { a: 1 } }) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 1 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'supported retryable writes' end context 'when the operation is find_one_and_replace' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) end let(:operation) do collection.find_one_and_replace({ a: 0 }, { a: 3 }) end let(:expectation) do check_collection.find(a: 3).count end let(:successful_retry_value) do 1 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'supported retryable writes' end context 'when the operation is find_one_and_delete' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:1) end let(:operation) do collection.find_one_and_delete({ a: 1 }) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 0 end let(:unsuccessful_retry_value) do 1 end it_behaves_like 'supported retryable writes' end context 'when the operation is update_many' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) authorized_collection.insert_one(a:0) end let(:operation) do collection.update_many({ a: 0 }, { '$set' => { a: 1 } }) end let(:expectation) do check_collection.find(a: 1).count end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'an operation that does not support retryable writes' end context 'when the operation is delete_many' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:1) authorized_collection.insert_one(a:1) end let(:operation) do collection.delete_many(a: 1) end let(:expectation) do check_collection.find(a: 1).count end let(:unsuccessful_retry_value) do 2 end it_behaves_like 'an operation that does not support retryable writes' end context 'when the operation is a bulk write' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a: 1) end let(:operation) do collection.bulk_write([{ delete_one: { filter: { a: 1 } } }, { insert_one: { a: 1 } }, { insert_one: { a: 1 } }]) end let(:expectation) do check_collection.find(a: 1).count end let(:successful_retry_value) do 2 end let(:unsuccessful_retry_value) do 1 end it_behaves_like 'supported retryable writes' end context 'when the operation is bulk write including delete_many' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:1) authorized_collection.insert_one(a:1) end let(:operation) do collection.bulk_write([{ delete_many: { filter: { a: 1 } } }]) end let(:expectation) do check_collection.find(a: 1).count end let(:unsuccessful_retry_value) do 2 end it_behaves_like 'an operation that does not support retryable writes' end context 'when the operation is bulk write including update_many' do before do # Account for when the collection has unacknowledged write concern and use authorized_collection here. authorized_collection.insert_one(a:0) authorized_collection.insert_one(a:0) end let(:operation) do collection.bulk_write([{ update_many: { filter: { a: 0 }, update: { "$set" => { a: 1 } } } }]) end let(:expectation) do check_collection.find(a: 1).count end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'an operation that does not support retryable writes' end context 'when the operation is database#command' do let(:operation) do collection.database.command(ping: 1) end let(:expectation) do 0 end let(:unsuccessful_retry_value) do 0 end it_behaves_like 'an operation that does not support retryable writes' end end