인프런 커뮤니티 질문&답변

이성민님의 프로필 이미지
이성민

작성한 질문수

FastAPI 완벽 가이드

FastAPI의 Lifespan 적용하여 FastAPI 종료 시 비동기 SQLAlchemy Engine Clear

lifespan 적용 관련 문의

작성

·

44

0

안녕하세요 강사님
강의 열심히 잘 듣고 있습니다.

덕분에 FastAPI에 대한 재미를 더 많이 가지고 있어 감사할 따름입니다.

 

lifespan 적용하신 강의 관련해서 질문이 있습니다.

저는 현재 환경을 강의 내용과 다르게 진행하고있습니다.

 

mysql이 아닌 postgresql로 진행하고 있고,

postgresql + asyncpg를 이용하여 비동기 처리를 하고있습니다. lifespan 부분을 적용 시 아래 오류가 발생되고있습니다.

 

Exception terminating connection <AdaptedConnection <asyncpg.connection.Connection object at 0x00000257982897B0>>

Traceback (most recent call last):

File "D:\personnel\Python\fastapi\fastapi_pguide-main\fastapi_pguide-main\AsyncDB_Handling\.venv\lib\site-packages\sqlalchemy\pool\base.py", line 374, in closeconnection

self._dialect.do_terminate(connection)

File "D:\personnel\Python\fastapi\fastapi_pguide-main\fastapi_pguide-main\AsyncDB_Handling\.venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 1117, in do_terminate

dbapi_connection.terminate()

File "D:\personnel\Python\fastapi\fastapi_pguide-main\fastapi_pguide-main\AsyncDB_Handling\.venv\lib\site-packages\sqlalchemy\dialects\postgresql\asyncpg.py", line 910, in terminate

self._connection.terminate()

File "D:\personnel\Python\fastapi\fastapi_pguide-main\fastapi_pguide-main\AsyncDB_Handling\.venv\lib\site-packages\asyncpg\connection.py", line 1515, in terminate

self._abort()

File "D:\personnel\Python\fastapi\fastapi_pguide-main\fastapi_pguide-main\AsyncDB_Handling\.venv\lib\site-packages\asyncpg\connection.py", line 1567, in _abort

self._protocol.abort()

File "asyncpg\\protocol\\protocol.pyx", line 608, in asyncpg.protocol.protocol.BaseProtocol.abort

File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\asyncio\selector_events.py", line 686, in abort

self._force_close(None)

File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\asyncio\selector_events.py", line 737, in forceclose

self._loop.call_soon(self._call_connection_lost, exc)

File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 753, in call_soon

self._check_closed()

File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 515, in checkclosed

raise RuntimeError('Event loop is closed')

RuntimeError: Event loop is closed

The garbage collector is trying to clean up non-checked-in connection <AdaptedConnection <asyncpg.connection.Connection object at 0x00000257982897B0>>, which will be terminated. Please ensure that SQLAlchemy pooled connections are returned to the pool explicitly, either by calling ``close()`` or by using appropriate context managers to manage their lifecycle.

sys:1: SAWarning: The garbage collector is trying to clean up non-checked-in connection <AdaptedConnection <asyncpg.connection.Connection object at 0x00000257982897B0>>, which will be terminated. Please ensure that SQLAlchemy pooled connections are returned to the pool explicitly, either by calling ``close()`` or by using appropriate context managers to manage their lifecycle.

 

위와 같은 오류가 발생되고 있습니다.

방법을 찾아보려 gpt에게 문의도 해봤지만 강의 내용과 동일하거나 또는 starting, shutting (구 FastAPI)방식을 알려주고 있습니다.

 

나름 응용을 해보겠다고 진행한 건데..

번거로운 질문 드려 죄송합니다.ㅜㅜ

 

제가 구성한 환경은 이와 같이 진행하였습니다. 참고부탁드리겠습니다.

 

python =="^3.10"
fastapi == "0.115.4"
uvicorn == "0.32.0"
python-multipart == "0.0.17"
python-dotenv == "1.0.1"
jinja2 == "3.1.4"
sqlalchemy == "2.0.36"
psycopg == {extras = ["binary", "pool"], version = "^3.2.3"}
asyncpg == "^0.30.0"
aiofiles == "24.1.0"

감사합니다.

답변 2

1

권 철민님의 프로필 이미지
권 철민
지식공유자

안녕하십니까,

잘 듣고 계시다니, 저도 기쁘군요 ^^

이건 저도 셋팅 후에 테스트를 해봐야 해서 시간이 걸릴 것 같습니다. 주말 정도에 답변 드릴 수 있을 것 같습니다.

그리고 수정하신 database.pymain.py 를 여기에 올려 주시겠습니까?

감사합니다.

이성민님의 프로필 이미지
이성민
질문자

네 강사님,

코드 올려드립니다.

 

오늘 하루 즐거운 하루 보내세요!

 

감사합니다.

 

main.py

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from routes import blog
from db.database import engine

@asynccontextmanager
async def lifespan(app: FastAPI):
    # FastAPI 인스턴스 기동시 필요한 작업 수행.
    print("Starting up...")
    yield
    
    # FastAPI 인스턴스 종료시 필요한 작업 수행    
    print("Shutting down...")
    
    await engine.dispose()

app = FastAPI(lifespan=lifespan)

app.mount("/static", StaticFiles(directory="static"), name="static")
app.include_router(blog.router)

database.py

from sqlalchemy import create_engine, Connection
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.pool import QueuePool, NullPool
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
from contextlib import contextmanager
from fastapi import status
from fastapi.exceptions import HTTPException
from dotenv import load_dotenv
import os

# database connection URL
# DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db"
load_dotenv()

DATABASE_CONN = os.getenv("DATABASE_CONN")

print("database_conn: ", DATABASE_CONN)

engine: AsyncEngine = create_async_engine(DATABASE_CONN, echo=True,
                       #poolclass=QueuePool,
                       #poolclass=NullPool, # Connection Pool 사용하지 않음. 
                       pool_size=10, max_overflow=0,
                       pool_recycle=300)

async def direct_get_conn():
    conn = None
    try:
        conn = await engine.connect()
        return conn
    except SQLAlchemyError as e:
        print(e)
        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                            detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.")

async def context_get_conn():
    conn = None
    try:
        conn = await engine.connect()
        yield conn
    except SQLAlchemyError as e:
        print(e)
        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                            detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.")
    finally:
        if conn:
            conn.close()
이성민님의 프로필 이미지
이성민
질문자

강사님...

답글 올리고 나서 강의 들으면서 다시 확인해보니..

database.py의 finally구문에 제가 await를 누락해서 오류 표시가 된 것으로 확인하였습니다.

 

final과 비교해보니 바로 나오더군요... ㅎ;

답글 주셔서 감사합니다.

권 철민님의 프로필 이미지
권 철민
지식공유자

오, 해결이 되었다니, 다행입니다.

즐강하십시요.

0

안녕하세요, 인프런 AI 인턴입니다.

이성민님께서 맞닥뜨리신 문제는 FastAPI 애플리케이션에서 lifespan 사용 중 발생하는 비동기 처리 관련 오류로 보입니다. 주로 AsyncIO와 관련된 오류이며, 이는 보통 데이터베이스 연결을 적절하게 종료하지 못할 때 발생합니다. 아래에서 이 문제를 해결하기 위한 몇 가지 접근법을 제안드리겠습니다.

  1. 데이터베이스 연결 관리: 비동기로 데이터베이스와 연결할 때, 연결이 제대로 해제되지 않으면 말씀하신 오류가 발생할 수 있습니다. 따라서 connection pool을 사용하는 경우, 모든 연결이 적절히 닫히도록 확실히 해야 합니다. 이를 위해 아래와 같은 패턴을 사용해볼 수 있습니다.

    from sqlalchemy.ext.asyncio import create_async_engine
    from sqlalchemy.ext.asyncio import AsyncSession
    from sqlalchemy.orm import sessionmaker
    
    
    engine = create_async_engine('postgresql+asyncpg://...')
    
    
    async_session = sessionmaker(
        bind=engine,
        expire_on_commit=False,
        class_=AsyncSession
    )
    
    
    async def get_db():
        async with async_session() as session:
            try:
                yield session
            finally:
                await session.close()
    
  2. lifespan 이벤트 설정: FastAPI에서 lifecycle events를 설정하는 방법을 사용하여 데이터베이스 연결을 생성하고 종료할 수 있습니다.

    from fastapi import FastAPI
    
    
    app = FastAPI()
    
    
    @app.on_event('startup')
    async def startup():
        # 데이터베이스 연결 초기화 코드
    
    
    @app.on_event('shutdown')
    async def shutdown():
        # 데이터베이스 연결 종료 코드
    
  3. Connection 확인 및 닫기: 모든 데이터베이스 연결이 쿼리 후에 명확히 닫혀야 합니다. 이를 위해 Session.remove() 메서드를 사용하여 연결을 반환하거나, async with 구문을 사용하여 자동으로 닫히도록 할 수 있습니다.

  4. Event Loop 주의: 오류 메시지에서 ‘Event loop is closed’ 라는 문구가 있는 것으로 보아, 애플리케이션의 이벤트 루프가 정상적으로 처리되지 않을 수 있습니다. 프로그램 종료 시 이벤트 루프가 종료되지 않는지, 다른 곳에서 이벤트 루프를 불필요하게 종료하는지를 확인해보세요.

이 정보가 해결에 도움이 되길 바랍니다. 기존 강의와 연계된 내용이므로 강사님의 답변도 살펴보시면 더 많은 도움이 될 수 있습니다.

다음 링크를 통해 유사한 질문과 답변, 해결방법을 확인해보시는 것도 추천드립니다:
- “lifespan과 이벤트 루프 처리 관련 문제”

저는 질문자님의 이해를 돕고자 유사한 사례와 해결 방법을 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

이성민님의 프로필 이미지
이성민

작성한 질문수

질문하기