@@ -1195,9 +1195,11 @@ def _get_topology(self):
11951195
11961196 @contextlib .contextmanager
11971197 def _get_socket (self , server , session , exhaust = False ):
1198- with self ._reset_on_error (server .description .address , session ):
1199- with server .get_socket (self .__all_credentials ,
1200- checkout = exhaust ) as sock_info :
1198+ with _MongoClientErrorHandler (
1199+ self , server .description .address , session ) as err_handler :
1200+ with server .get_socket (
1201+ self .__all_credentials , checkout = exhaust ) as sock_info :
1202+ err_handler .contribute_socket (sock_info )
12011203 yield sock_info
12021204
12031205 def _select_server (self , server_selector , session , address = None ):
@@ -1289,8 +1291,10 @@ def _run_operation_with_response(self, operation, unpack_res,
12891291 server = self ._select_server (
12901292 operation .read_preference , operation .session , address = address )
12911293
1292- with self ._reset_on_error (server .description .address ,
1293- operation .session ):
1294+ with _MongoClientErrorHandler (
1295+ self , server .description .address ,
1296+ operation .session ) as err_handler :
1297+ err_handler .contribute_socket (operation .exhaust_mgr .sock )
12941298 return server .run_operation_with_response (
12951299 operation .exhaust_mgr .sock ,
12961300 operation ,
@@ -1314,49 +1318,6 @@ def _cmd(session, server, sock_info, slave_ok):
13141318 retryable = isinstance (operation , message ._Query ),
13151319 exhaust = exhaust )
13161320
1317- @contextlib .contextmanager
1318- def _reset_on_error (self , server_address , session ):
1319- """On "not master" or "node is recovering" errors reset the server
1320- according to the SDAM spec.
1321-
1322- Unpin the session on transient transaction errors.
1323- """
1324- try :
1325- try :
1326- yield
1327- except PyMongoError as exc :
1328- if session and exc .has_error_label (
1329- "TransientTransactionError" ):
1330- session ._unpin_mongos ()
1331- raise
1332- except NetworkTimeout :
1333- # The socket has been closed. Don't reset the server.
1334- # Server Discovery And Monitoring Spec: "When an application
1335- # operation fails because of any network error besides a socket
1336- # timeout...."
1337- if session :
1338- session ._server_session .mark_dirty ()
1339- raise
1340- except NotMasterError :
1341- # "When the client sees a "not master" error it MUST replace the
1342- # server's description with type Unknown. It MUST request an
1343- # immediate check of the server."
1344- self ._reset_server_and_request_check (server_address )
1345- raise
1346- except ConnectionFailure :
1347- # "Client MUST replace the server's description with type Unknown
1348- # ... MUST NOT request an immediate check of the server."
1349- self .__reset_server (server_address )
1350- if session :
1351- session ._server_session .mark_dirty ()
1352- raise
1353- except OperationFailure as exc :
1354- if exc .code in helpers ._RETRYABLE_ERROR_CODES :
1355- # Do not request an immediate check since the server is likely
1356- # shutting down.
1357- self .__reset_server (server_address )
1358- raise
1359-
13601321 def _retry_with_session (self , retryable , func , session , bulk ):
13611322 """Execute an operation with at most one consecutive retries
13621323
@@ -1494,7 +1455,7 @@ def _retryable_write(self, retryable, func, session):
14941455 with self ._tmp_session (session ) as s :
14951456 return self ._retry_with_session (retryable , func , s , None )
14961457
1497- def __reset_server (self , address ):
1458+ def _reset_server (self , address ):
14981459 """Clear our connection pool for a server and mark it Unknown."""
14991460 self ._topology .reset_server (address )
15001461
@@ -2158,3 +2119,69 @@ def __next__(self):
21582119 raise TypeError ("'MongoClient' object is not iterable" )
21592120
21602121 next = __next__
2122+
2123+
2124+ class _MongoClientErrorHandler (object ):
2125+ """Error handler for MongoClient."""
2126+ __slots__ = ('_client' , '_server_address' , '_session' , '_max_wire_version' )
2127+
2128+ def __init__ (self , client , server_address , session ):
2129+ self ._client = client
2130+ self ._server_address = server_address
2131+ self ._session = session
2132+ self ._max_wire_version = None
2133+
2134+ def contribute_socket (self , sock_info ):
2135+ """Provide socket information to the error handler."""
2136+ # Currently, we only extract the max_wire_version information.
2137+ self ._max_wire_version = sock_info .max_wire_version
2138+
2139+ def __enter__ (self ):
2140+ return self
2141+
2142+ def __exit__ (self , exc_type , exc_val , exc_tb ):
2143+ if exc_type is None :
2144+ return
2145+
2146+ if issubclass (exc_type , PyMongoError ):
2147+ if self ._session and exc_val .has_error_label (
2148+ "TransientTransactionError" ):
2149+ self ._session ._unpin_mongos ()
2150+
2151+ if issubclass (exc_type , NetworkTimeout ):
2152+ # The socket has been closed. Don't reset the server.
2153+ # Server Discovery And Monitoring Spec: "When an application
2154+ # operation fails because of any network error besides a socket
2155+ # timeout...."
2156+ if self ._session :
2157+ self ._session ._server_session .mark_dirty ()
2158+ elif issubclass (exc_type , NotMasterError ):
2159+ # As per the SDAM spec if:
2160+ # - the server sees a "not master" error, and
2161+ # - the server is not shutting down, and
2162+ # - the server version is >= 4.2, then
2163+ # we keep the existing connection pool, but mark the server type
2164+ # as Unknown and request an immediate check of the server.
2165+ # Otherwise, we clear the connection pool, mark the server as
2166+ # Unknown and request an immediate check of the server.
2167+ err_code = exc_val .details .get ('code' , - 1 )
2168+ is_shutting_down = err_code in helpers ._SHUTDOWN_CODES
2169+ if (is_shutting_down or (self ._max_wire_version is None ) or
2170+ (self ._max_wire_version <= 7 )):
2171+ # Clear the pool, mark server Unknown and request check.
2172+ self ._client ._reset_server_and_request_check (
2173+ self ._server_address )
2174+ else :
2175+ self ._client ._topology .mark_server_unknown_and_request_check (
2176+ self ._server_address )
2177+ elif issubclass (exc_type , ConnectionFailure ):
2178+ # "Client MUST replace the server's description with type Unknown
2179+ # ... MUST NOT request an immediate check of the server."
2180+ self ._client ._reset_server (self ._server_address )
2181+ if self ._session :
2182+ self ._session ._server_session .mark_dirty ()
2183+ elif issubclass (exc_type , OperationFailure ):
2184+ # Do not request an immediate check since the server is likely
2185+ # shutting down.
2186+ if exc_val .code in helpers ._RETRYABLE_ERROR_CODES :
2187+ self ._client ._reset_server (self ._server_address )
0 commit comments