Skip to content

Commit abfe0a6

Browse files
committed
Backport PYTHON-5642
1 parent a5a50a8 commit abfe0a6

File tree

4 files changed

+98
-5
lines changed

4 files changed

+98
-5
lines changed

doc/changelog.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
Changelog
22
=========
33

4+
Changes in Version 4.15.5 (2025/11/25)
5+
--------------------------------------
6+
7+
Version 4.15.5 is a bug fix release.
8+
9+
- Fixed a bug that could cause ``AutoReconnect("connection pool paused")`` errors when cursors fetched more documents from the database after SDAM heartbeat failures.
10+
11+
Changes in Version 4.15.4 (2025/10/21)
12+
--------------------------------------
13+
14+
Version 4.15.4 is a bug fix release.
15+
16+
- Relaxed the callback type of :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.with_transaction` to allow the broader Awaitable type rather than only Coroutine objects.
17+
- Added the missing Python 3.14 trove classifier to the package metadata.
18+
19+
Issues Resolved
20+
...............
21+
22+
See the `PyMongo 4.15.4 release notes in JIRA`_ for the list of resolved issues
23+
in this release.
24+
25+
.. _PyMongo 4.15.4 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47237
26+
427
Changes in Version 4.15.3 (2025/10/07)
528
--------------------------------------
629

pymongo/topology_description.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def apply_selector(
322322
if address:
323323
# Ignore selectors when explicit address is requested.
324324
description = self.server_descriptions().get(address)
325-
return [description] if description else []
325+
return [description] if description and description.is_server_type_known else []
326326

327327
# Primary selection fast path.
328328
if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary:

test/asynchronous/test_server_selection.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
import os
1919
import sys
20+
import time
2021
from pathlib import Path
2122

22-
from pymongo import AsyncMongoClient, ReadPreference
23+
from pymongo import AsyncMongoClient, ReadPreference, monitoring
2324
from pymongo.asynchronous.settings import TopologySettings
2425
from pymongo.asynchronous.topology import Topology
2526
from pymongo.errors import ServerSelectionTimeoutError
@@ -30,7 +31,7 @@
3031

3132
sys.path[0:0] = [""]
3233

33-
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
34+
from test.asynchronous import AsyncIntegrationTest, async_client_context, client_knobs, unittest
3435
from test.asynchronous.utils import async_wait_until
3536
from test.asynchronous.utils_selection_tests import (
3637
create_selection_tests,
@@ -42,6 +43,7 @@
4243
)
4344
from test.utils_shared import (
4445
FunctionCallRecorder,
46+
HeartbeatEventListener,
4547
OvertCommandListener,
4648
)
4749

@@ -207,6 +209,40 @@ async def test_server_selector_bypassed(self):
207209
)
208210
self.assertEqual(selector.call_count, 0)
209211

212+
@async_client_context.require_replica_set
213+
@async_client_context.require_failCommand_appName
214+
async def test_server_selection_getMore_blocks(self):
215+
hb_listener = HeartbeatEventListener()
216+
client = await self.async_rs_client(
217+
event_listeners=[hb_listener], heartbeatFrequencyMS=500, appName="heartbeatFailedClient"
218+
)
219+
coll = client.db.test
220+
await coll.drop()
221+
docs = [{"x": 1} for _ in range(5)]
222+
await coll.insert_many(docs)
223+
224+
fail_heartbeat = {
225+
"configureFailPoint": "failCommand",
226+
"mode": {"times": 4},
227+
"data": {
228+
"failCommands": [HelloCompat.LEGACY_CMD, "hello"],
229+
"closeConnection": True,
230+
"appName": "heartbeatFailedClient",
231+
},
232+
}
233+
234+
def hb_failed(event):
235+
return isinstance(event, monitoring.ServerHeartbeatFailedEvent)
236+
237+
cursor = coll.find({}, batch_size=1)
238+
await cursor.next() # force initial query that will pin the address for the getMore
239+
240+
async with self.fail_point(fail_heartbeat):
241+
await async_wait_until(
242+
lambda: hb_listener.matching(hb_failed), "published failed event"
243+
)
244+
self.assertEqual(len(await cursor.to_list()), 4)
245+
210246

211247
if __name__ == "__main__":
212248
unittest.main()

test/test_server_selection.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
import os
1919
import sys
20+
import time
2021
from pathlib import Path
2122

22-
from pymongo import MongoClient, ReadPreference
23+
from pymongo import MongoClient, ReadPreference, monitoring
2324
from pymongo.errors import ServerSelectionTimeoutError
2425
from pymongo.hello import HelloCompat
2526
from pymongo.operations import _Op
@@ -30,7 +31,7 @@
3031

3132
sys.path[0:0] = [""]
3233

33-
from test import IntegrationTest, client_context, unittest
34+
from test import IntegrationTest, client_context, client_knobs, unittest
3435
from test.utils import wait_until
3536
from test.utils_selection_tests import (
3637
create_selection_tests,
@@ -42,6 +43,7 @@
4243
)
4344
from test.utils_shared import (
4445
FunctionCallRecorder,
46+
HeartbeatEventListener,
4547
OvertCommandListener,
4648
)
4749

@@ -205,6 +207,38 @@ def test_server_selector_bypassed(self):
205207
topology.select_server(writable_server_selector, _Op.TEST, server_selection_timeout=0.1)
206208
self.assertEqual(selector.call_count, 0)
207209

210+
@client_context.require_replica_set
211+
@client_context.require_failCommand_appName
212+
def test_server_selection_getMore_blocks(self):
213+
hb_listener = HeartbeatEventListener()
214+
client = self.rs_client(
215+
event_listeners=[hb_listener], heartbeatFrequencyMS=500, appName="heartbeatFailedClient"
216+
)
217+
coll = client.db.test
218+
coll.drop()
219+
docs = [{"x": 1} for _ in range(5)]
220+
coll.insert_many(docs)
221+
222+
fail_heartbeat = {
223+
"configureFailPoint": "failCommand",
224+
"mode": {"times": 4},
225+
"data": {
226+
"failCommands": [HelloCompat.LEGACY_CMD, "hello"],
227+
"closeConnection": True,
228+
"appName": "heartbeatFailedClient",
229+
},
230+
}
231+
232+
def hb_failed(event):
233+
return isinstance(event, monitoring.ServerHeartbeatFailedEvent)
234+
235+
cursor = coll.find({}, batch_size=1)
236+
cursor.next() # force initial query that will pin the address for the getMore
237+
238+
with self.fail_point(fail_heartbeat):
239+
wait_until(lambda: hb_listener.matching(hb_failed), "published failed event")
240+
self.assertEqual(len(cursor.to_list()), 4)
241+
208242

209243
if __name__ == "__main__":
210244
unittest.main()

0 commit comments

Comments
 (0)