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?
- Deploy an app that uses flask[async] and an async connector to connect to the db.
- Run a successful request
- Wait a few hours (not clear what the exact timeout is or if this is reproducible, we'll need to debug this further)
- Run another request that requires the database
- Observe a stacktrace where the connection cannot be established
Environment
- OS type and version:
python:3.12-slim
Docker container - Python version: 3.12
- Cloud SQL Python Connector version: 1.11.0
Additional Details
No response