59
59
ConfigurationError ,
60
60
ConnectionFailure ,
61
61
InvalidOperation ,
62
+ NotMasterError ,
62
63
OperationFailure ,
63
64
PyMongoError ,
64
65
ServerSelectionTimeoutError )
@@ -1265,7 +1266,9 @@ def _select_server(self, server_selector, session, address=None):
1265
1266
session ._pin_mongos (server )
1266
1267
return server
1267
1268
except PyMongoError as exc :
1268
- if session and exc .has_error_label ("TransientTransactionError" ):
1269
+ # Server selection errors in a transaction are transient.
1270
+ if session and session .in_transaction :
1271
+ exc ._add_error_label ("TransientTransactionError" )
1269
1272
session ._unpin_mongos ()
1270
1273
raise
1271
1274
@@ -1361,6 +1364,11 @@ def _retry_with_session(self, retryable, func, session, bulk):
1361
1364
"""
1362
1365
retryable = (retryable and self .retry_writes
1363
1366
and session and not session .in_transaction )
1367
+ return self ._retry_internal (retryable , func , session , bulk )
1368
+
1369
+ def _retry_internal (self , retryable , func , session , bulk ):
1370
+ """Internal retryable write helper."""
1371
+ max_wire_version = 0
1364
1372
last_error = None
1365
1373
retrying = False
1366
1374
@@ -1369,7 +1377,7 @@ def is_retrying():
1369
1377
# Increment the transaction id up front to ensure any retry attempt
1370
1378
# will use the proper txnNumber, even if server or socket selection
1371
1379
# fails before the command can be sent.
1372
- if retryable :
1380
+ if retryable and session and not session . in_transaction :
1373
1381
session ._start_retryable_write ()
1374
1382
if bulk :
1375
1383
bulk .started_retryable_write = True
@@ -1381,6 +1389,7 @@ def is_retrying():
1381
1389
session is not None and
1382
1390
server .description .retryable_writes_supported )
1383
1391
with self ._get_socket (server , session ) as sock_info :
1392
+ max_wire_version = sock_info .max_wire_version
1384
1393
if retryable and not supports_session :
1385
1394
if is_retrying ():
1386
1395
# A retry is not possible because this server does
@@ -1398,40 +1407,12 @@ def is_retrying():
1398
1407
# be a persistent outage. Attempting to retry in this case will
1399
1408
# most likely be a waste of time.
1400
1409
raise
1401
- except ConnectionFailure as exc :
1402
- if not retryable or is_retrying () :
1410
+ except Exception as exc :
1411
+ if not retryable :
1403
1412
raise
1404
- if bulk :
1405
- bulk .retrying = True
1406
- else :
1407
- retrying = True
1408
- last_error = exc
1409
- except BulkWriteError as exc :
1410
- if not retryable or is_retrying ():
1411
- raise
1412
- # Check the last writeConcernError to determine if this
1413
- # BulkWriteError is retryable.
1414
- wces = exc .details ['writeConcernErrors' ]
1415
- wce = wces [- 1 ] if wces else {}
1416
- if wce .get ('code' , 0 ) not in helpers ._RETRYABLE_ERROR_CODES :
1417
- raise
1418
- if bulk :
1419
- bulk .retrying = True
1420
- else :
1421
- retrying = True
1422
- last_error = exc
1423
- except OperationFailure as exc :
1424
- # retryWrites on MMAPv1 should raise an actionable error.
1425
- if (exc .code == 20 and
1426
- str (exc ).startswith ("Transaction numbers" )):
1427
- errmsg = (
1428
- "This MongoDB deployment does not support "
1429
- "retryable writes. Please add retryWrites=false "
1430
- "to your connection string." )
1431
- raise OperationFailure (errmsg , exc .code , exc .details )
1432
- if not retryable or is_retrying ():
1433
- raise
1434
- if exc .code not in helpers ._RETRYABLE_ERROR_CODES :
1413
+ # Add the RetryableWriteError label.
1414
+ if (not _retryable_writes_error (exc , max_wire_version )
1415
+ or is_retrying ()):
1435
1416
raise
1436
1417
if bulk :
1437
1418
bulk .retrying = True
@@ -2162,26 +2143,66 @@ def __next__(self):
2162
2143
next = __next__
2163
2144
2164
2145
2146
+ def _retryable_error_doc (exc ):
2147
+ """Return the server response from PyMongo exception or None."""
2148
+ if isinstance (exc , BulkWriteError ):
2149
+ # Check the last writeConcernError to determine if this
2150
+ # BulkWriteError is retryable.
2151
+ wces = exc .details ['writeConcernErrors' ]
2152
+ wce = wces [- 1 ] if wces else None
2153
+ return wce
2154
+ if isinstance (exc , (NotMasterError , OperationFailure )):
2155
+ return exc .details
2156
+ return None
2157
+
2158
+
2159
+ def _retryable_writes_error (exc , max_wire_version ):
2160
+ doc = _retryable_error_doc (exc )
2161
+ if doc :
2162
+ code = doc .get ('code' , 0 )
2163
+ # retryWrites on MMAPv1 should raise an actionable error.
2164
+ if (code == 20 and
2165
+ str (exc ).startswith ("Transaction numbers" )):
2166
+ errmsg = (
2167
+ "This MongoDB deployment does not support "
2168
+ "retryable writes. Please add retryWrites=false "
2169
+ "to your connection string." )
2170
+ raise OperationFailure (errmsg , code , exc .details )
2171
+ if max_wire_version >= 9 :
2172
+ # MongoDB 4.4+ utilizes RetryableWriteError.
2173
+ return 'RetryableWriteError' in doc .get ('errorLabels' , [])
2174
+ else :
2175
+ if code in helpers ._RETRYABLE_ERROR_CODES :
2176
+ exc ._add_error_label ("RetryableWriteError" )
2177
+ return True
2178
+ return False
2179
+
2180
+ if isinstance (exc , ConnectionFailure ):
2181
+ exc ._add_error_label ("RetryableWriteError" )
2182
+ return True
2183
+ return False
2184
+
2185
+
2165
2186
class _MongoClientErrorHandler (object ):
2166
- """Error handler for MongoClient ."""
2167
- __slots__ = ('_client ' , '_server_address ' , '_session ' ,
2168
- '_max_wire_version' , '_sock_generation ' )
2187
+ """Handle errors raised when executing an operation ."""
2188
+ __slots__ = ('client ' , 'server_address ' , 'session' , 'max_wire_version ' ,
2189
+ 'sock_generation ' )
2169
2190
2170
2191
def __init__ (self , client , server , session ):
2171
- self ._client = client
2172
- self ._server_address = server .description .address
2173
- self ._session = session
2174
- self ._max_wire_version = common .MIN_WIRE_VERSION
2192
+ self .client = client
2193
+ self .server_address = server .description .address
2194
+ self .session = session
2195
+ self .max_wire_version = common .MIN_WIRE_VERSION
2175
2196
# XXX: When get_socket fails, this generation could be out of date:
2176
2197
# "Note that when a network error occurs before the handshake
2177
2198
# completes then the error's generation number is the generation
2178
2199
# of the pool at the time the connection attempt was started."
2179
- self ._sock_generation = server .pool .generation
2200
+ self .sock_generation = server .pool .generation
2180
2201
2181
2202
def contribute_socket (self , sock_info ):
2182
2203
"""Provide socket information to the error handler."""
2183
- self ._max_wire_version = sock_info .max_wire_version
2184
- self ._sock_generation = sock_info .generation
2204
+ self .max_wire_version = sock_info .max_wire_version
2205
+ self .sock_generation = sock_info .generation
2185
2206
2186
2207
def __enter__ (self ):
2187
2208
return self
@@ -2190,15 +2211,16 @@ def __exit__(self, exc_type, exc_val, exc_tb):
2190
2211
if exc_type is None :
2191
2212
return
2192
2213
2193
- err_ctx = _ErrorContext (
2194
- exc_val , self ._max_wire_version , self ._sock_generation )
2195
- self ._client ._topology .handle_error (self ._server_address , err_ctx )
2214
+ if self .session :
2215
+ if issubclass (exc_type , ConnectionFailure ):
2216
+ if self .session .in_transaction :
2217
+ exc_val ._add_error_label ("TransientTransactionError" )
2218
+ self .session ._server_session .mark_dirty ()
2196
2219
2197
- if issubclass (exc_type , PyMongoError ):
2198
- if self ._session and exc_val .has_error_label (
2199
- "TransientTransactionError" ):
2200
- self ._session ._unpin_mongos ()
2220
+ if issubclass (exc_type , PyMongoError ):
2221
+ if exc_val .has_error_label ("TransientTransactionError" ):
2222
+ self .session ._unpin_mongos ()
2201
2223
2202
- if issubclass ( exc_type , ConnectionFailure ):
2203
- if self ._session :
2204
- self ._session . _server_session . mark_dirty ( )
2224
+ err_ctx = _ErrorContext (
2225
+ exc_val , self .max_wire_version , self . sock_generation )
2226
+ self .client . _topology . handle_error ( self . server_address , err_ctx )
0 commit comments