Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions docs/advanced/async_advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand All @@ -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
77 changes: 54 additions & 23 deletions docs/advanced/async_permanent_session.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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
10 changes: 4 additions & 6 deletions docs/code_examples/reconnecting_mutation_http.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)

Expand Down
10 changes: 4 additions & 6 deletions docs/code_examples/reconnecting_mutation_ws.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)

Expand Down
36 changes: 21 additions & 15 deletions gql/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
overload,
)

import backoff
from anyio import fail_after
from graphql import (
ExecutionResult,
Expand All @@ -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
Expand Down Expand Up @@ -1902,11 +1908,12 @@ def __init__(
"""
:param client: the :class:`client <gql.client.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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
]
Expand Down
Loading