diff --git a/core/management/commands/dropdb.py b/core/management/commands/dropdb.py index d61b9891..09455366 100644 --- a/core/management/commands/dropdb.py +++ b/core/management/commands/dropdb.py @@ -1,6 +1,6 @@ import sys -import psycopg2 -from psycopg2 import sql +import psycopg +from psycopg import sql from django.core.management.base import BaseCommand from django.conf import settings from django.db import connection @@ -38,8 +38,7 @@ class Command(BaseCommand): maintenance_db = 'postgres' try: self.stdout.write("Connecting to maintenance database...") - conn = psycopg2.connect(dbname=maintenance_db, user=user, password=password, host=host, port=port, **ssl_kwargs) - conn.autocommit = True + conn = psycopg.connect(dbname=maintenance_db, user=user, password=password, host=host, port=port, autocommit=True, **ssl_kwargs) cur = conn.cursor() self.stdout.write(f"Dropping database '{db_name}'...") cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(db_name))) diff --git a/core/tests.py b/core/tests.py index b4770f92..9c758e76 100644 --- a/core/tests.py +++ b/core/tests.py @@ -153,7 +153,7 @@ class EpgIgnoreListsTest(TestCase): class DropDBCommandTlsTest(TestCase): - """Verify dropdb management command passes TLS parameters to psycopg2.""" + """Verify dropdb management command passes TLS parameters to psycopg.""" databases = [] _DB_WITH_TLS = { @@ -184,7 +184,7 @@ class DropDBCommandTlsTest(TestCase): } } - @patch('core.management.commands.dropdb.psycopg2.connect') + @patch('core.management.commands.dropdb.psycopg.connect') @patch('core.management.commands.dropdb.connection') @patch('builtins.input', return_value='yes') def test_dropdb_passes_ssl_kwargs_when_tls_enabled(self, _inp, _conn, mock_connect): @@ -199,13 +199,14 @@ class DropDBCommandTlsTest(TestCase): mock_connect.assert_called_once_with( dbname='postgres', user='testuser', password='testpass', host='localhost', port=5432, + autocommit=True, sslmode='verify-full', sslrootcert='/certs/ca.crt', sslcert='/certs/client.crt', sslkey='/certs/client.key', ) - @patch('core.management.commands.dropdb.psycopg2.connect') + @patch('core.management.commands.dropdb.psycopg.connect') @patch('core.management.commands.dropdb.connection') @patch('builtins.input', return_value='yes') def test_dropdb_no_ssl_kwargs_when_tls_disabled(self, _inp, _conn, mock_connect): @@ -220,4 +221,5 @@ class DropDBCommandTlsTest(TestCase): mock_connect.assert_called_once_with( dbname='postgres', user='testuser', password='testpass', host='localhost', port=5432, + autocommit=True, ) diff --git a/dispatcharr/gevent_patch.py b/dispatcharr/gevent_patch.py index 86ac92ca..1c9f9727 100644 --- a/dispatcharr/gevent_patch.py +++ b/dispatcharr/gevent_patch.py @@ -1,24 +1,20 @@ """ Loaded via uWSGI's `import = dispatcharr.gevent_patch` directive. -Two things happen here: +gevent stdlib monkey-patching - replaces blocking socket/threading/os +primitives with gevent-cooperative versions. `gevent-early-monkey-patch = true` +in the ini should already have done this; calling it again is a safe no-op if +it did, and a necessary fallback if it didn't (e.g. older uWSGI build). -1. gevent stdlib monkey-patching - replaces blocking socket/threading/os - primitives with gevent-cooperative versions. `gevent-early-monkey-patch = true` - in the ini should already have done this; calling it again is a safe no-op if - it did, and a necessary fallback if it didn't (e.g. older uWSGI build). - -2. psycogreen - installs a wait callback on psycopg2 so libpq yields to the - gevent hub during I/O instead of blocking the OS thread. - -Without (1), `async_to_sync(channel_layer.group_send)` in send_websocket_update +Without this, `async_to_sync(channel_layer.group_send)` in send_websocket_update calls epoll_wait() directly, which blocks the OS thread and freezes all greenlets -on the worker until the call returns. With (1), select.epoll is replaced by -monkey-patching, which breaks asyncio event loop creation in threadpool threads. +on the worker until the call returns. With monkey-patching, select.epoll is +replaced, which breaks asyncio event loop creation in threadpool threads. send_websocket_update therefore uses a synchronous Redis path in gevent workers instead of asyncio - see _gevent_ws_send() in core/utils.py. -Without (2), psycopg2 network calls pin the worker during slow/stalled queries. +psycopg3 uses Python's socket layer for I/O, so monkey.patch_all() provides +gevent compatibility without any additional driver patching. Celery and Daphne run in separate daemon processes and do not load this module. @@ -40,15 +36,4 @@ if not monkey.is_module_patched("socket"): else: print("[gevent_patch] gevent stdlib monkey-patching already active.", flush=True) -try: - from psycogreen.gevent import patch_psycopg - patch_psycopg() - print("[gevent_patch] psycogreen: psycopg2 patched for gevent.", flush=True) -except ImportError: - print( - "[gevent_patch] WARNING: psycogreen not installed - " - "psycopg2 will block the gevent hub during DB I/O. " - "Run: uv pip install psycogreen", - file=sys.stderr, - flush=True, - ) + diff --git a/docker/uwsgi.debug.ini b/docker/uwsgi.debug.ini index 9a65db66..d5f577b3 100644 --- a/docker/uwsgi.debug.ini +++ b/docker/uwsgi.debug.ini @@ -42,8 +42,8 @@ gevent = 100 # Patch the stdlib (socket, threading, time, ...) before any app code # loads so blocking calls yield to the gevent hub. Without this, a single -# blocking psycopg2 / requests / DNS call freezes every greenlet on the -# worker. The companion module greens psycopg2 specifically. +# blocking requests / DNS call freezes every greenlet on the +# worker. psycopg3 uses Python's socket layer, so no additional patching is needed. gevent-early-monkey-patch = true import = dispatcharr.gevent_patch diff --git a/docker/uwsgi.dev.ini b/docker/uwsgi.dev.ini index 852a5114..a481952d 100644 --- a/docker/uwsgi.dev.ini +++ b/docker/uwsgi.dev.ini @@ -45,8 +45,8 @@ gevent = 100 # Patch the stdlib (socket, threading, time, ...) before any app code # loads so blocking calls yield to the gevent hub. Without this, a single -# blocking psycopg2 / requests / DNS call freezes every greenlet on the -# worker. The companion module greens psycopg2 specifically. +# blocking requests / DNS call freezes every greenlet on the +# worker. psycopg3 uses Python's socket layer, so no additional patching is needed. gevent-early-monkey-patch = true import = dispatcharr.gevent_patch diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index 76aaebdf..8e8f08dc 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -48,8 +48,8 @@ gevent = 400 # Each unused greenlet costs ~2-4KB of memory # Patch the stdlib (socket, threading, time, ...) before any app code # loads so blocking calls yield to the gevent hub. Without this, a single -# blocking psycopg2 / requests / DNS call freezes every greenlet on the -# worker. The companion module greens psycopg2 specifically. +# blocking requests / DNS call freezes every greenlet on the +# worker. psycopg3 uses Python's socket layer, so no additional patching is needed. gevent-early-monkey-patch = true import = dispatcharr.gevent_patch diff --git a/docker/uwsgi.modular.ini b/docker/uwsgi.modular.ini index 290081b0..7124b10d 100644 --- a/docker/uwsgi.modular.ini +++ b/docker/uwsgi.modular.ini @@ -44,8 +44,8 @@ gevent = 400 # Each unused greenlet costs ~2-4KB of memory # Patch the stdlib (socket, threading, time, ...) before any app code # loads so blocking calls yield to the gevent hub. Without this, a single -# blocking psycopg2 / requests / DNS call freezes every greenlet on the -# worker. The companion module greens psycopg2 specifically. +# blocking requests / DNS call freezes every greenlet on the +# worker. psycopg3 uses Python's socket layer, so no additional patching is needed. gevent-early-monkey-patch = true import = dispatcharr.gevent_patch