fastapi lifespan closing session raises AttributeError: 'SQLAlchemyUserDatabase' object has no attribute 'close'

1.3k Views Asked by At

I am using fastapi (0.95.0), fastapi-users (10.4.2), fastapi-users-db-sqlalchemy (5.0.0) and SQLAlchemy (2.0.10) in my application.

This is a simplified snippet of my code:

engine = create_async_engine(SQLALCHEMY_DATABASE_URL)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        yield session


async def get_user_db(session: AsyncSession = Depends(get_async_session)):
    yield SQLAlchemyUserDatabase(session, UserModel, OAuthAccount)



@asynccontextmanager
async def lifespan(fapp: FastAPI):
    # establish a connection to the database    
    fapp.state.async_session = await get_user_db().__anext__()
    yield
    # close the connection to the database
    await fapp.state.async_session.close()
    await fapp.state.async_session.engine.dispose()



app = FastAPI(lifespan=lifespan)

# Add Routes
# ...

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

When I use Ctrl-C to stop the running uvicorn server, I get the following error trace:

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
<class 'fastapi_users_db_sqlalchemy.SQLAlchemyUserDatabase'>
ERROR:    Traceback (most recent call last):
  File "/path/to/proj/env/lib/python3.10/site-packages/starlette/routing.py", line 677, in lifespan
    async with self.lifespan_context(app) as maybe_state:
  File "/usr/lib/python3.10/contextlib.py", line 206, in __aexit__
    await anext(self.gen)
  File "/path/to/proj/src/main.py", line 40, in lifespan
    await fastapi_app.state.async_session.close()
AttributeError: 'SQLAlchemyUserDatabase' object has no attribute 'close'

ERROR:    Application shutdown failed. Exiting.
INFO:     Finished server process [37752]

Which is strange, because I am calling close on a variable of type AsyncSession not SQLAlchemyUserDatabase, so based on this error message, I change the line statement to reference the session attribute of the SQLAlchemyUserDatabase class, and call close() on the session attribute, as shown below:

await fapp.state.async_session.session.close()

Now, I get this even more cryptic error trace:

INFO:     Started server process [33125]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
ERROR:    Traceback (most recent call last):
  File "/path/to/proj/env/lib/python3.10/site-packages/starlette/routing.py", line 677, in lifespan
    async with self.lifespan_context(app) as maybe_state:
  File "/path/to/proj/env/lib/python3.10/site-packages/starlette/routing.py", line 569, in __aexit__
    await self._router.shutdown()
  File "/path/to/proj/env/lib/python3.10/site-packages/starlette/routing.py", line 664, in shutdown
    await handler()
  File "/path/to/proj/src/main.py", line 88, in shutdown
    await app.state.async_session.session.close()
AttributeError: 'Depends' object has no attribute 'close'

ERROR:    Application shutdown failed. Exiting.

fapp.state.async_session.session should not be of type Depends.

Why is this type error occurring, and how do I resolve it, so that I can gracefully release resources when the server is shutdown?

2

There are 2 best solutions below

0
On

Would it be possible if you to create a separate function to establish the DB connection and return the SQLAlchemyUserDatabase object? and try using the asynccontextmanager to handle the lifecycle of the database session properly.

from sqlalchemy.ext.asyncio import async_contextmanager

@async_contextmanager
async def get_async_session() -> AsyncSession:
    async with async_sessionmaker() as session:
        yield session
1
On

Depends object is meant to be used as a dependency injection mechanism in FastAPI.

When you use Depends in a function signature, FastAPI understands that it needs to resolve that dependency before executing the function. FastAPI does this by calling the function specified as the argument to Depends and passing the resolved value to your route function.

By using yield in get_user_db, you are treating it as a generator function and directly yielding the Depends object itself instead of allowing FastAPI to resolve it.

So, update the function to return the session instead, see updated code here:

engine = create_async_engine(SQLALCHEMY_DATABASE_URL)
async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        yield session

async def get_user_db(session: AsyncSession = Depends(get_async_session)):
    return SQLAlchemyUserDatabase(session, UserModel, OAuthAccount)

# Lifespan context manager
@async_contextmanager
async def lifespan(fapp: FastAPI):
    fapp.state.async_session = await get_user_db()
    yield
    await fapp.state.async_session.session.close()
    await fapp.state.async_session.session.engine.dispose()


app = FastAPI(lifespan=lifespan)

# Add Routes
# ...

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

As the get_user_db function has been modified to return the resolved value, there is no need to use __anext__() or yield from it.