Skip to content

RuntimeError: Event loop is closed, when running through Flask[async] #1168

Open
@nioncode

Description

Bug Description

After seeing that gevent is not supported (#969), we started to port our app from flask to quart. At the moment, we successfully ported some endpoints to quart, but still have others that are kept in flask. To have a similar code structure, we now run flask with async views (using flask[async] as dependency) so that we can run the same async database driver etc. in flask and quart and don't have to maintain sync and async engines.

After the app was idle for some time, we could not establish new connections to the database, since the event loop is closed (see stacktrace below). After restarting the running container, connections can successfully be established again.

From what I understood, flask[async] creates a new event loop for each request, i.e. the event loop of flask gets stopped after every request. That's why we use the NullPool of sqlalchemy so that connections are not shared and new connections are established for each request (which is ok performance-wise for those endpoints that are still running in flask), since you cannot share connections across different event loops. This seems to work fine, since a new connection is created instead of re-using an existing one, so it is probably not the source of the issue.
Further, cloud-sql-python-connector seems to spin up its own thread + event loop, which is used for internal processing, so this is probably not the issue either.

What does NOT spin up its own event loop, is the aiohttp.ClientSession that gets lazily initialized during connector.connect_async and seems to use the loop that is currently running whenever the first db connection gets established. What I think is happening here is that this event loop gets closed at some point and then the CientSession can not perform any requests anymore. What I don't understand though is why the event loop gets closed (since it seems to not be the event loop started by flask[async] for request processing, since that loop should be stopped after every request and so the next request should immediately fail).

Should the aiohttp.ClientSession use the same event loop that the connector itself uses (which could just pass its internal loop to the session when initiating it) or is there a reason why it connects to the currently running loop?

Example code (or command)

        connector = Connector(refresh_strategy=RefreshStrategy.LAZY)
        atexit.register(lambda: connector.close())

        async def get_connection() -> Connection:
            return await connector.connect_async(
                _from_env_str("DB_CONNECTION_NAME"),
                "asyncpg",
                user=_from_env_str("DB_USER"),
                db=_from_env_str("DB_NAME"),
                ip_type=IPTypes.PRIVATE,
                enable_iam_auth=True,
            )

Stacktrace

Traceback (most recent call last):
  File "/app/my_app/auth.py", line 124, in _parse_user_object
    user = await user_lookup(
           ^^^^^^^^^^^^^^^^^^
  File "/app/my_app/auth.py", line 101, in lookup_tss
    db_object = await db_object.for_serial(username)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/my_app/models_db.py", line 732, in for_serial
    await db.session.scalars(
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/scoping.py", line 1113, in scalars
    return await self._proxied.scalars(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py", line 574, in scalars
    result = await self.execute(
             ^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py", line 461, in execute
    result = await greenlet_spawn(
             ^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 201, in greenlet_spawn
    result = context.throw(*sys.exc_info())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py", line 2351, in execute
    return self._execute_internal(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py", line 2226, in _execute_internal
    conn = self._connection_for_bind(bind)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py", line 2095, in _connection_for_bind
    return trans._connection_for_bind(engine, execution_options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 2, in _connection_for_bind
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/state_changes.py", line 139, in _go
    ret_value = fn(self, *arg, **kw)
                ^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py", line 1189, in _connection_for_bind
    conn = bind.connect()
           ^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/opentelemetry/instrumentation/sqlalchemy/engine.py", line 98, in _wrap_connect_internal
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 3276, in connect
    return self._connection_cls(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 146, in __init__
    self._dbapi_connection = engine.raw_connection()
                             ^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 3300, in raw_connection
    return self.pool.connect()
           ^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 449, in connect
    return _ConnectionFairy._checkout(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 1263, in _checkout
    fairy = _ConnectionRecord.checkout(pool)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 712, in checkout
    rec = pool._do_get()
          ^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/impl.py", line 308, in _do_get
    return self._create_connection()
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 390, in _create_connection
    return _ConnectionRecord(self)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 674, in __init__
    self.__connect()
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 900, in __connect
    with util.safe_reraise():
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/util/langhelpers.py", line 146, in __exit__
    raise exc_value.with_traceback(exc_tb)
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 896, in __connect
    self.dbapi_connection = connection = pool._invoke_creator(self)
                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 362, in <lambda>
    return lambda rec: creator_fn()
                       ^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/engine.py", line 115, in creator
    return sync_engine.dialect.dbapi.connect(  # type: ignore
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 932, in connect
    await_only(creator_fn(*arg, **kw)),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
    value = await result
            ^^^^^^^^^^^^
  File "/app/my_app/config.py", line 55, in get_connection
    return await connector.connect_async(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/google/cloud/sql/connector/connector.py", line 341, in connect_async
    conn_info = await cache.connect_info()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/google/cloud/sql/connector/lazy.py", line 103, in connect_info
    conn_info = await self._client.get_connection_info(
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/google/cloud/sql/connector/client.py", line 271, in get_connection_info
    metadata = await metadata_task
               ^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/google/cloud/sql/connector/client.py", line 128, in _get_metadata
    resp = await self._client.get(url, headers=headers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/aiohttp/client.py", line 535, in _request
    handle = tm.start()
             ^^^^^^^^^^
  File "/app/venv/lib/python3.12/site-packages/aiohttp/helpers.py", line 627, in start
    return self._loop.call_at(when, self.__call__)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/base_events.py", line 774, in call_at
    self._check_closed()
  File "/usr/local/lib/python3.12/asyncio/base_events.py", line 541, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

Steps to reproduce?

  1. Deploy an app that uses flask[async] and an async connector to connect to the db.
  2. Run a successful request
  3. Wait a few hours (not clear what the exact timeout is or if this is reproducible, we'll need to debug this further)
  4. Run another request that requires the database
  5. Observe a stacktrace where the connection cannot be established

Environment

  1. OS type and version: python:3.12-slim Docker container
  2. Python version: 3.12
  3. Cloud SQL Python Connector version: 1.11.0

Additional Details

No response

Metadata

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions