Skip to content

Commit 55b21ec

Browse files
committed
PYTHON-5846 Move duration tracking into telemetry classes
_CmapTelemetry now owns connection-creation and checkout durations: connection_created() starts the clock, connection_ready() computes it; checkout_started() starts the clock, checkout_succeeded/failed() compute it. _HeartbeatTelemetry.started() starts the clock; failed() computes its own duration instead of receiving it as a parameter. Removes checkout_started_time from Pool._get_conn, _raise_if_not_ready, and _raise_wait_queue_timeout, and removes AsyncConnection.creation_time.
1 parent 265971c commit 55b21ec

5 files changed

Lines changed: 48 additions & 62 deletions

File tree

pymongo/_telemetry.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import datetime
2020
import logging
2121
import queue
22+
import time
2223
from collections.abc import MutableMapping
2324
from typing import TYPE_CHECKING, Any, Optional
2425

@@ -198,7 +199,15 @@ def failed(
198199
class _CmapTelemetry:
199200
"""Combines CMAP structured logging and APM event publishing for pool and connection events."""
200201

201-
__slots__ = ("_address", "_client_id", "_listeners", "_should_log", "_should_publish")
202+
__slots__ = (
203+
"_address",
204+
"_checkout_start",
205+
"_client_id",
206+
"_conn_created_start",
207+
"_listeners",
208+
"_should_log",
209+
"_should_publish",
210+
)
202211

203212
def __init__(
204213
self,
@@ -258,13 +267,15 @@ def pool_closed(self) -> None:
258267
self._listeners.publish_pool_closed(self._address)
259268

260269
def connection_created(self, conn_id: int) -> None:
270+
self._conn_created_start = time.monotonic()
261271
# Log before publishing to prevent potential listener preemption in tests.
262272
self._emit_log(_ConnectionStatusMessage.CONN_CREATED, driverConnectionId=conn_id)
263273
if self._should_publish:
264274
assert self._listeners is not None
265275
self._listeners.publish_connection_created(self._address, conn_id)
266276

267-
def connection_ready(self, conn_id: int, duration: float) -> None:
277+
def connection_ready(self, conn_id: int) -> None:
278+
duration = max(0.0, time.monotonic() - self._conn_created_start)
268279
# Log before publishing to prevent potential listener preemption in tests.
269280
self._emit_log(
270281
_ConnectionStatusMessage.CONN_READY,
@@ -287,12 +298,14 @@ def connection_closed(self, conn_id: int, reason: str) -> None:
287298
)
288299

289300
def checkout_started(self) -> None:
301+
self._checkout_start = time.monotonic()
290302
if self._should_publish:
291303
assert self._listeners is not None
292304
self._listeners.publish_connection_check_out_started(self._address)
293305
self._emit_log(_ConnectionStatusMessage.CHECKOUT_STARTED)
294306

295-
def checkout_succeeded(self, conn_id: int, duration: float) -> None:
307+
def checkout_succeeded(self, conn_id: int) -> None:
308+
duration = max(0.0, time.monotonic() - self._checkout_start)
296309
if self._should_publish:
297310
assert self._listeners is not None
298311
self._listeners.publish_connection_checked_out(self._address, conn_id, duration)
@@ -302,7 +315,8 @@ def checkout_succeeded(self, conn_id: int, duration: float) -> None:
302315
durationMS=duration,
303316
)
304317

305-
def checkout_failed(self, reason: str, error: str, duration: float) -> None:
318+
def checkout_failed(self, reason: str, error: str) -> None:
319+
duration = max(0.0, time.monotonic() - self._checkout_start)
306320
if self._should_publish:
307321
assert self._listeners is not None
308322
self._listeners.publish_connection_check_out_failed(self._address, error, duration)
@@ -329,7 +343,7 @@ class _HeartbeatTelemetry:
329343
context, then :meth:`succeeded` or :meth:`failed` when the outcome is known.
330344
"""
331345

332-
__slots__ = ("_address", "_awaited", "_listeners", "_publish", "_topology_id")
346+
__slots__ = ("_address", "_awaited", "_listeners", "_publish", "_start", "_topology_id")
333347

334348
def __init__(
335349
self,
@@ -347,6 +361,7 @@ def __init__(
347361

348362
def started(self) -> None:
349363
"""Publish the APM heartbeat-started event (before connection checkout)."""
364+
self._start = time.monotonic()
350365
if self._publish:
351366
assert self._listeners is not None
352367
self._listeners.publish_server_heartbeat_started(self._address, self._awaited)
@@ -392,8 +407,9 @@ def succeeded(
392407
reply=response.document,
393408
)
394409

395-
def failed(self, duration: float, error: Exception, conn_id: Optional[int]) -> None:
410+
def failed(self, error: Exception, conn_id: Optional[int]) -> None:
396411
"""Emit the FAILED log entry and APM event."""
412+
duration = max(0.0, time.monotonic() - self._start)
397413
if self._publish:
398414
assert self._listeners is not None
399415
self._listeners.publish_server_heartbeat_failed(

pymongo/asynchronous/monitor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,17 +259,15 @@ async def _check_server(self) -> ServerDescription:
259259
"""
260260
self._conn_id = None
261261
self._current_hb = None
262-
start = time.monotonic()
263262
try:
264263
return await self._check_once()
265264
except ReferenceError:
266265
raise
267266
except Exception as error:
268267
_sanitize(error)
269268
address = self._server_description.address
270-
duration = _monotonic_duration(start)
271269
if self._current_hb is not None:
272-
self._current_hb.failed(duration, error, self._conn_id)
270+
self._current_hb.failed(error, self._conn_id)
273271
await self._reset_connection()
274272
if isinstance(error, _OperationCancelled):
275273
raise

pymongo/asynchronous/pool.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ def __init__(
166166
self.active = False
167167
self.last_timeout = self.opts.socket_timeout
168168
self.connect_rtt = 0.0
169-
self.creation_time = time.monotonic()
170169
# For gossiping $clusterTime from the connection handshake to the client.
171170
self._cluster_time = None
172171

@@ -467,8 +466,7 @@ async def authenticate(self, reauthenticate: bool = False) -> None:
467466

468467
await auth.authenticate(creds, self, reauthenticate=reauthenticate)
469468
self.ready = True
470-
duration = time.monotonic() - self.creation_time
471-
self._telemetry.connection_ready(self.id, duration)
469+
self._telemetry.connection_ready(self.id)
472470

473471
def validate_session(
474472
self, client: Optional[AsyncMongoClient[Any]], session: Optional[AsyncClientSession]
@@ -978,13 +976,11 @@ async def checkout(
978976
979977
:param handler: A _MongoClientErrorHandler.
980978
"""
981-
checkout_started_time = time.monotonic()
982979
self._telemetry.checkout_started()
983980

984-
conn = await self._get_conn(checkout_started_time, handler=handler)
981+
conn = await self._get_conn(handler=handler)
985982

986-
duration = time.monotonic() - checkout_started_time
987-
self._telemetry.checkout_succeeded(conn.id, duration)
983+
self._telemetry.checkout_succeeded(conn.id)
988984
try:
989985
async with self.lock:
990986
self.active_contexts.add(conn.cancel_context)
@@ -1015,14 +1011,12 @@ async def checkout(
10151011
elif conn.active:
10161012
await self.checkin(conn)
10171013

1018-
def _raise_if_not_ready(self, checkout_started_time: float, emit_event: bool) -> None:
1014+
def _raise_if_not_ready(self, emit_event: bool) -> None:
10191015
if self.state != PoolState.READY:
10201016
if emit_event:
1021-
duration = time.monotonic() - checkout_started_time
10221017
self._telemetry.checkout_failed(
10231018
"An error occurred while trying to establish a new connection",
10241019
ConnectionCheckOutFailedReason.CONN_ERROR,
1025-
duration,
10261020
)
10271021

10281022
details = _get_timeout_details(self.opts)
@@ -1031,7 +1025,7 @@ def _raise_if_not_ready(self, checkout_started_time: float, emit_event: bool) ->
10311025
)
10321026

10331027
async def _get_conn(
1034-
self, checkout_started_time: float, handler: Optional[_MongoClientErrorHandler] = None
1028+
self, handler: Optional[_MongoClientErrorHandler] = None
10351029
) -> AsyncConnection:
10361030
"""Get or create a AsyncConnection. Can raise ConnectionFailure."""
10371031
# We use the pid here to avoid issues with fork / multiprocessing.
@@ -1041,11 +1035,9 @@ async def _get_conn(
10411035
await self.reset_without_pause()
10421036

10431037
if self.closed:
1044-
duration = time.monotonic() - checkout_started_time
10451038
self._telemetry.checkout_failed(
10461039
"Connection pool was closed",
10471040
ConnectionCheckOutFailedReason.POOL_CLOSED,
1048-
duration,
10491041
)
10501042
raise _PoolClosedError(
10511043
"Attempted to check out a connection from closed connection pool"
@@ -1063,16 +1055,16 @@ async def _get_conn(
10631055
deadline = None
10641056

10651057
async with self.size_cond:
1066-
self._raise_if_not_ready(checkout_started_time, emit_event=True)
1058+
self._raise_if_not_ready(emit_event=True)
10671059
while not (self.requests < self.max_pool_size):
10681060
timeout = deadline - time.monotonic() if deadline else None
10691061
if not await _async_cond_wait(self.size_cond, timeout):
10701062
# Timed out, notify the next thread to ensure a
10711063
# timeout doesn't consume the condition.
10721064
if self.requests < self.max_pool_size:
10731065
self.size_cond.notify()
1074-
self._raise_wait_queue_timeout(checkout_started_time)
1075-
self._raise_if_not_ready(checkout_started_time, emit_event=True)
1066+
self._raise_wait_queue_timeout()
1067+
self._raise_if_not_ready(emit_event=True)
10761068
self.requests += 1
10771069

10781070
# We've now acquired the semaphore and must release it on error.
@@ -1087,7 +1079,7 @@ async def _get_conn(
10871079
# CMAP: we MUST wait for either maxConnecting OR for a socket
10881080
# to be checked back into the pool.
10891081
async with self._max_connecting_cond:
1090-
self._raise_if_not_ready(checkout_started_time, emit_event=False)
1082+
self._raise_if_not_ready(emit_event=False)
10911083
while not (self.conns or self._pending < self._max_connecting):
10921084
timeout = deadline - time.monotonic() if deadline else None
10931085
if not await _async_cond_wait(self._max_connecting_cond, timeout):
@@ -1096,8 +1088,8 @@ async def _get_conn(
10961088
if self.conns or self._pending < self._max_connecting:
10971089
self._max_connecting_cond.notify()
10981090
emitted_event = True
1099-
self._raise_wait_queue_timeout(checkout_started_time)
1100-
self._raise_if_not_ready(checkout_started_time, emit_event=False)
1091+
self._raise_wait_queue_timeout()
1092+
self._raise_if_not_ready(emit_event=False)
11011093

11021094
try:
11031095
conn = self.conns.popleft()
@@ -1126,11 +1118,9 @@ async def _get_conn(
11261118
self.size_cond.notify()
11271119

11281120
if not emitted_event:
1129-
duration = time.monotonic() - checkout_started_time
11301121
self._telemetry.checkout_failed(
11311122
"An error occurred while trying to establish a new connection",
11321123
ConnectionCheckOutFailedReason.CONN_ERROR,
1133-
duration,
11341124
)
11351125
raise
11361126

@@ -1222,12 +1212,10 @@ async def _perished(self, conn: AsyncConnection) -> bool:
12221212

12231213
return False
12241214

1225-
def _raise_wait_queue_timeout(self, checkout_started_time: float) -> NoReturn:
1226-
duration = time.monotonic() - checkout_started_time
1215+
def _raise_wait_queue_timeout(self) -> NoReturn:
12271216
self._telemetry.checkout_failed(
12281217
"Wait queue timeout elapsed without a connection becoming available",
12291218
ConnectionCheckOutFailedReason.TIMEOUT,
1230-
duration,
12311219
)
12321220
timeout = _csot.get_timeout() or self.opts.wait_queue_timeout
12331221
if self.opts.load_balanced:

pymongo/synchronous/monitor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,15 @@ def _check_server(self) -> ServerDescription:
257257
"""
258258
self._conn_id = None
259259
self._current_hb = None
260-
start = time.monotonic()
261260
try:
262261
return self._check_once()
263262
except ReferenceError:
264263
raise
265264
except Exception as error:
266265
_sanitize(error)
267266
address = self._server_description.address
268-
duration = _monotonic_duration(start)
269267
if self._current_hb is not None:
270-
self._current_hb.failed(duration, error, self._conn_id)
268+
self._current_hb.failed(error, self._conn_id)
271269
self._reset_connection()
272270
if isinstance(error, _OperationCancelled):
273271
raise

0 commit comments

Comments
 (0)