diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index f85763d5f..f3ed824a2 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -15,6 +15,7 @@ from celery.signals import task_postrun from celery.signals import task_prerun from celery.signals import task_revoked from celery.signals import worker_process_init +from celery.signals import worker_process_shutdown from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User @@ -1339,6 +1340,20 @@ def close_connection_pool_on_worker_init(**kwargs) -> None: conn.close_pool() +@worker_process_shutdown.connect +def close_connection_pool_on_worker_shutdown(**kwargs) -> None: # pragma: no cover + """ + Close the DB connection pool when a Celery child process exits. + + With CELERY_WORKER_MAX_TASKS_PER_CHILD=1 each child is replaced after a + single task. Without closing the pool on shutdown, its connections linger + on the server until TCP keepalive reaps them, accumulating over time. + """ + for conn in connections.all(initialized_only=True): + if conn.alias == "default" and hasattr(conn, "pool") and conn.pool: + conn.close_pool() + + def add_or_update_document_in_llm_index(sender, document, **kwargs): """ Add or update a document in the LLM index when it is created or updated. diff --git a/src/paperless/settings/custom.py b/src/paperless/settings/custom.py index d2a853c61..ef78e8dcb 100644 --- a/src/paperless/settings/custom.py +++ b/src/paperless/settings/custom.py @@ -252,6 +252,9 @@ def parse_db_settings(data_dir: Path) -> dict[str, dict[str, Any]]: "NAME": os.getenv("PAPERLESS_DBNAME", "paperless"), "USER": os.getenv("PAPERLESS_DBUSER", "paperless"), "PASSWORD": os.getenv("PAPERLESS_DBPASS", "paperless"), + # Validate pooled connections so a connection closed server-side + # is replaced rather than handed out as "the connection is closed". + "CONN_HEALTH_CHECKS": True, } base_options = { diff --git a/src/paperless/tests/settings/test_custom_parsers.py b/src/paperless/tests/settings/test_custom_parsers.py index 107d57bb2..5bf251cad 100644 --- a/src/paperless/tests/settings/test_custom_parsers.py +++ b/src/paperless/tests/settings/test_custom_parsers.py @@ -398,6 +398,7 @@ class TestParseDbSettings: { "default": { "ENGINE": "django.db.backends.postgresql", + "CONN_HEALTH_CHECKS": True, "HOST": "localhost", "NAME": "paperless", "USER": "paperless", @@ -426,6 +427,7 @@ class TestParseDbSettings: { "default": { "ENGINE": "django.db.backends.postgresql", + "CONN_HEALTH_CHECKS": True, "HOST": "paperless-db-host", "PORT": 1111, "NAME": "customdb", @@ -455,6 +457,7 @@ class TestParseDbSettings: { "default": { "ENGINE": "django.db.backends.postgresql", + "CONN_HEALTH_CHECKS": True, "HOST": "pghost", "NAME": "paperless", "USER": "paperless", @@ -485,6 +488,7 @@ class TestParseDbSettings: { "default": { "ENGINE": "django.db.backends.postgresql", + "CONN_HEALTH_CHECKS": True, "HOST": "pghost", "NAME": "paperless", "USER": "paperless",