# Async permanent session

Sometimes you want to have a single permanent reconnecting async session to a GraphQL backend, and that can be difficult to manage manually with the `async with client as session` syntax.

It is now possible to have a single reconnecting session using the `connect_async` method of Client with a `reconnecting=True` argument.

    # Create a session from the client which will reconnect automatically.
    # This session can be kept in a class for example to provide a way
    # to execute GraphQL queries from many different places
    session = await client.connect_async(reconnecting=True)

    # You can run execute or subscribe method on this session
    result = await session.execute(query)

    # When you want the connection to close (for cleanup),
    # you call close_async
    await client.close_async()

When you use `reconnecting=True`, gql will watch the exceptions generated during the execute and subscribe calls and, if it detects a TransportClosed exception (indicating that the link to the underlying transport is broken), it will try to reconnect to the backend again.

## Retries

### Connection retries

With `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 retry decorator (from tenacity) to the `retry_connect` argument.

    from tenacity import retry, retry_if_exception_type, wait_exponential

    # Here wait maximum 5 minutes between connection retries
    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,
        retry_connect=retry_connect,
    )

### Execution retries

With `reconnecting=True`, by default we will also retry up to 5 times when an exception happens during an execute call (to manage a possible loss in the connection to the transport).

There is no retry in case of a `TransportQueryError` exception as it indicates that the connection to the backend is working correctly.

You can change the default execute retry profile by providing your own retry decorator (from tenacity) to the `retry_execute` argument.

    from tenacity import (
        retry,
        retry_if_exception_type,
        stop_after_attempt,
        wait_exponential,
    )

    # Here Only 3 tries for execute calls
    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 `retry_execute=False`

Note

If you want to retry even with `TransportQueryError` exceptions, then you need to make your own retry decorator (from tenacity) on your own method:

    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)

### Subscription retries

There is no `retry_subscribe` as it is not feasible with async generators. If you want retries for your subscriptions, then you can do it yourself with retry decorators (from tenacity) on your methods.

    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)

## FastAPI example

    # First install fastapi and uvicorn:
    #
    # pip install fastapi uvicorn
    #
    # then run:
    #
    # uvicorn fastapi_async:app --reload

    import logging

    from fastapi import FastAPI, HTTPException
    from fastapi.responses import HTMLResponse

    from gql import Client, gql
    from gql.client import ReconnectingAsyncClientSession
    from gql.transport.aiohttp import AIOHTTPTransport

    logging.basicConfig(level=logging.DEBUG)
    log = logging.getLogger(__name__)

    transport = AIOHTTPTransport(url="https://countries.trevorblades.com/graphql")

    client = Client(transport=transport)

    query = gql(
        """
    query getContinentInfo($code: ID!) {
      continent(code:$code) {
        name
        code
        countries  {
          name
          capital
        }
      }
    }
    """
    )

    app = FastAPI()

    @app.on_event("startup")
    async def startup_event():
        print("Connecting to GraphQL backend")

        await client.connect_async(reconnecting=True)
        print("End of startup")

    @app.on_event("shutdown")
    async def shutdown_event():
        print("Shutting down GraphQL permanent connection...")
        await client.close_async()
        print("Shutting down GraphQL permanent connection... done")

    continent_codes = [
        "AF",
        "AN",
        "AS",
        "EU",
        "NA",
        "OC",
        "SA",
    ]

    @app.get("/", response_class=HTMLResponse)
    def get_root():

        continent_links = ", ".join(
            [f'<a href="continent/{code}">{code}</a>' for code in continent_codes]
        )

        return f"""
        <html>
            <head>
                <title>Continents</title>
            </head>
            <body>
                Continents: {continent_links}
            </body>
        </html>
    """

    @app.get("/continent/{continent_code}")
    async def get_continent(continent_code):

        if continent_code not in continent_codes:
            raise HTTPException(status_code=404, detail="Continent not found")

        try:
            assert isinstance(client.session, ReconnectingAsyncClientSession)
            query.variable_values = {"code": continent_code}
            result = await client.session.execute(query)
        except Exception as e:
            log.debug(f"get_continent Error: {e}")
            raise HTTPException(status_code=503, detail="GraphQL backend unavailable")

        return result

## Console example

    import asyncio
    import logging
    from typing import Optional

    from aioconsole import ainput

    from gql import Client, gql
    from gql.client import AsyncClientSession
    from gql.transport.aiohttp import AIOHTTPTransport

    logging.basicConfig(level=logging.INFO)

    GET_CONTINENT_NAME = """
        query getContinentName ($code: ID!) {
          continent (code: $code) {
            name
          }
        }
    """

    class GraphQLContinentClient:
        def __init__(self):
            self._client = Client(
                transport=AIOHTTPTransport(url="https://countries.trevorblades.com/")
            )
            self._session: Optional[AsyncClientSession] = None

            self.get_continent_name_query = gql(GET_CONTINENT_NAME)

        async def connect(self):
            self._session = await self._client.connect_async(reconnecting=True)

        async def close(self):
            await self._client.close_async()

        async def get_continent_name(self, code):
            self.get_continent_name_query.variable_values = {"code": code}

            assert self._session is not None

            answer = await self._session.execute(self.get_continent_name_query)

            return answer.get("continent").get("name")  # type: ignore

    async def main():
        continent_client = GraphQLContinentClient()

        continent_codes = ["AF", "AN", "AS", "EU", "NA", "OC", "SA"]

        await continent_client.connect()

        while True:

            answer = await ainput("\nPlease enter a continent code or 'exit':")
            answer = answer.strip()

            if answer == "exit":
                break
            elif answer in continent_codes:

                try:
                    continent_name = await continent_client.get_continent_name(answer)
                    print(f"The continent name is {continent_name}\n")
                except Exception as exc:
                    print(f"Received exception {exc} while trying to get continent name")

            else:
                print(f"Please enter a valid continent code from {continent_codes}")

        await continent_client.close()

    asyncio.run(main())