|
20 | 20 | import socketserver
|
21 | 21 | import sys
|
22 | 22 | import threading
|
| 23 | +import time |
23 | 24 | from asyncio import StreamReader, StreamWriter
|
24 | 25 | from pathlib import Path
|
25 | 26 | from test.asynchronous.helpers import ConcurrentRunner
|
26 | 27 |
|
| 28 | +from pymongo.asynchronous.pool import AsyncConnection |
| 29 | +from pymongo.operations import _Op |
| 30 | +from pymongo.server_selectors import writable_server_selector |
| 31 | + |
27 | 32 | sys.path[0:0] = [""]
|
28 | 33 |
|
29 | 34 | from test.asynchronous import (
|
@@ -370,6 +375,74 @@ async def test_pool_unpause(self):
|
370 | 375 | await listener.async_wait_for_event(monitoring.ServerHeartbeatSucceededEvent, 1)
|
371 | 376 | await listener.async_wait_for_event(monitoring.PoolReadyEvent, 1)
|
372 | 377 |
|
| 378 | + @async_client_context.require_failCommand_appName |
| 379 | + @async_client_context.require_test_commands |
| 380 | + @async_client_context.require_async |
| 381 | + async def test_connection_close_does_not_block_other_operations(self): |
| 382 | + listener = CMAPHeartbeatListener() |
| 383 | + client = await self.async_single_client( |
| 384 | + appName="SDAMConnectionCloseTest", |
| 385 | + event_listeners=[listener], |
| 386 | + heartbeatFrequencyMS=500, |
| 387 | + minPoolSize=10, |
| 388 | + ) |
| 389 | + server = await (await client._get_topology()).select_server( |
| 390 | + writable_server_selector, _Op.TEST |
| 391 | + ) |
| 392 | + await async_wait_until( |
| 393 | + lambda: len(server._pool.conns) == 10, |
| 394 | + "pool initialized with 10 connections", |
| 395 | + ) |
| 396 | + |
| 397 | + await client.db.test.insert_one({"x": 1}) |
| 398 | + close_delay = 0.1 |
| 399 | + latencies = [] |
| 400 | + should_exit = [] |
| 401 | + |
| 402 | + async def run_task(): |
| 403 | + while True: |
| 404 | + start_time = time.monotonic() |
| 405 | + await client.db.test.find_one({}) |
| 406 | + elapsed = time.monotonic() - start_time |
| 407 | + latencies.append(elapsed) |
| 408 | + if should_exit: |
| 409 | + break |
| 410 | + await asyncio.sleep(0.001) |
| 411 | + |
| 412 | + task = ConcurrentRunner(target=run_task) |
| 413 | + await task.start() |
| 414 | + original_close = AsyncConnection.close_conn |
| 415 | + try: |
| 416 | + # Artificially delay the close operation to simulate a slow close |
| 417 | + async def mock_close(self, reason): |
| 418 | + await asyncio.sleep(close_delay) |
| 419 | + await original_close(self, reason) |
| 420 | + |
| 421 | + AsyncConnection.close_conn = mock_close |
| 422 | + |
| 423 | + fail_hello = { |
| 424 | + "mode": {"times": 4}, |
| 425 | + "data": { |
| 426 | + "failCommands": [HelloCompat.LEGACY_CMD, "hello"], |
| 427 | + "errorCode": 91, |
| 428 | + "appName": "SDAMConnectionCloseTest", |
| 429 | + }, |
| 430 | + } |
| 431 | + async with self.fail_point(fail_hello): |
| 432 | + # Wait for server heartbeat to fail |
| 433 | + await listener.async_wait_for_event(monitoring.ServerHeartbeatFailedEvent, 1) |
| 434 | + # Wait until all idle connections are closed to simulate real-world conditions |
| 435 | + await listener.async_wait_for_event(monitoring.ConnectionClosedEvent, 10) |
| 436 | + # Wait for one more find to complete after the pool has been reset, then shutdown the task |
| 437 | + n = len(latencies) |
| 438 | + await async_wait_until(lambda: len(latencies) >= n + 1, "run one more find") |
| 439 | + should_exit.append(True) |
| 440 | + await task.join() |
| 441 | + # No operation latency should not significantly exceed close_delay |
| 442 | + self.assertLessEqual(max(latencies), close_delay * 5.0) |
| 443 | + finally: |
| 444 | + AsyncConnection.close_conn = original_close |
| 445 | + |
373 | 446 |
|
374 | 447 | class TestServerMonitoringMode(AsyncIntegrationTest):
|
375 | 448 | @async_client_context.require_no_serverless
|
|
0 commit comments