You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调用subprocess执行listen命令后Django测试数据库未自动删除

问题解决:Django-pgpubsub监听进程导致测试数据库无法自动删除

背景

我们用Django-pgpubsub替代Django signals或Celery,通过运行python manage.py listen监听PostgreSQL触发器,当Shipment模型的status字段变为特定值时触发邮件通知。集成测试中通过subprocess启动独立进程运行该监听命令,测试能正常通过,但测试数据库无法被pytest-django自动清理——即使日志显示Destroying test database for alias 'default' ('test_my_project_database'),数据库实际仍保留,后续测试会因数据库重复存在报错。目前用--reuse-db参数能规避问题,但不想依赖这个临时方案。

问题根源

测试中启动的listen进程会持续持有PostgreSQL数据库连接,pytest-django尝试销毁数据库时,该连接仍处于活跃状态。PostgreSQL不允许删除有活跃连接的数据库,哪怕调用了listen_process.terminate(),进程也可能没来得及正常关闭连接就被终止,导致数据库无法被删除。

解决方案

1. 确保监听进程彻底退出并释放连接

不要只用terminate(),改用“先终止+超时等待+强制杀死”的组合逻辑,确保进程完全退出:

# 替换原代码中的listen_process.terminate()
listen_process.terminate()
try:
    # 等待进程正常退出,设置超时时间
    listen_process.wait(timeout=5)
except subprocess.TimeoutExpired:
    # 超时则强制杀死进程
    listen_process.kill()
    listen_process.wait()

2. 动态获取测试数据库名称,避免硬编码

原代码中硬编码测试数据库名test_{config('POSTGRES_DB')},但pytest-django未启用--reuse-db时,可能生成带随机后缀的测试数据库。正确做法是从Django当前数据库配置中获取名称:

from django.db import connections

# 获取当前测试数据库的真实名称
db_name = connections['default'].settings_dict['NAME']
env.update(
    {
        "DATABASE_URL":
            f"postgresql://{config('POSTGRES_USER')}:{config('POSTGRES_PASSWORD')}@127.0.0.1:5432/{db_name}",
    }
)

3. 用pytest fixture统一管理监听进程

把启动/关闭监听进程的逻辑封装成fixture,确保每个测试后都能可靠清理:

import pytest
import subprocess
import os
import time
from django.conf import settings
from decouple import config
from django.db import connections

@pytest.fixture(scope="function")
def pgpubsub_listen_process():
    db_name = connections['default'].settings_dict['NAME']
    listen_command = [
        "python",
        str(settings.ROOT_DIR / "manage.py"),
        "listen",
    ]
    env = os.environ.copy()
    env.update(
        {
            "DATABASE_URL":
                f"postgresql://{config('POSTGRES_USER')}:{config('POSTGRES_PASSWORD')}@127.0.0.1:5432/{db_name}",
        }
    )
    listen_process = subprocess.Popen(listen_command, env=env)
    time.sleep(2)  # 等待进程启动完成
    yield listen_process
    
    # 测试结束后清理进程
    listen_process.terminate()
    try:
        listen_process.wait(timeout=5)
    except subprocess.TimeoutExpired:
        listen_process.kill()
        listen_process.wait()

测试函数可以简化为:

@pytest.mark.django_db(transaction=True)
def test_shipment_status_notifications_with_listen_command_subprocess(testuser, pgpubsub_listen_process):
    user = testuser
    # 测试数据准备
    notification_settings = NotificationSettings.objects.get(user=user)
    notification_settings.failed_attempt = True
    notification_settings.save()
    cust_email = "test@test.com"
    customer = Customer.objects.create(email=cust_email)
    order = Order.objects.create(customer=customer)
    shipment = Shipment.objects.create(user=user, order=order)

    # 触发状态变更
    shipment.status = Shipment.FAILED_ATTEMPT
    shipment.save()

    time.sleep(2)  # 等待触发器处理完成

    existing_notifications = Notification.objects.filter(
        type=Notification.Type.FAILED_DELIVERY_ATTEMPT,
        shipment=shipment,
        email=cust_email,
    )
    assert existing_notifications.count() == 1

4. 极端情况:强制关闭数据库所有连接

如果以上方法仍无效,可以在测试清理阶段强制关闭目标数据库的所有活跃连接:

import psycopg2
from psycopg2 import sql

def close_db_connections(db_name):
    # 连接到postgres系统数据库(而非测试数据库)
    conn = psycopg2.connect(
        dbname="postgres",
        user=config('POSTGRES_USER'),
        password=config('POSTGRES_PASSWORD'),
        host="127.0.0.1",
        port="5432"
    )
    conn.autocommit = True
    cur = conn.cursor()
    # 终止所有连接到目标测试库的进程
    cur.execute(sql.SQL("""
        SELECT pg_terminate_backend(pid)
        FROM pg_stat_activity
        WHERE datname = %s AND pid <> pg_backend_pid();
    """), [db_name])
    cur.close()
    conn.close()

然后在fixture的清理阶段调用该函数:

@pytest.fixture(scope="function")
def pgpubsub_listen_process():
    # 启动进程逻辑...
    yield listen_process
    # 先终止进程
    listen_process.terminate()
    try:
        listen_process.wait(timeout=5)
    except subprocess.TimeoutExpired:
        listen_process.kill()
        listen_process.wait()
    # 强制关闭所有数据库连接
    db_name = connections['default'].settings_dict['NAME']
    close_db_connections(db_name)

验证方式

运行测试后,查看PostgreSQL数据库列表,确认测试数据库已被自动删除,后续测试不会因数据库重复存在报错。

内容的提问来源于stack exchange,提问作者Nathan Smeltzer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 12:55:05