Skip to content
Closed
94 changes: 94 additions & 0 deletions .evergreen/spec-patch/PYTHON-5791.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
PYMONGO DOES NOT YET SEND readConcern.afterClusterTime IN RETRYABLE WRITE RETRIES

diff --git a/test/transactions-convenient-api/unified/callback-aborts.json b/test/transactions-convenient-api/unified/callback-aborts.json
index 20642871..dc8f7fb5 100644
--- a/test/transactions-convenient-api/unified/callback-aborts.json
+++ b/test/transactions-convenient-api/unified/callback-aborts.json
@@ -308,10 +308,12 @@
"lsid": {
"$$sessionLsid": "session0"
},
- "autocommit": {
- "$$exists": false
- },
"readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ }
+ },
+ "autocommit": {
"$$exists": false
},
"startTransaction": {
diff --git a/test/transactions-convenient-api/unified/callback-commits.json b/test/transactions-convenient-api/unified/callback-commits.json
index 06f791e9..edda3864 100644
--- a/test/transactions-convenient-api/unified/callback-commits.json
+++ b/test/transactions-convenient-api/unified/callback-commits.json
@@ -381,10 +381,12 @@
"lsid": {
"$$sessionLsid": "session0"
},
- "autocommit": {
- "$$exists": false
- },
"readConcern": {
+ "afterClusterTime": {
+ "$$exists": true
+ }
+ },
+ "autocommit": {
"$$exists": false
},
"startTransaction": {
diff --git a/test/transactions/unified/commit.json b/test/transactions/unified/commit.json
index ab778d8d..f0339069 100644
--- a/test/transactions/unified/commit.json
+++ b/test/transactions/unified/commit.json
@@ -1040,7 +1040,9 @@
],
"ordered": true,
"readConcern": {
- "$$exists": false
+ "afterClusterTime": {
+ "$$exists": true
+ }
},
"lsid": {
"$$sessionLsid": "session1"
@@ -1196,7 +1198,9 @@
],
"ordered": true,
"readConcern": {
- "$$exists": false
+ "afterClusterTime": {
+ "$$exists": true
+ }
},
"lsid": {
"$$sessionLsid": "session1"
diff --git a/test/transactions/unified/retryable-writes.json b/test/transactions/unified/retryable-writes.json
index c196e686..21ef22b8 100644
--- a/test/transactions/unified/retryable-writes.json
+++ b/test/transactions/unified/retryable-writes.json
@@ -217,7 +217,9 @@
],
"ordered": true,
"readConcern": {
- "$$exists": false
+ "afterClusterTime": {
+ "$$exists": true
+ }
},
"lsid": {
"$$sessionLsid": "session0"
@@ -306,7 +308,9 @@
],
"ordered": true,
"readConcern": {
- "$$exists": false
+ "afterClusterTime": {
+ "$$exists": true
+ }
},
"lsid": {
"$$sessionLsid": "session0"
62 changes: 0 additions & 62 deletions .evergreen/spec-patch/permanent/PYTHON-4201.patch

This file was deleted.

12 changes: 10 additions & 2 deletions pymongo/asynchronous/command_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,23 @@ async def _send_message(self, operation: _GetMore) -> None:
self._killed = True
if exc.timeout:
self._die_no_lock()
elif self._sock_mgr:
# In load balancer mode the pinned connection must stay checked
# out until cursor.close() is called. _killed is already True
# for cursor-closed errors (set above); for other errors leave
# it False so cursor.close() will send killCursors via the
# pinned connection before releasing it.
pass
else:
# Return the session and pinned connection, if necessary.
await self.close()
raise
except ConnectionFailure:
# Don't send killCursors because the cursor is already closed.
self._killed = True
# Return the session and pinned connection, if necessary.
await self.close()
if not self._sock_mgr:
# Return the session and pinned connection, if necessary.
await self.close()
raise
except Exception:
await self.close()
Expand Down
10 changes: 9 additions & 1 deletion pymongo/asynchronous/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,13 @@ async def _send_message(self, operation: Union[_Query, _GetMore]) -> None:
self._killed = True
if exc.timeout:
self._die_no_lock()
elif self._sock_mgr:
# In load balancer mode the pinned connection must stay checked
# out until cursor.close() is called. _killed is already True
# for cursor-closed errors (set above); for other errors leave
# it False so cursor.close() will send killCursors via the
# pinned connection before releasing it.
pass
else:
await self.close()
# If this is a tailable cursor the error is likely
Expand All @@ -1007,7 +1014,8 @@ async def _send_message(self, operation: Union[_Query, _GetMore]) -> None:
raise
except ConnectionFailure:
self._killed = True
await self.close()
if not self._sock_mgr:
await self.close()
raise
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
except BaseException:
Expand Down
39 changes: 34 additions & 5 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,11 +623,14 @@ async def _raise_connection_failure(self, error: BaseException) -> NoReturn:
# KeyboardInterrupt from the start, rather than as an initial
# socket.error, so we catch that, close the socket, and reraise it.
#
# The connection closed event will be emitted later in checkin.
if self.ready:
reason = None
# For ready connections the closed event is emitted later in checkin.
# For not-ready LB connections, defer the event so that pool.connect()'s
# except block can emit poolClearedEvent first (CMAP spec requirement).
# For not-ready non-LB connections, emit the event immediately as before.
if self.ready or not self.opts.load_balanced:
reason = None if self.ready else ConnectionClosedReason.ERROR
else:
reason = ConnectionClosedReason.ERROR
reason = None # deferred to pool.connect()'s except block
await self.close_conn(reason)
# SSLError from PyOpenSSL inherits directly from Exception.
if isinstance(error, (IOError, OSError, *SSLErrors)):
Expand Down Expand Up @@ -1086,7 +1089,33 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
self.active_contexts.discard(conn.cancel_context)
if not completed_hello:
self._handle_connection_error(e)
await conn.close_conn(ConnectionClosedReason.ERROR)
if completed_hello and handler and self.opts.load_balanced:
# LB only: hello succeeded so service_id is known. Run SDAM
# error handling (which clears the pool) before emitting
# connectionClosedEvent as required by the CMAP spec.
await handler.handle(type(e), e)
if self.opts.load_balanced and conn.closed:
# LB: _raise_connection_failure deferred the connectionClosedEvent.
# Emit it now, after any poolClearedEvent emitted above.
if conn.enabled_for_cmap:
assert conn.listeners is not None
conn.listeners.publish_connection_closed(
conn.address, conn.id, ConnectionClosedReason.ERROR
)
if conn.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_CONNECTION_LOGGER,
message=_ConnectionStatusMessage.CONN_CLOSED,
clientId=self._client_id,
serverHost=self.address[0],
serverPort=self.address[1],
driverConnectionId=conn.id,
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
error=ConnectionClosedReason.ERROR,
)
else:
# Non-LB: close_conn emits event if not already closed.
await conn.close_conn(ConnectionClosedReason.ERROR)
raise

if handler:
Expand Down
3 changes: 2 additions & 1 deletion pymongo/asynchronous/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
_SDAM_LOGGER,
_SERVER_SELECTION_LOGGER,
_debug_log,
_info_log,
_SDAMStatusMessage,
_ServerSelectionStatusMessage,
)
Expand Down Expand Up @@ -361,7 +362,7 @@ async def _select_servers_loop(
)

if not logged_waiting:
_debug_log(
_info_log(
_SERVER_SELECTION_LOGGER,
message=_ServerSelectionStatusMessage.WAITING,
selector=selector,
Expand Down
12 changes: 10 additions & 2 deletions pymongo/synchronous/command_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,23 @@ def _send_message(self, operation: _GetMore) -> None:
self._killed = True
if exc.timeout:
self._die_no_lock()
elif self._sock_mgr:
# In load balancer mode the pinned connection must stay checked
# out until cursor.close() is called. _killed is already True
# for cursor-closed errors (set above); for other errors leave
# it False so cursor.close() will send killCursors via the
# pinned connection before releasing it.
pass
else:
# Return the session and pinned connection, if necessary.
self.close()
raise
except ConnectionFailure:
# Don't send killCursors because the cursor is already closed.
self._killed = True
# Return the session and pinned connection, if necessary.
self.close()
if not self._sock_mgr:
# Return the session and pinned connection, if necessary.
self.close()
raise
except Exception:
self.close()
Expand Down
10 changes: 9 additions & 1 deletion pymongo/synchronous/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,13 @@ def _send_message(self, operation: Union[_Query, _GetMore]) -> None:
self._killed = True
if exc.timeout:
self._die_no_lock()
elif self._sock_mgr:
# In load balancer mode the pinned connection must stay checked
# out until cursor.close() is called. _killed is already True
# for cursor-closed errors (set above); for other errors leave
# it False so cursor.close() will send killCursors via the
# pinned connection before releasing it.
pass
else:
self.close()
# If this is a tailable cursor the error is likely
Expand All @@ -1005,7 +1012,8 @@ def _send_message(self, operation: Union[_Query, _GetMore]) -> None:
raise
except ConnectionFailure:
self._killed = True
self.close()
if not self._sock_mgr:
self.close()
raise
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
except BaseException:
Expand Down
39 changes: 34 additions & 5 deletions pymongo/synchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,11 +621,14 @@ def _raise_connection_failure(self, error: BaseException) -> NoReturn:
# KeyboardInterrupt from the start, rather than as an initial
# socket.error, so we catch that, close the socket, and reraise it.
#
# The connection closed event will be emitted later in checkin.
if self.ready:
reason = None
# For ready connections the closed event is emitted later in checkin.
# For not-ready LB connections, defer the event so that pool.connect()'s
# except block can emit poolClearedEvent first (CMAP spec requirement).
# For not-ready non-LB connections, emit the event immediately as before.
if self.ready or not self.opts.load_balanced:
reason = None if self.ready else ConnectionClosedReason.ERROR
else:
reason = ConnectionClosedReason.ERROR
reason = None # deferred to pool.connect()'s except block
self.close_conn(reason)
# SSLError from PyOpenSSL inherits directly from Exception.
if isinstance(error, (IOError, OSError, *SSLErrors)):
Expand Down Expand Up @@ -1082,7 +1085,33 @@ def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> Connect
self.active_contexts.discard(conn.cancel_context)
if not completed_hello:
self._handle_connection_error(e)
conn.close_conn(ConnectionClosedReason.ERROR)
if completed_hello and handler and self.opts.load_balanced:
# LB only: hello succeeded so service_id is known. Run SDAM
# error handling (which clears the pool) before emitting
# connectionClosedEvent as required by the CMAP spec.
handler.handle(type(e), e)
if self.opts.load_balanced and conn.closed:
# LB: _raise_connection_failure deferred the connectionClosedEvent.
# Emit it now, after any poolClearedEvent emitted above.
if conn.enabled_for_cmap:
assert conn.listeners is not None
conn.listeners.publish_connection_closed(
conn.address, conn.id, ConnectionClosedReason.ERROR
)
if conn.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_CONNECTION_LOGGER,
message=_ConnectionStatusMessage.CONN_CLOSED,
clientId=self._client_id,
serverHost=self.address[0],
serverPort=self.address[1],
driverConnectionId=conn.id,
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
error=ConnectionClosedReason.ERROR,
)
else:
# Non-LB: close_conn emits event if not already closed.
conn.close_conn(ConnectionClosedReason.ERROR)
raise

if handler:
Expand Down
3 changes: 2 additions & 1 deletion pymongo/synchronous/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
_SDAM_LOGGER,
_SERVER_SELECTION_LOGGER,
_debug_log,
_info_log,
_SDAMStatusMessage,
_ServerSelectionStatusMessage,
)
Expand Down Expand Up @@ -361,7 +362,7 @@ def _select_servers_loop(
)

if not logged_waiting:
_debug_log(
_info_log(
_SERVER_SELECTION_LOGGER,
message=_ServerSelectionStatusMessage.WAITING,
selector=selector,
Expand Down
Loading
Loading