Skip to content

Commit e2d4fe2

Browse files
committed
PYTHON-4724 - Document the behavior of using an async client across multiple event loop
1 parent 3210b17 commit e2d4fe2

File tree

4 files changed

+68
-1
lines changed

4 files changed

+68
-1
lines changed

pymongo/asynchronous/mongo_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,14 @@ def __init__(
878878

879879
self._opened = False
880880
self._closed = False
881+
self._loop: Optional[asyncio.AbstractEventLoop] = None
882+
if not _IS_SYNC:
883+
# Check if the client was created within a running event loop.
884+
try:
885+
self._loop = asyncio.get_running_loop()
886+
# It wasn't, so the loop will be stored during the first performed operation
887+
except RuntimeError:
888+
pass
881889
if not is_srv:
882890
self._init_background()
883891

@@ -1709,6 +1717,12 @@ async def _get_topology(self) -> Topology:
17091717
If this client was created with "connect=False", calling _get_topology
17101718
launches the connection process in the background.
17111719
"""
1720+
if not _IS_SYNC:
1721+
if self._loop is None:
1722+
self._loop = asyncio.get_running_loop()
1723+
else:
1724+
if self._loop != asyncio.get_running_loop():
1725+
raise RuntimeError("Cannot use AsyncMongoClient in different event loop")
17121726
if not self._opened:
17131727
if self._resolve_srv_info["is_srv"]:
17141728
await self._resolve_srv()

pymongo/synchronous/mongo_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,14 @@ def __init__(
876876

877877
self._opened = False
878878
self._closed = False
879+
self._loop: Optional[asyncio.AbstractEventLoop] = None
880+
if not _IS_SYNC:
881+
# Check if the client was created within a running event loop.
882+
try:
883+
self._loop = asyncio.get_running_loop()
884+
# It wasn't, so the loop will be stored during the first performed operation
885+
except RuntimeError:
886+
pass
879887
if not is_srv:
880888
self._init_background()
881889

@@ -1703,6 +1711,12 @@ def _get_topology(self) -> Topology:
17031711
If this client was created with "connect=False", calling _get_topology
17041712
launches the connection process in the background.
17051713
"""
1714+
if not _IS_SYNC:
1715+
if self._loop is None:
1716+
self._loop = asyncio.get_running_loop()
1717+
else:
1718+
if self._loop != asyncio.get_running_loop():
1719+
raise RuntimeError("Cannot use MongoClient in different event loop")
17061720
if not self._opened:
17071721
if self._resolve_srv_info["is_srv"]:
17081722
self._resolve_srv()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2025-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test that the asynchronous API detects event loop changes and fails correctly."""
16+
from __future__ import annotations
17+
18+
import asyncio
19+
import unittest
20+
21+
from pymongo import AsyncMongoClient
22+
23+
24+
class TestClientLoopSafety(unittest.TestCase):
25+
def test_client_errors_on_different_loop(self):
26+
client = AsyncMongoClient()
27+
loop1 = asyncio.new_event_loop()
28+
loop1.run_until_complete(client.server_info())
29+
loop2 = asyncio.new_event_loop()
30+
with self.assertRaisesRegex(
31+
RuntimeError, "Cannot use AsyncMongoClient in different event loop"
32+
):
33+
loop2.run_until_complete(client.server_info())
34+
loop1.run_until_complete(client.close())

tools/synchro.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@
179179

180180
def async_only_test(f: str) -> bool:
181181
"""Return True for async tests that should not be converted to sync."""
182-
return f in ["test_locks.py", "test_concurrency.py", "test_async_cancellation.py"]
182+
return f in [
183+
"test_locks.py",
184+
"test_concurrency.py",
185+
"test_async_cancellation.py",
186+
"test_async_loop_safety.py",
187+
]
183188

184189

185190
test_files = [

0 commit comments

Comments
 (0)