|
37 | 37 | from bson import DEFAULT_CODEC_OPTIONS
|
38 | 38 | from pymongo import _csot, helpers_shared
|
39 | 39 | from pymongo.asynchronous.client_session import _validate_session_write_concern
|
40 |
| -from pymongo.asynchronous.helpers import _handle_reauth |
| 40 | +from pymongo.asynchronous.helpers import _backoff, _handle_reauth |
41 | 41 | from pymongo.asynchronous.network import command
|
42 | 42 | from pymongo.common import (
|
43 | 43 | MAX_BSON_SIZE,
|
@@ -791,6 +791,7 @@ def __init__(
|
791 | 791 | self._max_connecting = self.opts.max_connecting
|
792 | 792 | self._pending = 0
|
793 | 793 | self._client_id = client_id
|
| 794 | + self._backoff = 0 |
794 | 795 | if self.enabled_for_cmap:
|
795 | 796 | assert self.opts._event_listeners is not None
|
796 | 797 | self.opts._event_listeners.publish_pool_created(
|
@@ -846,6 +847,8 @@ async def _reset(
|
846 | 847 | async with self.size_cond:
|
847 | 848 | if self.closed:
|
848 | 849 | return
|
| 850 | + # Clear the backoff state. |
| 851 | + self._backoff = 0 |
849 | 852 | if self.opts.pause_enabled and pause and not self.opts.load_balanced:
|
850 | 853 | old_state, self.state = self.state, PoolState.PAUSED
|
851 | 854 | self.gen.inc(service_id)
|
@@ -937,6 +940,12 @@ async def update_is_writable(self, is_writable: Optional[bool]) -> None:
|
937 | 940 | for _socket in self.conns:
|
938 | 941 | _socket.update_is_writable(self.is_writable) # type: ignore[arg-type]
|
939 | 942 |
|
| 943 | + async def backoff(self, service_id: Optional[ObjectId] = None) -> None: |
| 944 | + # Mark the pool as in backoff. |
| 945 | + # TODO: how to handle load balancers? |
| 946 | + self._backoff += 1 |
| 947 | + # TODO: emit a message. |
| 948 | + |
940 | 949 | async def reset(
|
941 | 950 | self, service_id: Optional[ObjectId] = None, interrupt_connections: bool = False
|
942 | 951 | ) -> None:
|
@@ -994,7 +1003,8 @@ async def remove_stale_sockets(self, reference_generation: int) -> None:
|
994 | 1003 | async with self._max_connecting_cond:
|
995 | 1004 | # If maxConnecting connections are already being created
|
996 | 1005 | # by this pool then try again later instead of waiting.
|
997 |
| - if self._pending >= self._max_connecting: |
| 1006 | + max_connecting = 1 if self._backoff else self._max_connecting |
| 1007 | + if self._pending >= max_connecting: |
998 | 1008 | return
|
999 | 1009 | self._pending += 1
|
1000 | 1010 | incremented = True
|
@@ -1051,6 +1061,10 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
|
1051 | 1061 | driverConnectionId=conn_id,
|
1052 | 1062 | )
|
1053 | 1063 |
|
| 1064 | + # Apply backoff if applicable. |
| 1065 | + if self._backoff: |
| 1066 | + await asyncio.sleep(_backoff(self._backoff)) |
| 1067 | + |
1054 | 1068 | try:
|
1055 | 1069 | networking_interface = await _configured_protocol_interface(self.address, self.opts)
|
1056 | 1070 | # Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
@@ -1103,6 +1117,8 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
|
1103 | 1117 | if handler:
|
1104 | 1118 | await handler.client._topology.receive_cluster_time(conn._cluster_time)
|
1105 | 1119 |
|
| 1120 | + # Clear the backoff state. |
| 1121 | + self._backoff = 0 |
1106 | 1122 | return conn
|
1107 | 1123 |
|
1108 | 1124 | @contextlib.asynccontextmanager
|
@@ -1279,12 +1295,13 @@ async def _get_conn(
|
1279 | 1295 | # to be checked back into the pool.
|
1280 | 1296 | async with self._max_connecting_cond:
|
1281 | 1297 | self._raise_if_not_ready(checkout_started_time, emit_event=False)
|
1282 |
| - while not (self.conns or self._pending < self._max_connecting): |
| 1298 | + max_connecting = 1 if self._backoff else self._max_connecting |
| 1299 | + while not (self.conns or self._pending < max_connecting): |
1283 | 1300 | timeout = deadline - time.monotonic() if deadline else None
|
1284 | 1301 | if not await _async_cond_wait(self._max_connecting_cond, timeout):
|
1285 | 1302 | # Timed out, notify the next thread to ensure a
|
1286 | 1303 | # timeout doesn't consume the condition.
|
1287 |
| - if self.conns or self._pending < self._max_connecting: |
| 1304 | + if self.conns or self._pending < max_connecting: |
1288 | 1305 | self._max_connecting_cond.notify()
|
1289 | 1306 | emitted_event = True
|
1290 | 1307 | self._raise_wait_queue_timeout(checkout_started_time)
|
@@ -1395,6 +1412,20 @@ async def checkin(self, conn: AsyncConnection) -> None:
|
1395 | 1412 | # Pool.reset().
|
1396 | 1413 | if self.stale_generation(conn.generation, conn.service_id):
|
1397 | 1414 | close_conn = True
|
| 1415 | + # If in backoff state, check the conn's readiness. |
| 1416 | + elif self._backoff: |
| 1417 | + # Set a 1ms read deadline and attempt to read 1 byte from the connection. |
| 1418 | + # Expect it to block for 1ms then return a deadline exceeded error. If it |
| 1419 | + # returns any other error, the connection is not usable, so return false. |
| 1420 | + # If it doesn't return an error and actually reads data, the connection is |
| 1421 | + # also not usable, so return false. |
| 1422 | + conn.conn.get_conn.settimeout(0.001) |
| 1423 | + close_conn = True |
| 1424 | + try: |
| 1425 | + conn.conn.get_conn.read() |
| 1426 | + except Exception as _: |
| 1427 | + # TODO: verify the exception |
| 1428 | + close_conn = False |
1398 | 1429 | else:
|
1399 | 1430 | conn.update_last_checkin_time()
|
1400 | 1431 | conn.update_is_writable(bool(self.is_writable))
|
|
0 commit comments