fix(capture): every process registers every table before its first query

The models now live in their units' packages, and a core model names a unit's by
string (User.subscriptions -> Subscription). The download child imports neither
catalog nor app.tables before db.get, so mapper configuration failed and every
download, clip, edit and upload job died with child_exit. Each entry point now
imports app.tables, and a test imports each one in a clean interpreter.
This commit is contained in:
2026-10-20 00:45:20 +02:00
parent 5ce292cd28
commit d73cdcee97
5 changed files with 67 additions and 0 deletions
+1
View File
@@ -16,6 +16,7 @@ import shutil
import subprocess
import sys
import app.tables # noqa: F401 — a fresh interpreter: every table, before the first query
from app import logs
from app import logsink
from app.config import settings
+1
View File
@@ -28,6 +28,7 @@ log = logs.get_logger(__name__)
from app import bodylimit, collation, logsink, state, sysconfig # noqa: E402 — logging is configured first
from app.config import settings # noqa: E402 — logging is configured first
from app.db import SessionLocal, engine # noqa: E402 — logging is configured first
import app.tables # noqa: E402 — every table, before the first query
from app.downloads import service as download_service # noqa: E402 — logging is configured first
from app.downloads import storage # noqa: E402 — logging is configured first
from app.plex import stream as plex_stream # noqa: E402 — logging is configured first
+1
View File
@@ -31,6 +31,7 @@ from functools import partial
from sqlalchemy import text
from sqlalchemy.orm import Session
import app.tables # noqa: F401 — every table, before the first query
from app import logs
from app import logsink
from app import sysconfig
+63
View File
@@ -0,0 +1,63 @@
"""Every process the app runs as sees every table before its first query.
The units declare their tables in their own `models` modules, and a core model names a unit's by
string (`User.subscriptions` → `Subscription`). A fresh interpreter that never imported the unit's
module fails to configure its mappers on its first query: the download child once did exactly that,
and every job failed, while this suite stayed green — it imports `app.tables` in-process, which hides
the gap. So each entry point is imported here in a clean interpreter, and must register the whole
schema on its own.
"""
import ast
import subprocess
import sys
from pathlib import Path
import app.tables # noqa: F401
from app.db import Base
BACKEND = Path(__file__).parent.parent
APP = BACKEND / "app"
# Each `python -m` module the deployment starts, and the API's app module.
ENTRY_POINTS = ("app.main", "app.worker", "app.downloads.runner")
_PROBE = (
"import importlib, sys\n"
"importlib.import_module(sys.argv[1])\n"
"from sqlalchemy.orm import configure_mappers\n"
"from app.db import Base\n"
"configure_mappers()\n"
"print('\\n'.join(sorted(Base.metadata.tables)))\n"
)
def test_each_entry_point_registers_the_whole_schema_on_its_own():
whole = sorted(Base.metadata.tables)
for module in ENTRY_POINTS:
r = subprocess.run(
[sys.executable, "-c", _PROBE, module],
cwd=BACKEND,
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert r.returncode == 0, (module, r.stderr[-2000:])
assert r.stdout.split() == whole, module
def test_every_module_that_runs_as_a_process_is_listed():
mains = set()
for path in APP.rglob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in tree.body:
if (
isinstance(node, ast.If)
and isinstance(node.test, ast.Compare)
and isinstance(node.test.left, ast.Name)
and node.test.left.id == "__name__"
):
rel = path.relative_to(BACKEND).with_suffix("").as_posix()
mains.add(rel.replace("/", "."))
assert mains <= set(ENTRY_POINTS), mains - set(ENTRY_POINTS)