From b1079af05c323035478fcc7f72354ec69e40f910 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Thu, 4 Sep 2014 16:37:33 +0100 Subject: [PATCH 01/15] Refactoring: move Selectable#with_retry to Cluster#with_retry --- .ruby-version | 2 +- lib/moped/cluster.rb | 30 +++++++++++++++++ lib/moped/read_preference/nearest.rb | 2 +- lib/moped/read_preference/primary.rb | 2 +- .../read_preference/primary_preferred.rb | 2 +- lib/moped/read_preference/secondary.rb | 2 +- .../read_preference/secondary_preferred.rb | 2 +- lib/moped/read_preference/selectable.rb | 32 ------------------- 8 files changed, 36 insertions(+), 38 deletions(-) diff --git a/.ruby-version b/.ruby-version index 3e3c2f1..eca07e4 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.1 +2.1.2 diff --git a/lib/moped/cluster.rb b/lib/moped/cluster.rb index 2bb17cb..4edf901 100644 --- a/lib/moped/cluster.rb +++ b/lib/moped/cluster.rb @@ -275,6 +275,36 @@ def with_secondary(&block) raise Errors::ConnectionFailure, "Could not connect to a secondary node for replica set #{inspect}" end + # Execute the provided block on the cluster and retry if the execution + # fails. + # + # @example Execute with retry. + # cluster.with_retry do + # cluster.with_primary do |node| + # node.refresh + # end + # end + # + # @param [ Integer ] retries The number of times to retry. + # + # @return [ Object ] The result of the block. + # + # @since 2.0.0 + def with_retry(retries = max_retries, &block) + begin + block.call + rescue Errors::ConnectionFailure => e + if retries > 0 + Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s).", "n/a") + sleep(retry_interval) + refresh + with_retry(retries - 1, &block) + else + raise e + end + end + end + private # Apply the credentials on all nodes diff --git a/lib/moped/read_preference/nearest.rb b/lib/moped/read_preference/nearest.rb index 663c550..25a354a 100644 --- a/lib/moped/read_preference/nearest.rb +++ b/lib/moped/read_preference/nearest.rb @@ -41,7 +41,7 @@ def name # # @since 2.0.0 def with_node(cluster, &block) - with_retry(cluster) do + cluster.with_retry do nearest = cluster.nodes.sort_by(&:latency).first if nearest block.call(nearest) diff --git a/lib/moped/read_preference/primary.rb b/lib/moped/read_preference/primary.rb index 68124aa..2675484 100644 --- a/lib/moped/read_preference/primary.rb +++ b/lib/moped/read_preference/primary.rb @@ -51,7 +51,7 @@ def query_options(options) # # @since 2.0.0 def with_node(cluster, &block) - with_retry(cluster) do + cluster.with_retry do cluster.with_primary(&block) end end diff --git a/lib/moped/read_preference/primary_preferred.rb b/lib/moped/read_preference/primary_preferred.rb index 3c35001..cf780ad 100644 --- a/lib/moped/read_preference/primary_preferred.rb +++ b/lib/moped/read_preference/primary_preferred.rb @@ -42,7 +42,7 @@ def name # # @since 2.0.0 def with_node(cluster, &block) - with_retry(cluster) do + cluster.with_retry do begin cluster.with_primary(&block) rescue Errors::ConnectionFailure diff --git a/lib/moped/read_preference/secondary.rb b/lib/moped/read_preference/secondary.rb index 44dfc1c..8cc338e 100644 --- a/lib/moped/read_preference/secondary.rb +++ b/lib/moped/read_preference/secondary.rb @@ -41,7 +41,7 @@ def name # # @since 2.0.0 def with_node(cluster, &block) - with_retry(cluster) do + cluster.with_retry do cluster.with_secondary(&block) end end diff --git a/lib/moped/read_preference/secondary_preferred.rb b/lib/moped/read_preference/secondary_preferred.rb index fd502a9..25de5b3 100644 --- a/lib/moped/read_preference/secondary_preferred.rb +++ b/lib/moped/read_preference/secondary_preferred.rb @@ -40,7 +40,7 @@ def name # # @since 2.0.0 def with_node(cluster, &block) - with_retry(cluster) do + cluster.with_retry do begin cluster.with_secondary(&block) rescue Errors::ConnectionFailure diff --git a/lib/moped/read_preference/selectable.rb b/lib/moped/read_preference/selectable.rb index 73686de..681b82c 100644 --- a/lib/moped/read_preference/selectable.rb +++ b/lib/moped/read_preference/selectable.rb @@ -42,38 +42,6 @@ def query_options(options) private - # Execute the provided block on the cluster and retry if the execution - # fails. - # - # @api private - # - # @example Execute with retry. - # preference.with_retry(cluster) do - # cluster.with_primary do |node| - # node.refresh - # end - # end - # - # @param [ Cluster ] cluster The cluster. - # @param [ Integer ] retries The number of times to retry. - # - # @return [ Object ] The result of the block. - # - # @since 2.0.0 - def with_retry(cluster, retries = cluster.max_retries, &block) - begin - block.call - rescue Errors::ConnectionFailure => e - if retries > 0 - Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s).", "n/a") - sleep(cluster.retry_interval) - cluster.refresh - with_retry(cluster, retries - 1, &block) - else - raise e - end - end - end end end end From b72c097802ca79315aa5aed996030b7e153fc640 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Thu, 4 Sep 2014 16:49:21 +0100 Subject: [PATCH 02/15] Retry on writes --- lib/moped/collection.rb | 6 +++-- lib/moped/query.rb | 54 +++++++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/moped/collection.rb b/lib/moped/collection.rb index d68f8da..9776df0 100644 --- a/lib/moped/collection.rb +++ b/lib/moped/collection.rb @@ -121,8 +121,10 @@ def initialize(database, name) # @since 1.0.0 def insert(documents, flags = nil) docs = documents.is_a?(Array) ? documents : [ documents ] - cluster.with_primary do |node| - node.insert(database.name, name, docs, write_concern, flags: flags || []) + cluster.with_retry do + cluster.with_primary do |node| + node.insert(database.name, name, docs, write_concern, flags: flags || []) + end end end diff --git a/lib/moped/query.rb b/lib/moped/query.rb index f308010..d89c6cf 100644 --- a/lib/moped/query.rb +++ b/lib/moped/query.rb @@ -321,14 +321,16 @@ def modify(change, options = {}) # # @since 1.0.0 def remove - cluster.with_primary do |node| - node.remove( - operation.database, - operation.collection, - operation.basic_selector, - write_concern, - flags: [ :remove_first ] - ) + cluster.with_retry do + cluster.with_primary do |node| + node.remove( + operation.database, + operation.collection, + operation.basic_selector, + write_concern, + flags: [ :remove_first ] + ) + end end end @@ -341,13 +343,15 @@ def remove # # @since 1.0.0 def remove_all - cluster.with_primary do |node| - node.remove( - operation.database, - operation.collection, - operation.basic_selector, - write_concern - ) + cluster.with_retry do + cluster.with_primary do |node| + node.remove( + operation.database, + operation.collection, + operation.basic_selector, + write_concern + ) + end end end @@ -423,15 +427,17 @@ def tailable # # @since 1.0.0 def update(change, flags = nil) - cluster.with_primary do |node| - node.update( - operation.database, - operation.collection, - operation.selector["$query"] || operation.selector, - change, - write_concern, - flags: flags - ) + cluster.with_retry do + cluster.with_primary do |node| + node.update( + operation.database, + operation.collection, + operation.selector["$query"] || operation.selector, + change, + write_concern, + flags: flags + ) + end end end From 439ea782dd494ab273875f9d305df8176bb43ed2 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 8 Sep 2014 15:33:27 +0200 Subject: [PATCH 03/15] Failover working --- lib/moped/cluster.rb | 2 +- lib/moped/errors.rb | 50 +++++++++++-------------------- lib/moped/failover.rb | 7 +++-- lib/moped/failover/reconfigure.rb | 36 ---------------------- lib/moped/node.rb | 25 ++++++++-------- lib/moped/protocol/command.rb | 2 +- lib/moped/protocol/get_more.rb | 7 ++--- lib/moped/protocol/query.rb | 2 +- lib/moped/protocol/reply.rb | 31 +++++++++++++++++-- 9 files changed, 67 insertions(+), 95 deletions(-) delete mode 100644 lib/moped/failover/reconfigure.rb diff --git a/lib/moped/cluster.rb b/lib/moped/cluster.rb index 4edf901..6d9321c 100644 --- a/lib/moped/cluster.rb +++ b/lib/moped/cluster.rb @@ -293,7 +293,7 @@ def with_secondary(&block) def with_retry(retries = max_retries, &block) begin block.call - rescue Errors::ConnectionFailure => e + rescue Errors::ConnectionFailure, Errors::ReplicaSetReconfigured, Errors::AuthorizationFailure => e if retries > 0 Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s).", "n/a") sleep(retry_interval) diff --git a/lib/moped/errors.rb b/lib/moped/errors.rb index c4c27b7..acf8a52 100644 --- a/lib/moped/errors.rb +++ b/lib/moped/errors.rb @@ -20,6 +20,7 @@ class PoolTimeout < RuntimeError; end # Generic error class for exceptions related to connection failures. class ConnectionFailure < StandardError; end + # Raised when a database name is invalid. class InvalidDatabaseName < StandardError; end @@ -28,7 +29,6 @@ class InvalidMongoURI < StandardError; end # Raised when providing an invalid string from an object id. class InvalidObjectId < StandardError - # Create the new error. # # @example Create the new error. @@ -42,6 +42,8 @@ def initialize(string) end end + + # Generic error class for exceptions generated on the remote MongoDB # server. class MongoError < StandardError @@ -105,29 +107,22 @@ def error_message end end - # Classes of errors that should not disconnect connections. - class DoNotDisconnect < MongoError; end - - # Classes of errors that could be caused by a replica set reconfiguration. - class PotentialReconfiguration < MongoError + # @api private + # + # Internal exception raised by Node#ensure_primary and captured by + # Cluster#with_primary. + class ReplicaSetReconfigured < MongoError; end - # Not master error codes. - NOT_MASTER = [ 13435, 13436, 10009 ] + class NotMaster < ReplicaSetReconfigured; end - # Error codes received around reconfiguration - CONNECTION_ERRORS_RECONFIGURATION = [ 15988, 10276, 11600, 9001, 13639, 10009 ] + # Exception raised when authentication fails. + class AuthenticationFailure < MongoError; end - # Replica set reconfigurations can be either in the form of an operation - # error with code 13435, or with an error message stating the server is - # not a master. (This encapsulates codes 10054, 10056, 10058) - def reconfiguring_replica_set? - err = details["err"] || details["errmsg"] || details["$err"] || "" - NOT_MASTER.include?(details["code"]) || err.include?("not master") - end + # Exception raised when authorization fails. + class AuthorizationFailure < MongoError; end - def connection_failure? - CONNECTION_ERRORS_RECONFIGURATION.include?(details["code"]) - end + # Classes of errors that could be caused by a replica set reconfiguration. + class OperationFailure < MongoError # Is the error due to a namespace not being found? # @@ -154,28 +149,17 @@ def ns_not_exists? end end - # Exception raised when authentication fails. - class AuthenticationFailure < DoNotDisconnect; end - - # Exception class for exceptions generated as a direct result of an - # operation, such as a failed insert or an invalid command. - class OperationFailure < PotentialReconfiguration; end # Exception raised on invalid queries. - class QueryFailure < PotentialReconfiguration; end + class QueryFailure < MongoError; end # Exception raised if the cursor could not be found. - class CursorNotFound < DoNotDisconnect + class CursorNotFound < MongoError def initialize(operation, cursor_id) super(operation, {"errmsg" => "cursor #{cursor_id} not found"}) end end - # @api private - # - # Internal exception raised by Node#ensure_primary and captured by - # Cluster#with_primary. - class ReplicaSetReconfigured < DoNotDisconnect; end # Tag applied to unhandled exceptions on a node. module SocketError; end diff --git a/lib/moped/failover.rb b/lib/moped/failover.rb index d893adc..014a674 100644 --- a/lib/moped/failover.rb +++ b/lib/moped/failover.rb @@ -1,7 +1,6 @@ # encoding: utf-8 require "moped/failover/disconnect" require "moped/failover/ignore" -require "moped/failover/reconfigure" require "moped/failover/retry" module Moped @@ -18,10 +17,12 @@ module Failover # @since 2.0.0 STRATEGIES = { Errors::AuthenticationFailure => Ignore, + Errors::AuthorizationFailure => Retry, Errors::ConnectionFailure => Retry, Errors::CursorNotFound => Ignore, - Errors::OperationFailure => Reconfigure, - Errors::QueryFailure => Reconfigure + Errors::OperationFailure => Ignore, + Errors::QueryFailure => Ignore, + Errors::NotMaster => Retry, }.freeze # Get the appropriate failover handler given the provided exception. diff --git a/lib/moped/failover/reconfigure.rb b/lib/moped/failover/reconfigure.rb deleted file mode 100644 index babe071..0000000 --- a/lib/moped/failover/reconfigure.rb +++ /dev/null @@ -1,36 +0,0 @@ -# encoding: utf-8 -module Moped - module Failover - - # Reconfigure is for exceptions that indicate that a replica set was - # potentially reconfigured in the middle of an operation. - # - # @since 2.0.0 - module Reconfigure - extend self - - # Executes the failover strategy. In the case of reconfigure, we check if - # the failure was due to a replica set reconfiguration mid operation and - # raise a new error if appropriate. - # - # @example Execute the reconfigure strategy. - # Moped::Failover::Reconfigure.execute(exception, node) - # - # @param [ Exception ] exception The raised exception. - # @param [ Node ] node The node the exception got raised on. - # - # @raise [ Exception, Errors::ReplicaSetReconfigure ] The exception that - # was previously thrown or a reconfiguration error. - # - # @since 2.0.0 - def execute(exception, node) - if exception.reconfiguring_replica_set? - raise(Errors::ReplicaSetReconfigured.new(exception.command, exception.details)) - elsif exception.connection_failure? - raise Errors::ConnectionFailure.new(exception.inspect) - end - raise(exception) - end - end - end -end diff --git a/lib/moped/node.rb b/lib/moped/node.rb index 0ede953..a48150a 100644 --- a/lib/moped/node.rb +++ b/lib/moped/node.rb @@ -169,16 +169,16 @@ def down! # # @since 1.0.0 def ensure_connected(&block) - unless (conn = stack(:connection)).empty? - return yield(conn.first) - end - begin - connection do |conn| - stack(:connection) << conn - connect(conn) unless conn.connected? - conn.apply_credentials(@credentials) - yield(conn) + unless (conn = stack(:connection)).empty? + yield(conn.first) + else + connection do |conn| + stack(:connection) << conn + connect(conn) unless conn.connected? + conn.apply_credentials(@credentials) + yield(conn) + end end rescue Exception => e Failover.get(e).execute(e, self, &block) @@ -585,14 +585,13 @@ def discover(*nodes) def flush(ops = queue) operations, callbacks = ops.transpose logging(operations) do - replies = nil ensure_connected do |conn| conn.write(operations) replies = conn.receive_replies(operations) + replies.zip(callbacks).map do |reply, callback| + callback ? callback[reply] : reply + end.last end - replies.zip(callbacks).map do |reply, callback| - callback ? callback[reply] : reply - end.last end ensure ops.clear diff --git a/lib/moped/protocol/command.rb b/lib/moped/protocol/command.rb index 8c0461c..9925cf2 100644 --- a/lib/moped/protocol/command.rb +++ b/lib/moped/protocol/command.rb @@ -37,7 +37,7 @@ def failure?(reply) # # @since 2.0.0 def failure_exception(reply) - Errors::OperationFailure.new(self, reply.documents.first) + reply.failure_exception || Errors::OperationFailure.new(self, reply.documents.first) end # Instantiate the new command. diff --git a/lib/moped/protocol/get_more.rb b/lib/moped/protocol/get_more.rb index 78710b4..7ec68e2 100644 --- a/lib/moped/protocol/get_more.rb +++ b/lib/moped/protocol/get_more.rb @@ -69,11 +69,8 @@ def failure?(reply) # # @since 2.0.0 def failure_exception(reply) - if reply.cursor_not_found? - Errors::CursorNotFound.new(self, cursor_id) - else - Errors::QueryFailure.new(self, reply.documents.first) - end + return Errors::CursorNotFound.new(self, cursor_id) if reply.cursor_not_found? + reply.failure_exception || Errors::QueryFailure.new(self, reply.documents.first) end # Create a new GetMore command. The database and collection arguments diff --git a/lib/moped/protocol/query.rb b/lib/moped/protocol/query.rb index ac455fc..e4788d2 100644 --- a/lib/moped/protocol/query.rb +++ b/lib/moped/protocol/query.rb @@ -91,7 +91,7 @@ def basic_selector # # @since 2.0.0 def failure_exception(reply) - Errors::QueryFailure.new(self, reply.documents.first) + reply.failure_exception || Errors::QueryFailure.new(self, reply.documents.first) end # Determine if the provided reply message is a failure with respect to a diff --git a/lib/moped/protocol/reply.rb b/lib/moped/protocol/reply.rb index e3797b7..f60300c 100644 --- a/lib/moped/protocol/reply.rb +++ b/lib/moped/protocol/reply.rb @@ -13,8 +13,11 @@ module Protocol class Reply include Message - # Unauthorized assertion errors. - UNAUTHORIZED = [ 10057, 16550 ] + # Error codes + UNAUTHORIZED = [ 13, 10057, 16550, 16544 ] + NOT_MASTER = [ 13435, 13436, 10009, 10058 ] + CONNECTION_ERRORS_RECONFIGURATION = [ 15988, 10276, 11600, 9001, 13639, 10009 ] + # @attribute # @return [Number] the length of the message @@ -71,6 +74,30 @@ def command_failure? (result["ok"] != 1.0 && result["ok"] != true) || error? end + def failure_exception + return Errors::AuthorizationFailure.new(self, documents.first) if unauthorized? + return Errors::NotMaster.new(self, documents.first) if not_master? + return Errors::ReplicaSetReconfigured.new(self, documents.first) if connection_failure? + end + + # Error codes received around reconfiguration + def connection_failure? + result = documents[0] + return false if result.nil? + CONNECTION_ERRORS_RECONFIGURATION.include?(result["code"]) + end + + # Not master error codes. + # Replica set reconfigurations can be either in the form of an operation + # error with code 13435, or with an error message stating the server is + # not a master. (This encapsulates codes 10054, 10056, 10058) + def not_master? + result = documents[0] + return false if result.nil? + err = error_message(result) + NOT_MASTER.include?(result["code"]) || err.include?("not master") + end + # Was the provided cursor id not found on the server? # # @example Is the cursor not on the server? From 8edebd76958407f138897de44c4b515f53db6b38 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 8 Sep 2014 15:51:40 +0200 Subject: [PATCH 04/15] Cleanup --- lib/moped/errors.rb | 11 +++-------- lib/moped/node.rb | 2 +- lib/moped/protocol/get_more.rb | 1 - lib/moped/protocol/reply.rb | 8 +++++--- lib/moped/read_preference/selectable.rb | 3 --- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/moped/errors.rb b/lib/moped/errors.rb index acf8a52..7354e80 100644 --- a/lib/moped/errors.rb +++ b/lib/moped/errors.rb @@ -20,7 +20,6 @@ class PoolTimeout < RuntimeError; end # Generic error class for exceptions related to connection failures. class ConnectionFailure < StandardError; end - # Raised when a database name is invalid. class InvalidDatabaseName < StandardError; end @@ -42,8 +41,6 @@ def initialize(string) end end - - # Generic error class for exceptions generated on the remote MongoDB # server. class MongoError < StandardError @@ -109,10 +106,10 @@ def error_message # @api private # - # Internal exception raised by Node#ensure_primary and captured by - # Cluster#with_primary. + # Exception indicating that replica set was most likely reconfigured class ReplicaSetReconfigured < MongoError; end + # Exception raised when database responds with 'not master' error class NotMaster < ReplicaSetReconfigured; end # Exception raised when authentication fails. @@ -121,7 +118,7 @@ class AuthenticationFailure < MongoError; end # Exception raised when authorization fails. class AuthorizationFailure < MongoError; end - # Classes of errors that could be caused by a replica set reconfiguration. + # Exception raised when operation fails class OperationFailure < MongoError # Is the error due to a namespace not being found? @@ -149,7 +146,6 @@ def ns_not_exists? end end - # Exception raised on invalid queries. class QueryFailure < MongoError; end @@ -160,7 +156,6 @@ def initialize(operation, cursor_id) end end - # Tag applied to unhandled exceptions on a node. module SocketError; end end diff --git a/lib/moped/node.rb b/lib/moped/node.rb index a48150a..2250d4e 100644 --- a/lib/moped/node.rb +++ b/lib/moped/node.rb @@ -589,7 +589,7 @@ def flush(ops = queue) conn.write(operations) replies = conn.receive_replies(operations) replies.zip(callbacks).map do |reply, callback| - callback ? callback[reply] : reply + callback ? callback.call(reply) : reply end.last end end diff --git a/lib/moped/protocol/get_more.rb b/lib/moped/protocol/get_more.rb index 7ec68e2..dde9cd1 100644 --- a/lib/moped/protocol/get_more.rb +++ b/lib/moped/protocol/get_more.rb @@ -69,7 +69,6 @@ def failure?(reply) # # @since 2.0.0 def failure_exception(reply) - return Errors::CursorNotFound.new(self, cursor_id) if reply.cursor_not_found? reply.failure_exception || Errors::QueryFailure.new(self, reply.documents.first) end diff --git a/lib/moped/protocol/reply.rb b/lib/moped/protocol/reply.rb index f60300c..535a21b 100644 --- a/lib/moped/protocol/reply.rb +++ b/lib/moped/protocol/reply.rb @@ -15,7 +15,7 @@ class Reply # Error codes UNAUTHORIZED = [ 13, 10057, 16550, 16544 ] - NOT_MASTER = [ 13435, 13436, 10009, 10058 ] + NOT_MASTER = [ 13435, 13436, 10009, 10054, 10056, 10058 ] CONNECTION_ERRORS_RECONFIGURATION = [ 15988, 10276, 11600, 9001, 13639, 10009 ] @@ -74,10 +74,12 @@ def command_failure? (result["ok"] != 1.0 && result["ok"] != true) || error? end + # Returns specific exception if it can be determined from error code or message returned by DB def failure_exception return Errors::AuthorizationFailure.new(self, documents.first) if unauthorized? return Errors::NotMaster.new(self, documents.first) if not_master? return Errors::ReplicaSetReconfigured.new(self, documents.first) if connection_failure? + return Errors::CursorNotFound.new(self, cursor_id) if reply.cursor_not_found? end # Error codes received around reconfiguration @@ -89,8 +91,8 @@ def connection_failure? # Not master error codes. # Replica set reconfigurations can be either in the form of an operation - # error with code 13435, or with an error message stating the server is - # not a master. (This encapsulates codes 10054, 10056, 10058) + # error falling into ReplicaSetReconfigured error or with an error message stating the server is + # not a master. def not_master? result = documents[0] return false if result.nil? diff --git a/lib/moped/read_preference/selectable.rb b/lib/moped/read_preference/selectable.rb index 681b82c..03be491 100644 --- a/lib/moped/read_preference/selectable.rb +++ b/lib/moped/read_preference/selectable.rb @@ -39,9 +39,6 @@ def query_options(options) options[:flags] |= [ :slave_ok ] options end - - private - end end end From 57ce75c68003f7ef9c8f87b7e7db52cb2cfc9aac Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 8 Sep 2014 16:37:59 +0200 Subject: [PATCH 05/15] Cleanup + fixing specs --- lib/moped/failover/retry.rb | 1 - lib/moped/protocol/get_more.rb | 1 + lib/moped/protocol/reply.rb | 3 +- spec/moped/errors_spec.rb | 108 ------------------------ spec/moped/failover/reconfigure_spec.rb | 50 ----------- spec/moped/failover_spec.rb | 28 +++++- spec/moped/node_spec.rb | 42 +++++---- 7 files changed, 52 insertions(+), 181 deletions(-) delete mode 100644 spec/moped/failover/reconfigure_spec.rb diff --git a/lib/moped/failover/retry.rb b/lib/moped/failover/retry.rb index bb8b091..dec4d1a 100644 --- a/lib/moped/failover/retry.rb +++ b/lib/moped/failover/retry.rb @@ -24,7 +24,6 @@ module Retry # # @since 2.0.0 def execute(exception, node) - node.disconnect begin node.connection do |conn| yield(conn) if block_given? diff --git a/lib/moped/protocol/get_more.rb b/lib/moped/protocol/get_more.rb index dde9cd1..7ec68e2 100644 --- a/lib/moped/protocol/get_more.rb +++ b/lib/moped/protocol/get_more.rb @@ -69,6 +69,7 @@ def failure?(reply) # # @since 2.0.0 def failure_exception(reply) + return Errors::CursorNotFound.new(self, cursor_id) if reply.cursor_not_found? reply.failure_exception || Errors::QueryFailure.new(self, reply.documents.first) end diff --git a/lib/moped/protocol/reply.rb b/lib/moped/protocol/reply.rb index 535a21b..9eeca71 100644 --- a/lib/moped/protocol/reply.rb +++ b/lib/moped/protocol/reply.rb @@ -79,7 +79,6 @@ def failure_exception return Errors::AuthorizationFailure.new(self, documents.first) if unauthorized? return Errors::NotMaster.new(self, documents.first) if not_master? return Errors::ReplicaSetReconfigured.new(self, documents.first) if connection_failure? - return Errors::CursorNotFound.new(self, cursor_id) if reply.cursor_not_found? end # Error codes received around reconfiguration @@ -97,7 +96,7 @@ def not_master? result = documents[0] return false if result.nil? err = error_message(result) - NOT_MASTER.include?(result["code"]) || err.include?("not master") + NOT_MASTER.include?(result["code"]) || err && err.include?("not master") end # Was the provided cursor id not found on the server? diff --git a/spec/moped/errors_spec.rb b/spec/moped/errors_spec.rb index 6558490..ea9a64f 100644 --- a/spec/moped/errors_spec.rb +++ b/spec/moped/errors_spec.rb @@ -31,112 +31,4 @@ end end end - - describe "#reconfiguring_replica_set?" do - - context "when error code 13435" do - - let(:details) do - { "code" => 13435 } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when error code 10009" do - - let(:details) do - { "code" => 10009 } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when error code 13436" do - - let(:details) do - { "code" => 13436 } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when 'err' is not master" do - - let(:details) do - { "err" => "not master" } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when 'errmsg' is not master" do - - let(:details) do - { "errmsg" => "not master" } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when 'err' contains not master" do - - let(:details) do - { "errmsg" => "not master or secondary; cannot currently read from this replSet member" } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns true" do - error.should be_reconfiguring_replica_set - end - end - - context "when errors are not matching not master" do - - let(:details) do - { "errmsg" => "unauthorized" } - end - - let(:error) do - Moped::Errors::PotentialReconfiguration.new({}, details) - end - - it "returns false" do - error.should_not be_reconfiguring_replica_set - end - end - end end diff --git a/spec/moped/failover/reconfigure_spec.rb b/spec/moped/failover/reconfigure_spec.rb deleted file mode 100644 index 64bc3d7..0000000 --- a/spec/moped/failover/reconfigure_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require "spec_helper" - -describe Moped::Failover::Reconfigure do - - describe "#execute" do - - let(:node) do - Moped::Node.new("127.0.0.1:27017") - end - - context "when the exception is reconfiguring a replica set" do - - let(:exception) do - Moped::Errors::QueryFailure.new({}, { "code" => 13435 }) - end - - it "raises a replica set reconfigured exception" do - expect { - described_class.execute(exception, node) - }.to raise_error(Moped::Errors::ReplicaSetReconfigured) - end - end - - context "when the exception is a conncetion failure with the server" do - - let(:exception) do - Moped::Errors::QueryFailure.new({}, { "code" => 15988 }) - end - - it "raises a connection failure exception" do - expect { - described_class.execute(exception, node) - }.to raise_error(Moped::Errors::ConnectionFailure) - end - end - - context "when no replica set reconfiguration is happening" do - - let(:exception) do - Moped::Errors::QueryFailure.new({}, {}) - end - - it "raises the exception" do - expect { - described_class.execute(exception, node) - }.to raise_error(exception.class) - end - end - end -end diff --git a/spec/moped/failover_spec.rb b/spec/moped/failover_spec.rb index ff0833e..4dc0f29 100644 --- a/spec/moped/failover_spec.rb +++ b/spec/moped/failover_spec.rb @@ -23,8 +23,8 @@ described_class.get(Moped::Errors::OperationFailure.new({}, {})) end - it "returns a reconfigure" do - expect(failover).to be_a(Moped::Failover::Reconfigure) + it "returns a ignore" do + expect(failover).to be_a(Moped::Failover::Ignore) end end @@ -34,8 +34,8 @@ described_class.get(Moped::Errors::QueryFailure.new({}, {})) end - it "returns a reconfigure" do - expect(failover).to be_a(Moped::Failover::Reconfigure) + it "returns a ignore" do + expect(failover).to be_a(Moped::Failover::Ignore) end end @@ -71,6 +71,26 @@ expect(failover).to be_a(Moped::Failover::Retry) end end + + context "when providing an authorization failure" do + let(:failover) do + described_class.get(Moped::Errors::AuthorizationFailure.new({}, {})) + end + + it "returns a retry" do + expect(failover).to be_a(Moped::Failover::Retry) + end + end + + context "when providing a not-master failure" do + let(:failover) do + described_class.get(Moped::Errors::NotMaster.new({}, {})) + end + + it "returns a retry" do + expect(failover).to be_a(Moped::Failover::Retry) + end + end end end end diff --git a/spec/moped/node_spec.rb b/spec/moped/node_spec.rb index ee641cb..e6c2af6 100644 --- a/spec/moped/node_spec.rb +++ b/spec/moped/node_spec.rb @@ -230,40 +230,50 @@ context "when there is a reconfiguration" do - let(:potential_reconfiguration_error) do - Moped::Errors::QueryFailure.new("", {}) - end + let(:error) { Moped::Errors::QueryFailure.new("", {}) } before do node.connection do |conn| conn.stub(:connected?).and_return(false) - conn.stub(:connect).and_raise(potential_reconfiguration_error) + conn.stub(:connect).and_raise(error) end end - context "and the reconfigation is of a replica set" do - before do - potential_reconfiguration_error.stub(:reconfiguring_replica_set?).and_return(true) + + context "and the cluster returns 'not master' error" do + + let(:error) { Moped::Errors::NotMaster.new("", {}) } + + it "it retries and reraises same error" do + expect { + node.ensure_connected { raise error } + }.to raise_error(error.class) end - it "raises a ReplicaSetReconfigured error" do + it "it disconnects" do + expect(node).to receive(:disconnect) expect { - node.ensure_connected {} - }.to raise_error(Moped::Errors::ReplicaSetReconfigured) + node.ensure_connected { raise error } + }.to raise_error end end - context "and the reconfigation is not of a replica set" do + context "and the cluster returns authorization error" do - before do - potential_reconfiguration_error.stub(:reconfiguring_replica_set?).and_return(false) + let(:error) { Moped::Errors::AuthorizationFailure.new("", {}) } + + it "it retries and reraises same error" do + expect { + node.ensure_connected { raise error } + }.to raise_error(error.class) end - it "raises a PotentialReconfiguration error" do + it "it disconnects" do + expect(node).to receive(:disconnect) expect { - node.ensure_connected {} - }.to raise_error(Moped::Errors::PotentialReconfiguration) + node.ensure_connected { raise error } + }.to raise_error end end end From 977d776147c171a0879ab2441b34b1399219095d Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 8 Sep 2014 17:03:54 +0200 Subject: [PATCH 06/15] Fixed read specs --- spec/moped/operation/read_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/moped/operation/read_spec.rb b/spec/moped/operation/read_spec.rb index 1d7e4b2..3bb5300 100644 --- a/spec/moped/operation/read_spec.rb +++ b/spec/moped/operation/read_spec.rb @@ -39,10 +39,10 @@ replica_set_node.unauthorized_on_next_message! end - it "raises a failure error" do + it "does not raise a failure error (it retries and succeeds)" do expect { read.execute(node) - }.to raise_error(exception) + }.to_not raise_error end end end From a11bb3acbef6f2f57c73069a735a2a2370ee5d62 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Tue, 9 Sep 2014 11:11:16 +0200 Subject: [PATCH 07/15] Refreshed node is not down! --- lib/moped/cluster.rb | 9 +++------ lib/moped/failover/retry.rb | 3 ++- lib/moped/node.rb | 1 + lib/moped/protocol/reply.rb | 19 ++++++++++++++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/moped/cluster.rb b/lib/moped/cluster.rb index 6d9321c..a3e094b 100644 --- a/lib/moped/cluster.rb +++ b/lib/moped/cluster.rb @@ -238,11 +238,8 @@ def retry_interval # @since 1.0.0 def with_primary(&block) if node = nodes.find(&:primary?) - begin - node.ensure_primary do - return yield(node) - end - rescue Errors::ConnectionFailure, Errors::ReplicaSetReconfigured + node.ensure_primary do + return yield(node) end end raise Errors::ConnectionFailure, "Could not connect to a primary node for replica set #{inspect}" @@ -295,7 +292,7 @@ def with_retry(retries = max_retries, &block) block.call rescue Errors::ConnectionFailure, Errors::ReplicaSetReconfigured, Errors::AuthorizationFailure => e if retries > 0 - Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s).", "n/a") + Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s). Exception: #{ e.class.name }, #{ e.message }", "n/a") sleep(retry_interval) refresh with_retry(retries - 1, &block) diff --git a/lib/moped/failover/retry.rb b/lib/moped/failover/retry.rb index dec4d1a..45c3448 100644 --- a/lib/moped/failover/retry.rb +++ b/lib/moped/failover/retry.rb @@ -9,7 +9,7 @@ module Failover module Retry extend self - # Executes the failover strategy. In the case of retyr, we disconnect and + # Executes the failover strategy. In the case of retry, we disconnect and # reconnect, then try the operation one more time. # # @example Execute the retry strategy. @@ -24,6 +24,7 @@ module Retry # # @since 2.0.0 def execute(exception, node) + node.disconnect begin node.connection do |conn| yield(conn) if block_given? diff --git a/lib/moped/node.rb b/lib/moped/node.rb index 2250d4e..ae0e6f9 100644 --- a/lib/moped/node.rb +++ b/lib/moped/node.rb @@ -548,6 +548,7 @@ def configure(settings) @arbiter = settings["arbiterOnly"] @passive = settings["passive"] @primary = settings["ismaster"] + @down_at = nil @secondary = settings["secondary"] discover(settings["hosts"]) if auto_discovering? end diff --git a/lib/moped/protocol/reply.rb b/lib/moped/protocol/reply.rb index 9eeca71..5378cc7 100644 --- a/lib/moped/protocol/reply.rb +++ b/lib/moped/protocol/reply.rb @@ -14,9 +14,22 @@ class Reply include Message # Error codes - UNAUTHORIZED = [ 13, 10057, 16550, 16544 ] - NOT_MASTER = [ 13435, 13436, 10009, 10054, 10056, 10058 ] - CONNECTION_ERRORS_RECONFIGURATION = [ 15988, 10276, 11600, 9001, 13639, 10009 ] + UNAUTHORIZED = [ + 13, # not authorized for query on ... + 10057, + 16550, # not authorized for query on ... + 16544, # not authorized for insert on ... + ] + NOT_MASTER = [ 13435, 13436, 10009, 10054, 10056, 10058, 10107] + + CONNECTION_ERRORS_RECONFIGURATION = [ + 9001, + 10009, + 10276, # DBClientBase::findN: transport error + 11600, # interrupted at shutdown + 13639, # can't connect to new replica set master + 15988, # error querying server + ] # @attribute From 455a69de6fbfcc9b16d7942a1f8660bf19713aca Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Tue, 9 Sep 2014 11:14:52 +0200 Subject: [PATCH 08/15] Fix node spec --- spec/moped/node_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/moped/node_spec.rb b/spec/moped/node_spec.rb index e6c2af6..9853cef 100644 --- a/spec/moped/node_spec.rb +++ b/spec/moped/node_spec.rb @@ -252,7 +252,7 @@ end it "it disconnects" do - expect(node).to receive(:disconnect) + expect(node).to receive(:disconnect).at_least(:once) expect { node.ensure_connected { raise error } }.to raise_error @@ -270,7 +270,7 @@ end it "it disconnects" do - expect(node).to receive(:disconnect) + expect(node).to receive(:disconnect).at_least(:once) expect { node.ensure_connected { raise error } }.to raise_error From cc7992055889db13c1dddbf8260d2efa34e5950c Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Tue, 9 Sep 2014 11:40:45 +0200 Subject: [PATCH 09/15] Dont change Ruby version --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index eca07e4..3e3c2f1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.2 +2.1.1 From e41100fb2c42cbfc487aa3eb1f1ee99fb8eac3bd Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 22 Sep 2014 13:44:08 +0100 Subject: [PATCH 10/15] Cluster#with_retry using Failover::STRATEGIES --- lib/moped/cluster.rb | 3 ++- lib/moped/failover.rb | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/moped/cluster.rb b/lib/moped/cluster.rb index a3e094b..22b5db8 100644 --- a/lib/moped/cluster.rb +++ b/lib/moped/cluster.rb @@ -290,7 +290,8 @@ def with_secondary(&block) def with_retry(retries = max_retries, &block) begin block.call - rescue Errors::ConnectionFailure, Errors::ReplicaSetReconfigured, Errors::AuthorizationFailure => e + rescue StandardError => e + raise e unless Failover::STRATEGIES[e.class] == Failover::Retry if retries > 0 Loggable.warn(" MOPED:", "Retrying connection attempt #{retries} more time(s). Exception: #{ e.class.name }, #{ e.message }", "n/a") sleep(retry_interval) diff --git a/lib/moped/failover.rb b/lib/moped/failover.rb index 014a674..087812c 100644 --- a/lib/moped/failover.rb +++ b/lib/moped/failover.rb @@ -23,6 +23,7 @@ module Failover Errors::OperationFailure => Ignore, Errors::QueryFailure => Ignore, Errors::NotMaster => Retry, + Errors::ReplicaSetReconfigured => Retry, }.freeze # Get the appropriate failover handler given the provided exception. From a81effef5c4473aa141fe0ff269e647f8bb165a4 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 22 Sep 2014 14:00:59 +0100 Subject: [PATCH 11/15] Fixing Typo --- spec/moped/failover_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/moped/failover_spec.rb b/spec/moped/failover_spec.rb index 4dc0f29..41c4508 100644 --- a/spec/moped/failover_spec.rb +++ b/spec/moped/failover_spec.rb @@ -23,7 +23,7 @@ described_class.get(Moped::Errors::OperationFailure.new({}, {})) end - it "returns a ignore" do + it "returns an ignore" do expect(failover).to be_a(Moped::Failover::Ignore) end end @@ -34,7 +34,7 @@ described_class.get(Moped::Errors::QueryFailure.new({}, {})) end - it "returns a ignore" do + it "returns an ignore" do expect(failover).to be_a(Moped::Failover::Ignore) end end From 827bedc74b572f734de4b2d63703782fc7ae1109 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 22 Sep 2014 14:10:58 +0100 Subject: [PATCH 12/15] Dont do unless .. else .. --- lib/moped/node.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/moped/node.rb b/lib/moped/node.rb index ae0e6f9..62930e2 100644 --- a/lib/moped/node.rb +++ b/lib/moped/node.rb @@ -170,7 +170,7 @@ def down! # @since 1.0.0 def ensure_connected(&block) begin - unless (conn = stack(:connection)).empty? + if (conn = stack(:connection)).length > 0 yield(conn.first) else connection do |conn| From 2be410c668f1829c3910975d71c4afb470b9e1cf Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Mon, 22 Sep 2014 15:17:21 +0100 Subject: [PATCH 13/15] Additional specs --- spec/moped/cluster_spec.rb | 47 +++++++++++++++++++++++++++++++ spec/moped/node_spec.rb | 20 +++++++------ spec/moped/operation/read_spec.rb | 2 +- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/spec/moped/cluster_spec.rb b/spec/moped/cluster_spec.rb index f51111c..18bf54e 100644 --- a/spec/moped/cluster_spec.rb +++ b/spec/moped/cluster_spec.rb @@ -54,6 +54,29 @@ end.should raise_exception(Moped::Errors::ConnectionFailure) end end + + describe '#with_retry' do + it 'retries' do + cluster.should_receive(:with_retry).twice.and_call_original + lambda do + cluster.with_retry do + cluster.with_secondary do |node| + node.command("admin", ping: 1) + end + end + end.should raise_exception(Moped::Errors::ConnectionFailure) + end + + it 'raises connection error' do + lambda do + cluster.with_retry do + cluster.with_secondary do |node| + node.command("admin", ping: 1) + end + end + end.should raise_exception(Moped::Errors::ConnectionFailure) + end + end end context "when the replica set hasn't connected yet" do @@ -95,6 +118,19 @@ end end.should raise_exception(Moped::Errors::ConnectionFailure) end + + context 'with_retry' do + it 'raises connection error after retrying' do + cluster.should_receive(:with_retry).twice.and_call_original + lambda do + cluster.with_retry do + cluster.with_primary do |node| + node.command("admin", ping: 1) + end + end + end.should raise_exception(Moped::Errors::ConnectionFailure) + end + end end describe "#with_secondary" do @@ -104,6 +140,17 @@ @secondaries.map(&:address).should include node.address.original end end + + context 'with_retry' do + it 'does not retry' do + cluster.should_receive(:with_retry).once.and_call_original + cluster.with_retry do + cluster.with_secondary do |node| + node.command("admin", ping: 1) + end + end + end + end end end diff --git a/spec/moped/node_spec.rb b/spec/moped/node_spec.rb index 9853cef..c1b4217 100644 --- a/spec/moped/node_spec.rb +++ b/spec/moped/node_spec.rb @@ -369,12 +369,16 @@ describe "#refresh" do - context "when the ismaster command fails" do + let(:node) do + described_class.new("127.0.0.1:27017") + end - let(:node) do - described_class.new("127.0.0.1:27017") - end + it 'marks node as not down any more when it succeeds to refresh' do + node.down! + expect{ node.refresh }.to change{ node.down? }.to(nil) + end + context "when the ismaster command fails" do before do node.should_receive(:command).with("admin", ismaster: 1).and_raise(Timeout::Error) node.refresh @@ -383,14 +387,14 @@ it "still sets the refresh time" do expect(node.refreshed_at).to_not be_nil end + + it 'keeps node down' do + expect(node).to be_down + end end context "when the node has authentication details" do - let(:node) do - described_class.new("127.0.0.1:27017") - end - before do node.credentials["moped_test"] = [ "user", "pass" ] end diff --git a/spec/moped/operation/read_spec.rb b/spec/moped/operation/read_spec.rb index 3bb5300..44cf274 100644 --- a/spec/moped/operation/read_spec.rb +++ b/spec/moped/operation/read_spec.rb @@ -39,7 +39,7 @@ replica_set_node.unauthorized_on_next_message! end - it "does not raise a failure error (it retries and succeeds)" do + it "does not raise a failure error (as it retries and succeeds)" do expect { read.execute(node) }.to_not raise_error From edd9eedc0738496c72ae75456d97ee4a9c34d14a Mon Sep 17 00:00:00 2001 From: Wandenberg Date: Sat, 20 Sep 2014 20:53:00 -0300 Subject: [PATCH 14/15] try to do tests more stable --- spec/support/replica_set_simulator.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/support/replica_set_simulator.rb b/spec/support/replica_set_simulator.rb index af6d30b..6808f5e 100644 --- a/spec/support/replica_set_simulator.rb +++ b/spec/support/replica_set_simulator.rb @@ -357,7 +357,13 @@ def next_client servers.each do |server| Moped.logger.debug "replica_set: accepting new client for #{server.port}" - @clients << server.accept + begin + @clients << server.accept + rescue IOError, Errno::EBADF, TypeError + # Looks like we hit a bad file descriptor or closed connection. + Moped.logger.debug "replica_set: io error, retrying" + retry + end end Moped.logger.debug "replica_set: closing dead clients" From 4c2a0a29122e585c4a5dea24a46ca1684bc97ed1 Mon Sep 17 00:00:00 2001 From: Dawid Sklodowski Date: Tue, 23 Sep 2014 13:47:03 +0100 Subject: [PATCH 15/15] Revert "try to do tests more stable" This reverts commit edd9eedc0738496c72ae75456d97ee4a9c34d14a. --- spec/support/replica_set_simulator.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/support/replica_set_simulator.rb b/spec/support/replica_set_simulator.rb index 6808f5e..af6d30b 100644 --- a/spec/support/replica_set_simulator.rb +++ b/spec/support/replica_set_simulator.rb @@ -357,13 +357,7 @@ def next_client servers.each do |server| Moped.logger.debug "replica_set: accepting new client for #{server.port}" - begin - @clients << server.accept - rescue IOError, Errno::EBADF, TypeError - # Looks like we hit a bad file descriptor or closed connection. - Moped.logger.debug "replica_set: io error, retrying" - retry - end + @clients << server.accept end Moped.logger.debug "replica_set: closing dead clients"