diff --git a/lib/mongo/retryable.rb b/lib/mongo/retryable.rb index 5e67b2d58d..c7ad0bcc08 100644 --- a/lib/mongo/retryable.rb +++ b/lib/mongo/retryable.rb @@ -24,6 +24,10 @@ module Retryable # @since 2.1.0 NOT_MASTER = 'not master'.freeze + # Error codes received around reconfiguration + CONNECTION_ERRORS_RECONFIGURATION_CODES = [ 15988, 10276, 11600, 9001, 13639, 10009, 11002, 7 ].map(&:to_s).freeze + CONNECTION_ERRORS_MAGIC_STRINGS = [ 'could not get last error', 'connection attempt failed' ].freeze + # Execute a read operation with a retry. # # @example Execute the read. @@ -43,6 +47,12 @@ def read_with_retry(&block) block.call rescue Error::SocketError, Error::SocketTimeoutError retry_operation(&block) + rescue Error::OperationFailure => e + if CONNECTION_ERRORS_RECONFIGURATION_CODES.any? {|code| e.message.include?(code) } || CONNECTION_ERRORS_MAGIC_STRINGS.any? {|str| e.message.include?(str)} + retry_operation(&block) + else + raise e + end end end diff --git a/lib/mongo/server/connectable.rb b/lib/mongo/server/connectable.rb index ba716519c0..a334eefda9 100644 --- a/lib/mongo/server/connectable.rb +++ b/lib/mongo/server/connectable.rb @@ -20,6 +20,10 @@ class Server # @since 2.0.0 module Connectable + def self.included(base) + base.__send__(:include, Mongo::Retryable) + end + # The ssl option prefix. # # @since 2.1.0 @@ -103,7 +107,9 @@ def ensure_same_process! end def read - ensure_connected{ |socket| Protocol::Reply.deserialize(socket) } + read_with_retry do + ensure_connected{ |socket| Protocol::Reply.deserialize(socket) } + end end end end diff --git a/spec/mongo/retryable_spec.rb b/spec/mongo/retryable_spec.rb index 3a622510bc..f7fdedb321 100644 --- a/spec/mongo/retryable_spec.rb +++ b/spec/mongo/retryable_spec.rb @@ -66,6 +66,58 @@ def write end end + context 'when an operation error occurs that could not get last error' do + + before do + expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new("could not get last error")).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.read).to be true + end + end + + context 'when an operation error occurs that had a connection attempt failed' do + + before do + expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new("connection attempt failed")).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.read).to be true + end + end + + context 'when an operation error occurs with code 15988' do + + before do + expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new("error querying server (15988)")).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.read).to be true + end + end + + context 'when an operation error occurs with code 13639' do + + before do + expect(operation).to receive(:execute).and_raise(Mongo::Error::OperationFailure.new("connection attempt failed (13639)")).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.read).to be true + end + end + context 'when a socket timeout error occurs' do before do