我正在使用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 created
和Session 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.4
到0.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流程和代码的理解不足以理解根本原因,但我会在问题跟踪器中报告这个问题,并在问题得到解决时尝试记住更新。