시험 DB는 마이그레이션해 둔 템플릿에서 복제한다
워커마다 migrate를 돌리지 않는다. 마이그레이션 해시로 이름 지은 템플릿 DB를 한 번 만들고 CREATE DATABASE … TEMPLATE으로 복제하면 7~9초가 1초 미만이 된다.
pytest-django를 xdist로 병렬로 돌리면 워커마다 제 DB를 만들고 마이그레이션을 처음부터 전부 돌린다. 한 번에 약 7.5초이고, 그 시간이 각 워커의 첫 시험 준비에 붙는다. CI 워커가 12개면 합쳐서 90초다.
마이그레이션은 한 번만 돌려 템플릿 DB를 만들고, 워커 DB는 Postgres의 CREATE DATABASE … TEMPLATE으로 복제한다. 복제는 파일 복사라 7~9초 걸리던 준비가 1초 아래로 내려간다.
import hashlib
import pathlib
import psycopg
def template_name() -> str:
files = sorted(pathlib.Path(".").glob("*/migrations/*.py"))
digest = hashlib.sha256(b"".join(p.read_bytes() for p in files)).hexdigest()[:12]
return f"test_tpl_{digest}"
def clone_test_db(admin_dsn: str, target: str) -> None:
tpl = template_name()
with psycopg.connect(admin_dsn, autocommit=True) as conn:
with conn.transaction():
conn.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (tpl,))
found = conn.execute(
"SELECT 1 FROM pg_database WHERE datname = %s", (tpl,)
).fetchone()
if not found:
build_template(tpl) # migrate한 뒤 템플릿에 걸린 연결을 모두 닫는다
conn.execute(f'CREATE DATABASE "{target}" TEMPLATE "{tpl}"')
요령 몇 가지:
- 이름에 마이그레이션 해시를 넣는다. 스키마가 바뀌면 이름이 바뀌어 저절로 다시 만든다.
- 누가 만들지는 DB 잠금으로 가른다. 파일 잠금은 호스트와 컨테이너가 서로 못 보지만,
pg_advisory_xact_lock은 같은 DB를 보는 모두가 본다. 세션 잠금이 아니라 트랜잭션 잠금을 쓰는 이유는, pgbouncer 트랜잭션 모드 뒤에서는 세션 잠금이 문장마다 다른 연결에 떨어지기 때문이다. - 템플릿에 연결이 남아 있으면 복제가 거절된다. 만든 뒤 연결을 닫는다.
- xdist 컨트롤러에서 미리 만든다(
pytest_sessionstart). 워커가 각자 잠금 앞에서 기다리면 그 기다림이 또 첫 시험에 붙는다. - 지난 실행이 남긴 템플릿을 그대로 믿지 않는다. 템플릿 COMMENT에 실행 표식을 적어 두고 다르면 다시 만든다. 체크아웃 두 개가 같은 Postgres를 쓰면 이름에 경로 해시도 넣어 서로 지우지 않게 한다.