From effab7a67beb357d814933c6fe3040832014e767 Mon Sep 17 00:00:00 2001 From: Evgeniy Martynenko Date: Wed, 12 Nov 2025 11:39:34 +0300 Subject: [PATCH] chore: Replace archived backoff with tenacity The backoff library is no longer maintained. This commit replaces it with tenacity, which provides equivalent functionality for retry logic. Changes: - Replace backoff dependency with tenacity>=9.1.2 in setup.py - Update retry decorators in gql/client.py to use tenacity API - Update all documentation examples to use tenacity instead of backoff - Update code examples to use tenacity retry decorators The functionality remains the same: - retry_connect: exponential backoff with max 60 seconds (infinite retries) - retry_execute: exponential backoff, 5 attempts, excluding TransportQueryError --- docs/advanced/async_advanced_usage.rst | 24 ++++-- docs/advanced/async_permanent_session.rst | 77 +++++++++++++------ .../reconnecting_mutation_http.py | 10 +-- .../code_examples/reconnecting_mutation_ws.py | 10 +-- gql/client.py | 36 +++++---- setup.py | 2 +- 6 files changed, 102 insertions(+), 57 deletions(-) diff --git a/docs/advanced/async_advanced_usage.rst b/docs/advanced/async_advanced_usage.rst index 4164cb37..78952d0f 100644 --- a/docs/advanced/async_advanced_usage.rst +++ b/docs/advanced/async_advanced_usage.rst @@ -6,7 +6,7 @@ Async advanced usage It is possible to send multiple GraphQL queries (query, mutation or subscription) in parallel, on the same websocket connection, using asyncio tasks. -In order to retry in case of connection failure, we can use the great `backoff`_ module. +In order to retry in case of connection failure, we can use the great `tenacity`_ module. .. code-block:: python @@ -28,10 +28,22 @@ In order to retry in case of connection failure, we can use the great `backoff`_ async for result in session.subscribe(subscription2): print(result) - # Then create a couroutine which will connect to your API and run all your queries as tasks. - # We use a `backoff` decorator to reconnect using exponential backoff in case of connection failure. - - @backoff.on_exception(backoff.expo, Exception, max_time=300) + # Then create a couroutine which will connect to your API and run all your + # queries as tasks. We use a `tenacity` retry decorator to reconnect using + # exponential backoff in case of connection failure. + + from tenacity import ( + retry, + retry_if_exception_type, + stop_after_delay, + wait_exponential, + ) + + @retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_delay(300), # max_time in seconds + wait=wait_exponential(), + ) async def graphql_connection(): transport = WebsocketsTransport(url="wss://YOUR_URL") @@ -54,4 +66,4 @@ Subscriptions tasks can be stopped at any time by running task.cancel() -.. _backoff: https://github.com/litl/backoff +.. _tenacity: https://github.com/jd/tenacity diff --git a/docs/advanced/async_permanent_session.rst b/docs/advanced/async_permanent_session.rst index e42010cf..885d2fd2 100644 --- a/docs/advanced/async_permanent_session.rst +++ b/docs/advanced/async_permanent_session.rst @@ -36,19 +36,22 @@ Retries Connection retries ^^^^^^^^^^^^^^^^^^ -With :code:`reconnecting=True`, gql will use the `backoff`_ module to repeatedly try to connect with -exponential backoff and jitter with a maximum delay of 60 seconds by default. +With :code:`reconnecting=True`, gql will use the `tenacity`_ module to repeatedly +try to connect with exponential backoff and jitter with a maximum delay of +60 seconds by default. You can change the default reconnecting profile by providing your own -backoff decorator to the :code:`retry_connect` argument. +retry decorator (from tenacity) to the :code:`retry_connect` argument. .. code-block:: python + from tenacity import retry, retry_if_exception_type, wait_exponential + # Here wait maximum 5 minutes between connection retries - retry_connect = backoff.on_exception( - backoff.expo, # wait generator (here: exponential backoff) - Exception, # which exceptions should cause a retry (here: everything) - max_value=300, # max wait time in seconds + retry_connect = retry( + # which exceptions should cause a retry (here: everything) + retry=retry_if_exception_type(Exception), + wait=wait_exponential(max=300), # max wait time in seconds ) session = await client.connect_async( reconnecting=True, @@ -66,32 +69,49 @@ There is no retry in case of a :code:`TransportQueryError` exception as it indic the connection to the backend is working correctly. You can change the default execute retry profile by providing your own -backoff decorator to the :code:`retry_execute` argument. +retry decorator (from tenacity) to the :code:`retry_execute` argument. .. code-block:: python + from tenacity import ( + retry, + retry_if_exception_type, + stop_after_attempt, + wait_exponential, + ) + # Here Only 3 tries for execute calls - retry_execute = backoff.on_exception( - backoff.expo, - Exception, - max_tries=3, + retry_execute = retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_attempt(3), + wait=wait_exponential(), ) session = await client.connect_async( reconnecting=True, retry_execute=retry_execute, ) -If you don't want any retry on the execute calls, you can disable the retries with :code:`retry_execute=False` +If you don't want any retry on the execute calls, you can disable the retries +with :code:`retry_execute=False` .. note:: If you want to retry even with :code:`TransportQueryError` exceptions, - then you need to make your own backoff decorator on your own method: + then you need to make your own retry decorator (from tenacity) on your own method: .. code-block:: python - @backoff.on_exception(backoff.expo, - Exception, - max_tries=3) + from tenacity import ( + retry, + retry_if_exception_type, + stop_after_attempt, + wait_exponential, + ) + + @retry( + retry=retry_if_exception_type(Exception), + stop=stop_after_attempt(3), + wait=wait_exponential(), + ) async def execute_with_retry(session, query): return await session.execute(query) @@ -100,14 +120,25 @@ Subscription retries There is no :code:`retry_subscribe` as it is not feasible with async generators. If you want retries for your subscriptions, then you can do it yourself -with backoff decorators on your methods. +with retry decorators (from tenacity) on your methods. .. code-block:: python - @backoff.on_exception(backoff.expo, - Exception, - max_tries=3, - giveup=lambda e: isinstance(e, TransportQueryError)) + from tenacity import ( + retry, + retry_if_exception_type, + retry_unless_exception_type, + stop_after_attempt, + wait_exponential, + ) + from gql.transport.exceptions import TransportQueryError + + @retry( + retry=retry_if_exception_type(Exception) + & retry_unless_exception_type(TransportQueryError), + stop=stop_after_attempt(3), + wait=wait_exponential(), + ) async def execute_subscription1(session): async for result in session.subscribe(subscription1): print(result) @@ -123,4 +154,4 @@ Console example .. literalinclude:: ../code_examples/console_async.py .. _difficult to manage: https://github.com/graphql-python/gql/issues/179 -.. _backoff: https://github.com/litl/backoff +.. _tenacity: https://github.com/jd/tenacity diff --git a/docs/code_examples/reconnecting_mutation_http.py b/docs/code_examples/reconnecting_mutation_http.py index 5deb5063..1eaf0111 100644 --- a/docs/code_examples/reconnecting_mutation_http.py +++ b/docs/code_examples/reconnecting_mutation_http.py @@ -1,7 +1,7 @@ import asyncio import logging -import backoff +from tenacity import retry, retry_if_exception_type, wait_exponential from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport @@ -17,11 +17,9 @@ async def main(): client = Client(transport=transport) - retry_connect = backoff.on_exception( - backoff.expo, - Exception, - max_value=10, - jitter=None, + retry_connect = retry( + retry=retry_if_exception_type(Exception), + wait=wait_exponential(max=10), ) session = await client.connect_async(reconnecting=True, retry_connect=retry_connect) diff --git a/docs/code_examples/reconnecting_mutation_ws.py b/docs/code_examples/reconnecting_mutation_ws.py index d7e7cfe2..4d083d54 100644 --- a/docs/code_examples/reconnecting_mutation_ws.py +++ b/docs/code_examples/reconnecting_mutation_ws.py @@ -1,7 +1,7 @@ import asyncio import logging -import backoff +from tenacity import retry, retry_if_exception_type, wait_exponential from gql import Client, gql from gql.transport.websockets import WebsocketsTransport @@ -17,11 +17,9 @@ async def main(): client = Client(transport=transport) - retry_connect = backoff.on_exception( - backoff.expo, - Exception, - max_value=10, - jitter=None, + retry_connect = retry( + retry=retry_if_exception_type(Exception), + wait=wait_exponential(max=10), ) session = await client.connect_async(reconnecting=True, retry_connect=retry_connect) diff --git a/gql/client.py b/gql/client.py index e17a0b7c..93c1078c 100644 --- a/gql/client.py +++ b/gql/client.py @@ -21,7 +21,6 @@ overload, ) -import backoff from anyio import fail_after from graphql import ( ExecutionResult, @@ -31,6 +30,13 @@ parse, validate, ) +from tenacity import ( + retry, + retry_if_exception_type, + retry_unless_exception_type, + stop_after_attempt, + wait_exponential, +) from .graphql_request import GraphQLRequest, support_deprecated_request from .transport.async_transport import AsyncTransport @@ -1902,11 +1908,12 @@ def __init__( """ :param client: the :class:`client ` used. :param retry_connect: Either a Boolean to activate/deactivate the retries - for the connection to the transport OR a backoff decorator to - provide specific retries parameters for the connections. + for the connection to the transport OR a retry decorator + (e.g., from tenacity) to provide specific retries parameters + for the connections. :param retry_execute: Either a Boolean to activate/deactivate the retries - for the execute method OR a backoff decorator to - provide specific retries parameters for this method. + for the execute method OR a retry decorator (e.g., from tenacity) + to provide specific retries parameters for this method. """ self.client = client self._connect_task = None @@ -1917,10 +1924,9 @@ def __init__( if retry_connect is True: # By default, retry again and again, with maximum 60 seconds # between retries - self.retry_connect = backoff.on_exception( - backoff.expo, - Exception, - max_value=60, + self.retry_connect = retry( + retry=retry_if_exception_type(Exception), + wait=wait_exponential(max=60), ) elif retry_connect is False: self.retry_connect = lambda e: e @@ -1930,11 +1936,11 @@ def __init__( if retry_execute is True: # By default, retry 5 times, except if we receive a TransportQueryError - self.retry_execute = backoff.on_exception( - backoff.expo, - Exception, - max_tries=5, - giveup=lambda e: isinstance(e, TransportQueryError), + self.retry_execute = retry( + retry=retry_if_exception_type(Exception) + & retry_unless_exception_type(TransportQueryError), + stop=stop_after_attempt(5), + wait=wait_exponential(), ) elif retry_execute is False: self.retry_execute = lambda e: e @@ -1943,7 +1949,7 @@ def __init__( self.retry_execute = retry_execute # Creating the _execute_with_retries and _connect_with_retries methods - # using the provided backoff decorators + # using the provided retry decorators self._execute_with_retries = self.retry_execute(self._execute_once) self._connect_with_retries = self.retry_connect(self.transport.connect) diff --git a/setup.py b/setup.py index 39a6e453..e0764d5d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ install_requires = [ "graphql-core>=3.3.0a3,<3.4", "yarl>=1.6,<2.0", - "backoff>=1.11.1,<3.0", + "tenacity>=9.1.2,<10.0", "anyio>=3.0,<5", "typing_extensions>=4.0.0; python_version<'3.11'", ]