This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Problems with await in async dependencies (when using dependencies and middleware simultaneously) #4719
Comments
Yes, having the same problem with from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from app.config import settings
engine = create_async_engine(
settings.DB_DSN,
echo=settings.DB_ECHO,
pool_size=settings.DB_POOL_SIZE,
max_overflow=settings.DB_MAX_OVERFLOW,
future=True,
)
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession, future=True, autoflush=False)
async def get_db():
db = async_session()
try:
yield db
finally:
await db.close() And I have middlewares also |
Same here with async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
async_sessionmaker = request.app.state.sessionmaker
session: AsyncSession
async with async_sessionmaker() as session:
try:
yield session
except SQLAlchemyError:
await shield(session.rollback())
logger = get_logger()
logger.exception("sqlalchemy_exception") |
Same problem here with |
It seems that the problem has to do with
|
It looks like SQLAlchemy is using contextvars internally: https://github.com/sqlalchemy/sqlalchemy/blob/57286c3137cbb572a03fbec9bf5c428b76804d4a/lib/sqlalchemy/util/_concurrency_py3k.py#L10 And I think all of you are using Try removing your middleware. If that fixes the issue, then you just need to rewrite your middleware to not use |
I'm using |
No idea why Nginx would have anything to do with this, I'm guessing it's just a symptom and not the cause. As per above Here's my minimal reproducible example: from typing import Awaitable, Callable
from fastapi import FastAPI, Depends, Request ,Response
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
app = FastAPI()
@app.on_event("startup")
def startup() -> None:
engine = create_async_engine(
'postgresql+asyncpg://postgres:postgres@localhost:5433/postgres',
)
app.state.session_maker = sessionmaker(
bind=engine,
class_=AsyncSession
)
async def get_db():
db: AsyncSession = app.state.session_maker()
try:
yield db
finally:
print('finally get_db. Before close')
await db.close()
print('finally get_db. After close')
@app.get('/')
async def endpoint(
db: AsyncSession = Depends(get_db)
):
await db.execute(text('Select 1'))
@app.middleware('http')
async def mw(
request: Request,
call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
return await call_next(request)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app) Again, I would recommend you try rewriting your middleware as generic ASGI middleware and go from there. See this thread for some details: #2215 (comment) |
Thanks for your help, @adriangb, it's very much appreciated!
Should this reproduce the problem? I don't see the SAWarning and db connections don't pile up when running your code. |
Sorry, my bad: I was using swagger, but the problem starts when using curl, as reported by @Rashid567. |
Interestingly, if I swap Uvicorn for Hypercorn, your code seems to run fine, @adriangb. Does that make sense? |
Maybe they handle lifespans differently? I know that Uvicorn runs lifespans in a sibling task to requests, which IMO can also lead to problems with things that use contextvars and the like. |
Adrian, I followed your advise, that is, ditched |
I'm using https://github.com/trallnag/prometheus-fastapi-instrumentator which is using BaseHTTPMiddleware I will try to replace it with https://github.com/stephenhillier/starlette_exporter |
Hmm, I didn't notice it earlier, because it's logged at INFO, but with my new middleware I get this at startup:
The application is functioning, but |
Ah, you'll have to manage the lifetime scope: https://pgjones.dev/blog/how-to-write-asgi-middleware-2021/. |
Can this issue be closed? |
Yes |
Not true, I was definitely using |
This is definitely still an issue, both with |
BaseHTTPMiddleware is fundamentally broken, I would not count on it being fixed. |
We face a similar problem after upgrading FastAPI from 0.73.0 to 0.78.0. From the comments above, it seems that the problem started with 0.74.0. From the diff : 0.73.0...0.74.0, I assume that it is probably caused by: #4575 |
I checked issue again with latest packages. And also dropped db from test. Python 3.10.7 requirements.txt
Code import asyncio
import datetime as dt
import fastapi
async def fake_get_db():
db = 'fake_conn'
try:
yield db
finally:
print('Before finally await')
await asyncio.sleep(1)
print('After finally await')
def get_app():
app = fastapi.FastAPI()
@app.middleware('http')
async def add_process_time_header(request: fastapi.Request, call_next):
start_time = dt.datetime.now()
response = await call_next(request)
process_time = (dt.datetime.now() - start_time).total_seconds()
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get('/test')
async def test(fake_db: str = fastapi.Depends(fake_get_db)):
return 'OK'
return app
if __name__ == '__main__':
import uvicorn
_app = get_app()
uvicorn.run(_app, host='0.0.0.0', port=7776) The problem still remains and it's not in db client. For example this code works fine: import requests
from time import sleep
with requests.session() as s:
print(s.get('http://localhost:7776/test'))
sleep(1) But if you comment sleep(1), you will not reach print('After finally await'). @adriangb Nginx use http 1.0 when sending requests to the app and close connection after getting response. This is why @byrman gets errors. Also i tried:
I think it's dirrectly connected to AsyncExitStackMiddleware in fastapi. But how i don't have a clue @byrman i don't think "not using http middleware with dependencies" and "not using uvicorn" is solution. So i am not going to close issue |
That is really interesting. The It seems that, when using Not behaviour one would expect, for sure. And again, when not using |
Facing the same issue in 0.85.0, Depends(get_session) not close normally. In pgadmin shows that connections with IDLE state. I did a test on async_session or sync_session, it turns out both of them are working abnormal |
after deep investigation, the issue was resolved by using Null pool |
Before:from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm.session import sessionmaker
from app.core.config import settings
if settings.ENVIRONMENT == "PYTEST":
async_sqlalchemy_database_uri = settings.TEST_SQLALCHEMY_DATABASE_URI
else:
async_sqlalchemy_database_uri = settings.ASYNC_DEFAULT_SQLALCHEMY_DATABASE_URI
async_engine = create_async_engine(async_sqlalchemy_database_uri, pool_pre_ping=True)
async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) get_session: async def get_session():
async with new_session() as session:
yield session After:from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import NullPool
from app.core.config import settings
if settings.ENVIRONMENT == "PYTEST":
async_sqlalchemy_database_uri = settings.TEST_SQLALCHEMY_DATABASE_URI
else:
async_sqlalchemy_database_uri = settings.ASYNC_DEFAULT_SQLALCHEMY_DATABASE_URI
async_engine = create_async_engine(async_sqlalchemy_database_uri, pool_pre_ping=True, poolclass=NullPool)
async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) |
Is there any drawbacks when using I also check docs and there is |
I am facing a similar issue as a regression in 0.85.0. MySQL connections not being closed correctly when injected using dependency injection. Pinning FastAPI to 0.84.0 fixes the issue. This issue persists when using Further details (at an earlier stage of troubleshooting, at this point I didn't make the connection to FastAPI yet) can be found here: https://stackoverflow.com/questions/73927410/pytest-fixtures-sqalchemy-connections-not-closing-when-running-in-docker After further investigation, removing the http middlewares does work around the issue as well, so does downgrading to 0.84.0 |
I have the same problem after an upgrade to 0.85, database session dependency starts leaking connections |
Do you use BaseHTTPMiddleware in your code? |
I have implement NullPool in our prod env for a few weeks, working fine till now |
@JarroVGIT yes, I use |
@mvbrn there seem to be some issues with the BaseHTTPMiddleware, and I have seen this issue come up as well. The yield part is not executed sometimes, haven’t been able to pinpoint why but I would expect this to happen with larger requests maybe? Either way, there is discussion ongoing on deprecating it in the Starlette repo. |
If I don't use Depends or any other middleware, do I risk explode database sessions? |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
First Check
Commit to Help
Example Code
Description
FastAPI version>=0.74 has a very strange problem with dependency (func get_db in example).
If you run this code as it is and send request from swagger every thing is ok (Connection with db will close and you see message 'finally get_db. After close' in console).
If you run this code as it is and use curl or aiohttp to send request, then "await db.close()" is never finish and you will not see message 'finally get_db. After close' in console. And if you continue to send requests, then you get warnings from Garbage collector and SQLAlchemy:
The garbage collector is trying to clean up connection <AdaptedConnection <asyncpg.connection.Connection object at 0x7fd3626a4740>>. This feature is unsupported on async dbapi, since no IO can be performed at this stage to reset the connection. Please close out all connections when they are no longer used, calling "close()" or using a context manager to manage their lifetime.
/usr/lib/python3.10/ipaddress.py:45: SAWarning: The garbage collector is trying to clean up connection <AdaptedConnection <asyncpg.connection.Connection object at 0x7fd3626a4740>>. This feature is unsupported on async dbapi, since no IO can be performed at this stage to reset the connection. Please close out all connections when they are no longer used, calling "close()" or using a context manager to manage their lifetime.
return IPv4Address(address)
If you comment middleware and send requests from any client (swagger, curl and aiohttp), then every thing is ok.
If you run this code as it is with FastAPI version <= 0.73, then every thing is ok.
In each cases clients has no problems in getting 200 responses from server.
Unfortunately i have no idea why this happening.
Requirements:
anyio==3.5.0
asgiref==3.5.0
asyncpg==0.25.0
click==8.0.4
coloredlogs==15.0.1
fastapi==0.75.0 | 0.73.0
greenlet==1.1.2
h11==0.13.0
httptools==0.3.0
humanfriendly==10.0
idna==3.3
mypy==0.931
mypy-extensions==0.4.3
pydantic==1.9.0
python-dateutil==2.8.2
python-dotenv==0.19.2
PyYAML==6.0
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
sqlalchemy2-stubs==0.0.2a20
starlette==0.17.1
tomli==2.0.1
typing_extensions==4.1.1
uvicorn==0.17.5
uvloop==0.16.0
watchgod==0.7
websockets==10.2
Operating System
Linux
Operating System Details
No response
FastAPI Version
Python Version
3.9 and 3.10
Additional Context
No response
The text was updated successfully, but these errors were encountered: