|
42 | 42 | import weakref |
43 | 43 | from collections import defaultdict |
44 | 44 | from collections.abc import AsyncGenerator, Collection, Coroutine, Mapping, MutableMapping, Sequence |
45 | | -from contextlib import AbstractAsyncContextManager |
46 | 45 | from typing import ( |
47 | 46 | TYPE_CHECKING, |
48 | 47 | Any, |
|
141 | 140 | from pymongo.asynchronous.client_session import AsyncClientSession, _ServerSession |
142 | 141 | from pymongo.asynchronous.cursor_base import _ConnectionManager |
143 | 142 | from pymongo.asynchronous.encryption import _Encrypter |
144 | | - from pymongo.asynchronous.pool import AsyncConnection |
| 143 | + from pymongo.asynchronous.pool import AsyncConnection, _PoolCheckout |
145 | 144 | from pymongo.asynchronous.server import Server |
146 | 145 | from pymongo.read_concern import ReadConcern |
147 | 146 | from pymongo.response import Response |
@@ -1789,39 +1788,8 @@ async def _get_topology(self) -> Topology: |
1789 | 1788 | self._opened = True |
1790 | 1789 | return self._topology |
1791 | 1790 |
|
1792 | | - @contextlib.asynccontextmanager |
1793 | | - async def _checkout( |
1794 | | - self, server: Server, session: Optional[AsyncClientSession] |
1795 | | - ) -> AsyncGenerator[AsyncConnection, None]: |
1796 | | - in_txn = session and session.in_transaction |
1797 | | - async with _MongoClientErrorHandler(self, server, session) as err_handler: |
1798 | | - # Reuse the pinned connection, if it exists. |
1799 | | - if in_txn and session and session._pinned_connection: |
1800 | | - err_handler.contribute_socket(session._pinned_connection) |
1801 | | - yield session._pinned_connection |
1802 | | - return |
1803 | | - async with await server.checkout(handler=err_handler) as conn: |
1804 | | - # Pin this session to the selected server or connection. |
1805 | | - if ( |
1806 | | - in_txn |
1807 | | - and session |
1808 | | - and server.description.server_type |
1809 | | - in ( |
1810 | | - SERVER_TYPE.Mongos, |
1811 | | - SERVER_TYPE.LoadBalancer, |
1812 | | - ) |
1813 | | - ): |
1814 | | - session._pin(server, conn) |
1815 | | - err_handler.contribute_socket(conn) |
1816 | | - if ( |
1817 | | - self._encrypter |
1818 | | - and not self._encrypter._bypass_auto_encryption |
1819 | | - and conn.max_wire_version < 8 |
1820 | | - ): |
1821 | | - raise ConfigurationError( |
1822 | | - "Auto-encryption requires a minimum MongoDB version of 4.2" |
1823 | | - ) |
1824 | | - yield conn |
| 1791 | + def _checkout(self, server: Server, session: Optional[AsyncClientSession]) -> _ClientCheckout: |
| 1792 | + return _ClientCheckout(self, server, session) |
1825 | 1793 |
|
1826 | 1794 | async def _select_server( |
1827 | 1795 | self, |
@@ -1874,41 +1842,22 @@ async def _select_server( |
1874 | 1842 |
|
1875 | 1843 | async def _conn_for_writes( |
1876 | 1844 | self, session: Optional[AsyncClientSession], operation: str |
1877 | | - ) -> AbstractAsyncContextManager[AsyncConnection]: |
| 1845 | + ) -> _ClientCheckout: |
1878 | 1846 | server = await self._select_server(writable_server_selector, session, operation) |
1879 | 1847 | return self._checkout(server, session) |
1880 | 1848 |
|
1881 | | - @contextlib.asynccontextmanager |
1882 | | - async def _conn_from_server( |
| 1849 | + def _conn_from_server( |
1883 | 1850 | self, read_preference: _ServerMode, server: Server, session: Optional[AsyncClientSession] |
1884 | | - ) -> AsyncGenerator[tuple[AsyncConnection, _ServerMode], None]: |
| 1851 | + ) -> _ClientReadCheckout: |
1885 | 1852 | assert read_preference is not None, "read_preference must not be None" |
1886 | | - # Get a connection for a server matching the read preference, and yield |
1887 | | - # conn with the effective read preference. The Server Selection |
1888 | | - # Spec says not to send any $readPreference to standalones and to |
1889 | | - # always send primaryPreferred when directly connected to a repl set |
1890 | | - # member. |
1891 | | - # Thread safe: if the type is single it cannot change. |
1892 | | - # NOTE: We already opened the Topology when selecting a server so there's no need |
1893 | | - # to call _get_topology() again. |
1894 | | - single = self._topology.description.topology_type == TOPOLOGY_TYPE.Single |
1895 | | - async with self._checkout(server, session) as conn: |
1896 | | - if single: |
1897 | | - if conn.is_repl and not (session and session.in_transaction): |
1898 | | - # Use primary preferred to ensure any repl set member |
1899 | | - # can handle the request. |
1900 | | - read_preference = ReadPreference.PRIMARY_PREFERRED |
1901 | | - elif conn.is_standalone: |
1902 | | - # Don't send read preference to standalones. |
1903 | | - read_preference = ReadPreference.PRIMARY |
1904 | | - yield conn, read_preference |
| 1853 | + return _ClientReadCheckout(self, server, session, read_preference) |
1905 | 1854 |
|
1906 | 1855 | async def _conn_for_reads( |
1907 | 1856 | self, |
1908 | 1857 | read_preference: _ServerMode, |
1909 | 1858 | session: Optional[AsyncClientSession], |
1910 | 1859 | operation: str, |
1911 | | - ) -> AbstractAsyncContextManager[tuple[AsyncConnection, _ServerMode]]: |
| 1860 | + ) -> _ClientReadCheckout: |
1912 | 1861 | assert read_preference is not None, "read_preference must not be None" |
1913 | 1862 | server = await self._select_server(read_preference, session, operation) |
1914 | 1863 | return self._conn_from_server(read_preference, server, session) |
@@ -1937,8 +1886,12 @@ async def _run_operation( |
1937 | 1886 | ) |
1938 | 1887 |
|
1939 | 1888 | async with operation.conn_mgr._lock: |
1940 | | - async with _MongoClientErrorHandler(self, server, operation.session) as err_handler: # type: ignore[arg-type] |
1941 | | - err_handler.contribute_socket(operation.conn_mgr.conn) |
| 1889 | + async with _ClientCheckout.for_existing_conn( |
| 1890 | + self, |
| 1891 | + server, |
| 1892 | + operation.session, # type: ignore[arg-type] |
| 1893 | + operation.conn_mgr.conn, |
| 1894 | + ): |
1942 | 1895 | return await run_with_conn( |
1943 | 1896 | operation.conn_mgr.conn, operation, operation.read_preference |
1944 | 1897 | ) |
@@ -2670,10 +2623,16 @@ def _add_retryable_write_error(exc: PyMongoError, max_wire_version: int, is_mong |
2670 | 2623 | exc_to_check._add_error_label("RetryableWriteError") |
2671 | 2624 |
|
2672 | 2625 |
|
2673 | | -class _MongoClientErrorHandler: |
2674 | | - """Handle errors raised when executing an operation.""" |
| 2626 | +class _ClientCheckout: |
| 2627 | + """Context manager for checking out a connection from the pool. |
| 2628 | +
|
| 2629 | + Handles pool checkout, SDAM error handling, and session pinning. |
| 2630 | + """ |
2675 | 2631 |
|
2676 | 2632 | __slots__ = ( |
| 2633 | + "_existing_conn", |
| 2634 | + "_pool_checkout", |
| 2635 | + "_server", |
2677 | 2636 | "client", |
2678 | 2637 | "completed_handshake", |
2679 | 2638 | "handled", |
@@ -2707,9 +2666,12 @@ def __init__( |
2707 | 2666 | self.completed_handshake = False |
2708 | 2667 | self.service_id: Optional[ObjectId] = None |
2709 | 2668 | self.handled = False |
| 2669 | + self._existing_conn: Optional[AsyncConnection] = None |
| 2670 | + self._pool_checkout: Optional[_PoolCheckout] = None |
| 2671 | + self._server = server |
2710 | 2672 |
|
2711 | 2673 | def contribute_socket(self, conn: AsyncConnection, completed_handshake: bool = True) -> None: |
2712 | | - """Provide socket information to the error handler.""" |
| 2674 | + """Record connection metadata needed for SDAM error handling.""" |
2713 | 2675 | self.max_wire_version = conn.max_wire_version |
2714 | 2676 | self.sock_generation = conn.generation |
2715 | 2677 | self.service_id = conn.service_id |
@@ -2744,21 +2706,156 @@ async def handle( |
2744 | 2706 | assert self.client._topology is not None |
2745 | 2707 | await self.client._topology.handle_error(self.server_address, err_ctx) |
2746 | 2708 |
|
2747 | | - async def __aenter__(self) -> _MongoClientErrorHandler: |
2748 | | - return self |
| 2709 | + async def __aenter__(self) -> AsyncConnection: |
| 2710 | + if self._existing_conn is not None: |
| 2711 | + return self._existing_conn |
| 2712 | + server = self._server |
| 2713 | + session = self.session |
| 2714 | + in_txn = session and session.in_transaction |
| 2715 | + # Reuse the pinned connection, if it exists. |
| 2716 | + if in_txn and session and session._pinned_connection: |
| 2717 | + self.contribute_socket(session._pinned_connection) |
| 2718 | + return session._pinned_connection |
| 2719 | + pool_checkout = server.pool.checkout(self) |
| 2720 | + try: |
| 2721 | + conn = await pool_checkout.__aenter__() |
| 2722 | + except BaseException as exc: |
| 2723 | + # __aenter__ raised — pool already cleaned up internally. |
| 2724 | + # Run SDAM error handling so the topology learns about the failure. |
| 2725 | + await self.handle(type(exc), exc) |
| 2726 | + raise |
| 2727 | + self._pool_checkout = pool_checkout |
| 2728 | + try: |
| 2729 | + # Pin this session to the selected server or connection. |
| 2730 | + if ( |
| 2731 | + in_txn |
| 2732 | + and session |
| 2733 | + and server.description.server_type |
| 2734 | + in ( |
| 2735 | + SERVER_TYPE.Mongos, |
| 2736 | + SERVER_TYPE.LoadBalancer, |
| 2737 | + ) |
| 2738 | + ): |
| 2739 | + session._pin(server, conn) |
| 2740 | + self.contribute_socket(conn) |
| 2741 | + if ( |
| 2742 | + self.client._encrypter |
| 2743 | + and not self.client._encrypter._bypass_auto_encryption |
| 2744 | + and conn.max_wire_version < 8 |
| 2745 | + ): |
| 2746 | + raise ConfigurationError( |
| 2747 | + "Auto-encryption requires a minimum MongoDB version of 4.2" |
| 2748 | + ) |
| 2749 | + except BaseException as exc: |
| 2750 | + try: |
| 2751 | + await self.handle(type(exc), exc) |
| 2752 | + finally: |
| 2753 | + # Reset before checkin so pool.checkin() does not decrement ntxns. |
| 2754 | + conn.pinned_txn = False |
| 2755 | + conn.pinned_cursor = False |
| 2756 | + # Clear stale session pin state so future ops don't reuse it. |
| 2757 | + if session and session.in_transaction: |
| 2758 | + await session._unpin() |
| 2759 | + await pool_checkout.__aexit__(type(exc), exc, exc.__traceback__) |
| 2760 | + self._pool_checkout = None |
| 2761 | + raise |
| 2762 | + return conn |
2749 | 2763 |
|
2750 | 2764 | async def __aexit__( |
2751 | 2765 | self, |
2752 | | - exc_type: Optional[type[Exception]], |
2753 | | - exc_val: Optional[Exception], |
| 2766 | + exc_type: Optional[type[BaseException]], |
| 2767 | + exc_val: Optional[BaseException], |
2754 | 2768 | exc_tb: Optional[TracebackType], |
2755 | 2769 | ) -> None: |
2756 | | - return await self.handle(exc_type, exc_val) |
| 2770 | + # Perform SDAM error handling while the connection is still checked out. |
| 2771 | + try: |
| 2772 | + await self.handle(exc_type, exc_val) |
| 2773 | + finally: |
| 2774 | + if self._pool_checkout is not None: |
| 2775 | + await self._pool_checkout.__aexit__(exc_type, exc_val, exc_tb) |
| 2776 | + |
| 2777 | + @classmethod |
| 2778 | + def for_existing_conn( |
| 2779 | + cls, |
| 2780 | + client: AsyncMongoClient, # type: ignore[type-arg] |
| 2781 | + server: Server, |
| 2782 | + session: Optional[AsyncClientSession], |
| 2783 | + conn: AsyncConnection, |
| 2784 | + ) -> _ClientCheckout: |
| 2785 | + """Return a _ClientCheckout for an already-checked-out connection. |
| 2786 | +
|
| 2787 | + Used when SDAM error handling is needed around an existing connection |
| 2788 | + without performing a new pool checkout (e.g. re-running a getMore). |
| 2789 | + """ |
| 2790 | + checkout = cls(client, server, session) |
| 2791 | + checkout.contribute_socket(conn) |
| 2792 | + checkout._existing_conn = conn |
| 2793 | + return checkout |
| 2794 | + |
| 2795 | + |
| 2796 | +class _ClientReadCheckout(_ClientCheckout): |
| 2797 | + """Context manager for read operations. |
| 2798 | +
|
| 2799 | + Extends _ClientCheckout to apply the single-topology read preference |
| 2800 | + adjustment and return the effective read preference alongside the connection. |
| 2801 | + """ |
| 2802 | + |
| 2803 | + __slots__ = ("_read_preference",) |
| 2804 | + |
| 2805 | + def __init__( |
| 2806 | + self, |
| 2807 | + client: AsyncMongoClient, # type: ignore[type-arg] |
| 2808 | + server: Server, |
| 2809 | + session: Optional[AsyncClientSession], |
| 2810 | + read_preference: _ServerMode, |
| 2811 | + ) -> None: |
| 2812 | + super().__init__(client, server, session) |
| 2813 | + self._read_preference: _ServerMode = read_preference |
| 2814 | + |
| 2815 | + async def __aenter__(self) -> tuple[AsyncConnection, _ServerMode]: # type: ignore[override] |
| 2816 | + conn = await super().__aenter__() |
| 2817 | + # The Server Selection Spec says not to send any $readPreference to |
| 2818 | + # standalones and to always send primaryPreferred when directly |
| 2819 | + # connected to a replica set member. |
| 2820 | + # Thread safe: topology type cannot change once set to Single. |
| 2821 | + read_pref = self._read_preference |
| 2822 | + single = self.client._topology.description.topology_type == TOPOLOGY_TYPE.Single |
| 2823 | + if single: |
| 2824 | + if conn.is_repl and not (self.session and self.session.in_transaction): |
| 2825 | + read_pref = ReadPreference.PRIMARY_PREFERRED |
| 2826 | + elif conn.is_standalone: |
| 2827 | + read_pref = ReadPreference.PRIMARY |
| 2828 | + return conn, read_pref |
2757 | 2829 |
|
2758 | 2830 |
|
2759 | 2831 | class _ClientConnectionRetryable(Generic[T]): |
2760 | 2832 | """Responsible for executing retryable connections on read or write operations""" |
2761 | 2833 |
|
| 2834 | + __slots__ = ( |
| 2835 | + "_address", |
| 2836 | + "_always_retryable", |
| 2837 | + "_attempt_number", |
| 2838 | + "_base_backoff_ms", |
| 2839 | + "_bulk", |
| 2840 | + "_client", |
| 2841 | + "_deprioritized_servers", |
| 2842 | + "_func", |
| 2843 | + "_is_aggregate_write", |
| 2844 | + "_is_read", |
| 2845 | + "_is_run_command", |
| 2846 | + "_last_error", |
| 2847 | + "_max_retries", |
| 2848 | + "_operation", |
| 2849 | + "_operation_id", |
| 2850 | + "_read_pref", |
| 2851 | + "_retry_policy", |
| 2852 | + "_retryable", |
| 2853 | + "_retrying", |
| 2854 | + "_server", |
| 2855 | + "_server_selector", |
| 2856 | + "_session", |
| 2857 | + ) |
| 2858 | + |
2762 | 2859 | def __init__( |
2763 | 2860 | self, |
2764 | 2861 | mongo_client: AsyncMongoClient, # type: ignore[type-arg] |
@@ -2791,7 +2888,7 @@ def __init__( |
2791 | 2888 | ) |
2792 | 2889 | self._address = address |
2793 | 2890 | self._server: Server = None # type: ignore |
2794 | | - self._deprioritized_servers: list[Server] = [] |
| 2891 | + self._deprioritized_servers: Optional[list[Server]] = None |
2795 | 2892 | self._operation = operation |
2796 | 2893 | # Only generate an operation id when APM/logging is enabled |
2797 | 2894 | if operation_id is None and ( |
@@ -2946,6 +3043,8 @@ async def run(self) -> T: |
2946 | 3043 | self._client.topology_description.topology_type_name == "Sharded" |
2947 | 3044 | or (overloaded and self._client.options.enable_overload_retargeting) |
2948 | 3045 | ): |
| 3046 | + if self._deprioritized_servers is None: |
| 3047 | + self._deprioritized_servers = [] |
2949 | 3048 | self._deprioritized_servers.append(self._server) |
2950 | 3049 |
|
2951 | 3050 | self._always_retryable = always_retryable |
|
0 commit comments