PyTest Fixtures-在Docker中运行时,SQAlchemy连接未关闭



我正在使用pytest为我的基于FastAPI的应用程序创建测试。它使用SQLAlchemy+pymysql连接到MySQL数据库,使用以下代码:

def override_get_db():
engine = create_engine(SQLALCHEMY_DATABASE_URL, poolclass=NullPool)
SessionLocal = sessionmaker(autocommit=False, autoflush=True, bind=engine)
print("Session created")
db = SessionLocal()
try:
yield db
finally:
print("Session closed")
db.close()
engine.dispose()

然后将其作为依赖项注入FastAPI应用程序定义中。为了让版本摆脱困境(在macOS上的本地环境以及Docker容器中使用的版本相同(:

  • Python:3.10.3
  • SQLAlchemy:1.4.41
  • pymysql:1.0.2
  • 外部:7.1.3

在本地,pytest运行良好。所有测试都成功,并在pytest退出之前打印出最终结果。不过,在Docker容器中(无论是在Docker桌面上还是在带有Docker executor的Gitlab runner上(,实际测试运行得很好,但pytest不会退出,并永远被卡住。

经过进一步的调查,我已经确定了它被卡住的地方——有一个pytest固定装置在做非常基本的数据库操作:

@pytest.fixture(scope="session")
def create_database():
"""
Create the database using the root credentials towards the test database instance
"""
# Connect to the database
connection = pymysql.connect(
host=settings.test_db_host,
user="root",
password=settings.test_db_root_password,
port=settings.test_db_port,
cursorclass=pymysql.cursors.DictCursor,
autocommit=True,
)
# Create the new database
with closing(connection.cursor()) as cursor:
sql = "CREATE DATABASE IF NOT EXISTS %s;" % settings.test_db_database
cursor.execute(sql)
yield
with closing(connection.cursor()) as cursor:
# And drop it after the tests have finished
sql = "DROP DATABASE %s;" % settings.test_db_database
cursor.execute(sql)
connection.close()

应用程序在尝试删除数据库时被卡住。对数据库的进一步调查表明,并非所有连接都已关闭,DROP DATABASE正在等待锁定释放(见第3行和以下行中的打开连接(:

mysql> SHOW FULL PROCESSLIST;
+------+-----------------+------------------+--------------+---------+-------+---------------------------------+----------------------------+
| Id   | User            | Host             | db           | Command | Time  | State                           | Info                       |
+------+-----------------+------------------+--------------+---------+-------+---------------------------------+----------------------------+
|    5 | event_scheduler | localhost        | NULL         | Daemon  | 91256 | Waiting on empty queue          | NULL                       |
| 2598 | root            | localhost        | NULL         | Query   |     0 | init                            | SHOW FULL PROCESSLIST      |
| 5874 | root            | 172.20.0.4:50410 | NULL         | Query   |     6 | Waiting for table metadata lock | DROP DATABASE backend_test |
| 5918 | root            | 172.20.0.4:50498 | backend_test | Sleep   |     8 |                                 | NULL                       |
| 5944 | root            | 172.20.0.4:50550 | backend_test | Sleep   |     7 |                                 | NULL                       |
| 5963 | root            | 172.20.0.4:50588 | backend_test | Sleep   |     7 |                                 | NULL                       |
| 5999 | root            | 172.20.0.4:50660 | backend_test | Sleep   |     7 |                                 | NULL                       |
| 6012 | root            | 172.20.0.4:50686 | backend_test | Sleep   |     7 |                                 | NULL                       |
+------+-----------------+------------------+--------------+---------+-------+---------------------------------+----------------------------+
8 rows in set (0.00 sec)

我在上面显示的数据库会话代码中添加了print()语句,并注意到以下行为:

  • 在本地环境中本地运行代码时,Session createdSession closed分别被调用93次
  • 在Docker容器中运行时,Session created也被调用了93次,但关闭连接的代码执行较少。确切的数字取决于运行,但通常在84或85之间执行,这意味着连接保持打开状态

mysql版本为8.0.28,本地和Docker测试都使用完全相同的mysql服务器实例。

与本地相比,为什么在Docker中运行代码时不调用"finally"块?

编辑:如果重要的话,用于构建应用程序容器的Dockerfile:

FROM python:3.10.3-slim as python-base
ENV PYTHONUNBUFFERED=1 
PYTHONDONTWRITEBYTECODE=1 
PIP_NO_CACHE_DIR=off 
PIP_DISABLE_PIP_VERSION_CHECK=on 
PIP_DEFAULT_TIMEOUT=100 
POETRY_VERSION=1.2.1 
POETRY_HOME="/opt/poetry" 
POETRY_VIRTUALENVS_IN_PROJECT=true 
POETRY_NO_INTERACTION=1 
PYSETUP_PATH="/opt/pysetup" 
VENV_PATH="/opt/pysetup/.venv"
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
FROM python-base as builder-base
RUN apt-get update && apt-get install --no-install-recommends -y curl build-essential
RUN curl -sSL https://install.python-poetry.org | python3 -
WORKDIR $PYSETUP_PATH
COPY poetry.lock pyproject.toml ./
RUN poetry install --no-root
FROM python-base as production
ENV FASTAPI_ENV=production
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
COPY . /app/
WORKDIR /app
RUN apt-get update && apt-get install --no-install-recommends  -y locales
RUN sed -i -e 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen 
&& locale-gen
RUN cp /app/mksp_backend/config.example.py /app/mksp_backend/config.py
CMD ["uvicorn", "mksp_backend.main:app", "--host", "0.0.0.0", "--port", "80"]

编辑:

经过进一步的故障排除,我发现将FastAPI从0.85.0降级到0.84.0(因此也将starlette从0.20.4降级到0.19.1(可以解决这个问题。看起来这是在最新的FastAPI版本中引入的。

此外,删除所有http中间件(@app.middleware("http")(可以解决0.85.0中的问题。

原来FastAPI在本地系统的0.84.0上,在Docker环境中的0.85.0上。这个版本的变化还改变了一个依赖项,即从0.20.40.19.1的starlette。

Starlette0.20.4包含以下更改,这似乎是导致该问题的原因:https://github.com/encode/starlette/pull/1609/files

我使用如下所示的自定义http中间件来捕获异常,并确保日志环境的日志格式正确。

# Add middleware to catch all exceptions
@app.middleware("http")
async def exception_handler(request: Request, call_next):
try:
return await call_next(request)
except Exception as e:
logger.error("An unexpected error occured during request: {0}".format(str(e)))
logger.info(
"Traceback is: {0}".format(str(traceback.format_tb(e.__traceback__, 10)))
)
return JSONResponse(
content={
"message": "Custom Message"
},
status_code=500,
)

有了这个中间件,生成DB连接的生成器似乎不会为了关闭会话而跳回。由于某些原因,返回的JSONResponse对象不再工作,并且是最新版本。

我对starlette流程和代码的理解不足以理解根本原因,但我会在问题跟踪器中报告这个问题,并在问题得到解决时尝试记住更新。

最新更新