3737from bson import DEFAULT_CODEC_OPTIONS
3838from pymongo import _csot , helpers_shared
3939from 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
4141from pymongo .asynchronous .network import command
4242from pymongo .common import (
4343 MAX_BSON_SIZE ,
@@ -788,9 +788,9 @@ def __init__(
788788 # Enforces: maxConnecting
789789 # Also used for: clearing the wait queue
790790 self ._max_connecting_cond = _async_create_condition (self .lock )
791- self ._max_connecting = self .opts .max_connecting
792791 self ._pending = 0
793792 self ._client_id = client_id
793+ self ._backoff = 0
794794 if self .enabled_for_cmap :
795795 assert self .opts ._event_listeners is not None
796796 self .opts ._event_listeners .publish_pool_created (
@@ -846,6 +846,8 @@ async def _reset(
846846 async with self .size_cond :
847847 if self .closed :
848848 return
849+ # Clear the backoff state.
850+ self ._backoff = 0
849851 if self .opts .pause_enabled and pause and not self .opts .load_balanced :
850852 old_state , self .state = self .state , PoolState .PAUSED
851853 self .gen .inc (service_id )
@@ -928,6 +930,11 @@ async def _reset(
928930 for conn in sockets :
929931 await conn .close_conn (ConnectionClosedReason .STALE )
930932
933+ @property
934+ def max_connecting (self ) -> int :
935+ """The current max connecting limit for the pool."""
936+ return 1 if self ._backoff else self .opts .max_connecting
937+
931938 async def update_is_writable (self , is_writable : Optional [bool ]) -> None :
932939 """Updates the is_writable attribute on all sockets currently in the
933940 Pool.
@@ -994,7 +1001,7 @@ async def remove_stale_sockets(self, reference_generation: int) -> None:
9941001 async with self ._max_connecting_cond :
9951002 # If maxConnecting connections are already being created
9961003 # by this pool then try again later instead of waiting.
997- if self ._pending >= self ._max_connecting :
1004+ if self ._pending >= self .max_connecting :
9981005 return
9991006 self ._pending += 1
10001007 incremented = True
@@ -1022,6 +1029,30 @@ async def remove_stale_sockets(self, reference_generation: int) -> None:
10221029 self .requests -= 1
10231030 self .size_cond .notify ()
10241031
1032+ def _handle_connection_error (self , error : BaseException , phase : str , conn_id : int ) -> None :
1033+ # Handle system overload condition for non-sdam pools.
1034+ # Look for an AutoReconnect error raised from a ConnectionResetError with
1035+ # errno == errno.ECONNRESET or raised from an OSError that we've created due to
1036+ # a closed connection.
1037+ # If found, set backoff and add error labels.
1038+ if self .is_sdam or type (error ) != AutoReconnect :
1039+ return
1040+ self ._backoff += 1
1041+ error ._add_error_label ("SystemOverloadedError" )
1042+ error ._add_error_label ("RetryableError" )
1043+ # Log the pool backoff message.
1044+ if self .enabled_for_logging and _CONNECTION_LOGGER .isEnabledFor (logging .DEBUG ):
1045+ _debug_log (
1046+ _CONNECTION_LOGGER ,
1047+ message = _ConnectionStatusMessage .POOL_BACKOFF ,
1048+ clientId = self ._client_id ,
1049+ serverHost = self .address [0 ],
1050+ serverPort = self .address [1 ],
1051+ driverConnectionId = conn_id ,
1052+ reason = _verbose_connection_error_reason (ConnectionClosedReason .POOL_BACKOFF ),
1053+ error = ConnectionClosedReason .POOL_BACKOFF ,
1054+ )
1055+
10251056 async def connect (self , handler : Optional [_MongoClientErrorHandler ] = None ) -> AsyncConnection :
10261057 """Connect to Mongo and return a new AsyncConnection.
10271058
@@ -1051,8 +1082,17 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
10511082 driverConnectionId = conn_id ,
10521083 )
10531084
1085+ # Apply backoff if applicable.
1086+ if self ._backoff :
1087+ await asyncio .sleep (_backoff (self ._backoff ))
1088+
1089+ # Pass a context to determine if we successfully create a configured socket.
1090+ context = dict (has_created_socket = False )
1091+
10541092 try :
1055- networking_interface = await _configured_protocol_interface (self .address , self .opts )
1093+ networking_interface = await _configured_protocol_interface (
1094+ self .address , self .opts , context = context
1095+ )
10561096 # Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
10571097 except BaseException as error :
10581098 async with self .lock :
@@ -1073,10 +1113,11 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
10731113 reason = _verbose_connection_error_reason (ConnectionClosedReason .ERROR ),
10741114 error = ConnectionClosedReason .ERROR ,
10751115 )
1116+ if context ["has_created_socket" ]:
1117+ self ._handle_connection_error (error , "handshake" , conn_id )
10761118 if isinstance (error , (IOError , OSError , * SSLErrors )):
10771119 details = _get_timeout_details (self .opts )
10781120 _raise_connection_failure (self .address , error , timeout_details = details )
1079-
10801121 raise
10811122
10821123 conn = AsyncConnection (networking_interface , self , self .address , conn_id , self .is_sdam ) # type: ignore[arg-type]
@@ -1094,15 +1135,18 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
10941135
10951136 await conn .authenticate ()
10961137 # Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
1097- except BaseException :
1138+ except BaseException as e :
10981139 async with self .lock :
10991140 self .active_contexts .discard (conn .cancel_context )
1141+ self ._handle_connection_error (e , "hello" , conn_id )
11001142 await conn .close_conn (ConnectionClosedReason .ERROR )
11011143 raise
11021144
11031145 if handler :
11041146 await handler .client ._topology .receive_cluster_time (conn ._cluster_time )
11051147
1148+ # Clear the backoff state.
1149+ self ._backoff = 0
11061150 return conn
11071151
11081152 @contextlib .asynccontextmanager
@@ -1279,12 +1323,12 @@ async def _get_conn(
12791323 # to be checked back into the pool.
12801324 async with self ._max_connecting_cond :
12811325 self ._raise_if_not_ready (checkout_started_time , emit_event = False )
1282- while not (self .conns or self ._pending < self ._max_connecting ):
1326+ while not (self .conns or self ._pending < self .max_connecting ):
12831327 timeout = deadline - time .monotonic () if deadline else None
12841328 if not await _async_cond_wait (self ._max_connecting_cond , timeout ):
12851329 # Timed out, notify the next thread to ensure a
12861330 # timeout doesn't consume the condition.
1287- if self .conns or self ._pending < self ._max_connecting :
1331+ if self .conns or self ._pending < self .max_connecting :
12881332 self ._max_connecting_cond .notify ()
12891333 emitted_event = True
12901334 self ._raise_wait_queue_timeout (checkout_started_time )
@@ -1425,8 +1469,8 @@ async def _perished(self, conn: AsyncConnection) -> bool:
14251469 :class:`~pymongo.errors.AutoReconnect` exceptions on server
14261470 hiccups, etc. We only check if the socket was closed by an external
14271471 error if it has been > 1 second since the socket was checked into the
1428- pool, to keep performance reasonable - we can't avoid AutoReconnects
1429- completely anyway.
1472+ pool, or we are in backoff mode, to keep performance reasonable -
1473+ we can't avoid AutoReconnects completely anyway.
14301474 """
14311475 idle_time_seconds = conn .idle_time_seconds ()
14321476 # If socket is idle, open a new one.
@@ -1437,8 +1481,11 @@ async def _perished(self, conn: AsyncConnection) -> bool:
14371481 await conn .close_conn (ConnectionClosedReason .IDLE )
14381482 return True
14391483
1440- if self ._check_interval_seconds is not None and (
1441- self ._check_interval_seconds == 0 or idle_time_seconds > self ._check_interval_seconds
1484+ check_interval_seconds = self ._check_interval_seconds
1485+ if self ._backoff :
1486+ check_interval_seconds = 0
1487+ if check_interval_seconds is not None and (
1488+ check_interval_seconds == 0 or idle_time_seconds > check_interval_seconds
14421489 ):
14431490 if conn .conn_closed ():
14441491 await conn .close_conn (ConnectionClosedReason .ERROR )
0 commit comments