diff --git a/src/core/runner.py b/src/core/runner.py index b800ea7..2dc9327 100644 --- a/src/core/runner.py +++ b/src/core/runner.py @@ -58,3 +58,42 @@ class RobustCompressorTestRunner(DiscoverRunner): except PermissionError: time.sleep(0.05) raise CompressorFileStorage.save = _robust_save + + def teardown_databases(self, old_config, **kwargs): + """Force-close any lingering DB connections before Django issues + `DROP DATABASE`. Without this, CI fails post-test w. Postgres' + `ObjectInUse: database "..." is being accessed by other users` + even on a fully-green run. + + Root cause: `core.settings.DATABASES['default']['conn_max_age'] + = 600` keeps Postgres connections alive in the per-thread pool + for 10 min. Channels' `database_sync_to_async` (used in + `apps.epic.tests.integrated.test_consumers`'s `CursorMoveConsumerTest` + + `SigHoverConsumerTest`) runs in a global asgiref threadpool; + each worker thread accumulates its own DB connection that + outlives the test + sits idle in the pool when teardown fires. + Postgres refuses the drop while ANY session targets the row. + + Two-step fix — `connections.close_all()` covers the main + thread (test runner's own connection); `pg_terminate_backend` + targets any worker-thread session still pinning the test DB. + Both are safe on a green test run: the DB is about to be + dropped anyway, and prod's `CONN_MAX_AGE=600` stays untouched + (the fix lives in the test runner, not in `settings.py`). + + Local dev (SQLite, no DROP step) is unaffected; CI Postgres + is where the leak was visible. See [[feedback-test-teardown-conn-leak]]. + """ + from django.db import connections + connections.close_all() + for connection, _, _ in old_config: + if connection.vendor == "postgresql": + test_db_name = connection.settings_dict["NAME"] + with connection._nodb_cursor() as cursor: + cursor.execute( + "SELECT pg_terminate_backend(pid) " + "FROM pg_stat_activity " + "WHERE datname = %s AND pid <> pg_backend_pid()", + [test_db_name], + ) + super().teardown_databases(old_config, **kwargs)