diff --git a/core/apps.py b/core/apps.py index 4b7fed5c..ee182e89 100644 --- a/core/apps.py +++ b/core/apps.py @@ -25,14 +25,6 @@ class CoreConfig(AppConfig): import core.signals from dispatcharr.app_initialization import should_skip_initialization - # Force UTC0 on every new DB connection. - from django.db.backends.signals import connection_created - - def _force_utc0(sender, connection, **kwargs): - connection.cursor().execute("SET TIME ZONE 'UTC0'") - - connection_created.connect(_force_utc0, dispatch_uid='force_db_utc0') - # Sync developer notifications and check for version updates on startup # Only run in the main process (not in management commands, migrations, or workers) if should_skip_initialization(): diff --git a/core/tests/test_db_session_timezone.py b/core/tests/test_db_session_timezone.py new file mode 100644 index 00000000..bae5373a --- /dev/null +++ b/core/tests/test_db_session_timezone.py @@ -0,0 +1,49 @@ +"""DB sessions must run in UTC regardless of the server default timezone.""" + +import copy +import unittest + +from django.db import connection, connections +from django.test import TestCase + + +@unittest.skipUnless( + connection.vendor == "postgresql", + "PostgreSQL-only: exercises the psycopg3 gevent pool backend", +) +class DatabaseSessionTimezoneTests(TestCase): + def _pool_backend_wrapper(self): + from dispatcharr.db.backends.postgresql_psycopg3.base import DatabaseWrapper + + settings_dict = copy.deepcopy(connections.databases["default"]) + settings_dict["ENGINE"] = "dispatcharr.db.backends.postgresql_psycopg3" + settings_dict.setdefault("OPTIONS", {}) + return DatabaseWrapper(settings_dict, alias="tz_probe") + + @staticmethod + def _teardown_wrapper(wrapper): + wrapper.close() + pool = wrapper._connection_pools.pop("tz_probe", None) + if pool is not None: + pool.close() + + def test_pool_session_timezone_pinned_to_utc(self): + wrapper = self._pool_backend_wrapper() + try: + wrapper.connect() + with wrapper.connection.cursor() as cursor: + cursor.execute("SHOW TimeZone") + self.assertEqual(cursor.fetchone()[0], "UTC") + finally: + self._teardown_wrapper(wrapper) + + def test_pool_session_timezone_is_session_default(self): + wrapper = self._pool_backend_wrapper() + try: + wrapper.connect() + with wrapper.connection.cursor() as cursor: + cursor.execute("RESET TimeZone") + cursor.execute("SHOW TimeZone") + self.assertEqual(cursor.fetchone()[0], "UTC") + finally: + self._teardown_wrapper(wrapper) diff --git a/dispatcharr/db/backends/postgresql_psycopg3/base.py b/dispatcharr/db/backends/postgresql_psycopg3/base.py index 098a01f4..c3bca122 100644 --- a/dispatcharr/db/backends/postgresql_psycopg3/base.py +++ b/dispatcharr/db/backends/postgresql_psycopg3/base.py @@ -41,6 +41,9 @@ class DatabaseWrapper(DatabaseWrapperMixin, OriginalDatabaseWrapper): conn_params = super().get_connection_params() conn_params["application_name"] = db_application_name() + # Force UTC on every new DB connection. + existing_options = conn_params.get("options", "") + conn_params["options"] = f"{existing_options} -c TimeZone=UTC".strip() for attr in ("MAX_CONNS", "REUSE_CONNS", "CONN_MAX_LIFETIME"): if attr in self.settings_dict["OPTIONS"]: conn_params[attr] = self.settings_dict["OPTIONS"][attr]