From cec7bc2f45c90c472ef9cc6e87b9c9ae0a1cbd71 Mon Sep 17 00:00:00 2001 From: Shane Harvey Date: Thu, 14 Aug 2025 11:03:20 -0700 Subject: [PATCH 1/2] DRIVERS-1934 POC exponential backoff in withTransaction --- pymongo/asynchronous/client_session.py | 10 ++++++++++ pymongo/synchronous/client_session.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index c30fc6679f..68b566dd16 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -135,7 +135,9 @@ from __future__ import annotations +import asyncio import collections +import random import time import uuid from collections.abc import Mapping as _Mapping @@ -471,6 +473,8 @@ def _max_time_expired_error(exc: PyMongoError) -> bool: # This limit is non-configurable and was chosen to be twice the 60 second # default value of MongoDB's `transactionLifetimeLimitSeconds` parameter. _WITH_TRANSACTION_RETRY_TIME_LIMIT = 120 +_BACKOFF_MAX = 1 +_BACKOFF_INITIAL = 0.050 # 50ms initial backoff def _within_time_limit(start_time: float) -> bool: @@ -700,7 +704,13 @@ async def callback(session, custom_arg, custom_kwarg=None): https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback """ start_time = time.monotonic() + retry = 0 while True: + if retry > 1: # Implement exponential backoff after the first retry. + jitter = random.random() # noqa: S311 + backoff = jitter * min(_BACKOFF_INITIAL * (2 ** (retry - 1)), _BACKOFF_MAX) + await asyncio.sleep(backoff) + retry += 1 await self.start_transaction( read_concern, write_concern, read_preference, max_commit_time_ms ) diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 68a01dd7e7..151f6f9184 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -136,6 +136,7 @@ from __future__ import annotations import collections +import random import time import uuid from collections.abc import Mapping as _Mapping @@ -470,6 +471,8 @@ def _max_time_expired_error(exc: PyMongoError) -> bool: # This limit is non-configurable and was chosen to be twice the 60 second # default value of MongoDB's `transactionLifetimeLimitSeconds` parameter. _WITH_TRANSACTION_RETRY_TIME_LIMIT = 120 +_BACKOFF_MAX = 1 +_BACKOFF_INITIAL = 0.050 # 50ms initial backoff def _within_time_limit(start_time: float) -> bool: @@ -699,7 +702,13 @@ def callback(session, custom_arg, custom_kwarg=None): https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback """ start_time = time.monotonic() + retry = 0 while True: + if retry > 1: # Implement exponential backoff after the first retry. + jitter = random.random() # noqa: S311 + backoff = jitter * min(_BACKOFF_INITIAL * (2 ** (retry - 1)), _BACKOFF_MAX) + time.sleep(backoff) + retry += 1 self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms) try: ret = callback(self) From 86172fc0447a2c1e7399d77f483705128634b921 Mon Sep 17 00:00:00 2001 From: Shane Harvey Date: Tue, 19 Aug 2025 10:14:53 -0700 Subject: [PATCH 2/2] PYTHON-5504 Start backoff on first retry not second --- pymongo/asynchronous/client_session.py | 4 ++-- pymongo/synchronous/client_session.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 68b566dd16..4bb927d995 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -706,9 +706,9 @@ async def callback(session, custom_arg, custom_kwarg=None): start_time = time.monotonic() retry = 0 while True: - if retry > 1: # Implement exponential backoff after the first retry. + if retry: # Implement exponential backoff on retry. jitter = random.random() # noqa: S311 - backoff = jitter * min(_BACKOFF_INITIAL * (2 ** (retry - 1)), _BACKOFF_MAX) + backoff = jitter * min(_BACKOFF_INITIAL * (2**retry), _BACKOFF_MAX) await asyncio.sleep(backoff) retry += 1 await self.start_transaction( diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 151f6f9184..a8f03fac74 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -704,9 +704,9 @@ def callback(session, custom_arg, custom_kwarg=None): start_time = time.monotonic() retry = 0 while True: - if retry > 1: # Implement exponential backoff after the first retry. + if retry: # Implement exponential backoff on retry. jitter = random.random() # noqa: S311 - backoff = jitter * min(_BACKOFF_INITIAL * (2 ** (retry - 1)), _BACKOFF_MAX) + backoff = jitter * min(_BACKOFF_INITIAL * (2**retry), _BACKOFF_MAX) time.sleep(backoff) retry += 1 self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)