From 9b6c793b1186da86c2860f74006ee8b42d23ad1c Mon Sep 17 00:00:00 2001 From: Endoze Date: Thu, 26 Feb 2026 09:41:57 -0500 Subject: [PATCH 1/2] fix(wsgi): ensure gevent monkey-patching before Django import chain Celery's Redis connection pool was silently failing in modular deployment mode because Django's setup imports celery.py before uWSGI's gevent plugin reliably patches stdlib sockets. Moving patch_all() to the top of wsgi.py guarantees all broker connections are gevent-aware regardless of uWSGI initialization order. --- dispatcharr/tests/__init__.py | 0 dispatcharr/tests/test_wsgi.py | 39 ++++++++++++++++++++++++++++++++++ dispatcharr/wsgi.py | 15 +++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 dispatcharr/tests/__init__.py create mode 100644 dispatcharr/tests/test_wsgi.py diff --git a/dispatcharr/tests/__init__.py b/dispatcharr/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dispatcharr/tests/test_wsgi.py b/dispatcharr/tests/test_wsgi.py new file mode 100644 index 00000000..cdf7a2e9 --- /dev/null +++ b/dispatcharr/tests/test_wsgi.py @@ -0,0 +1,39 @@ +"""Tests for dispatcharr/wsgi.py gevent monkey-patching.""" + +import os +import subprocess +import sys +import unittest + + +class TestWSGIGeventPatching(unittest.TestCase): + """Verify that wsgi.py applies gevent monkey-patching before Django imports. + + These tests run in subprocesses because monkey.patch_all() must execute + before any other imports. By the time Django's test runner loads this + file, ssl/socket are already imported, so calling patch_all() in-process + would fail. A subprocess mirrors how uWSGI actually loads wsgi.py. + """ + + def test_socket_is_patched_after_wsgi_import(self): + """Loading wsgi.py first (as uWSGI does) should patch sockets.""" + result = subprocess.run( + [ + sys.executable, "-c", + "import dispatcharr.wsgi; " + "from gevent import monkey; " + "assert monkey.is_module_patched('socket'), " + "'socket module was not patched by wsgi.py'; " + "print('PASS')", + ], + capture_output=True, + text=True, + env={**os.environ, "DJANGO_SETTINGS_MODULE": "dispatcharr.settings"}, + timeout=60, + cwd="/app", + ) + self.assertEqual( + result.returncode, + 0, + f"monkey-patching check failed:\n{result.stderr}", + ) diff --git a/dispatcharr/wsgi.py b/dispatcharr/wsgi.py index 29c2ba01..6e8860af 100644 --- a/dispatcharr/wsgi.py +++ b/dispatcharr/wsgi.py @@ -1,6 +1,21 @@ """ WSGI config for dispatcharr project. """ +# When running under uWSGI with gevent, ensure monkey-patching is fully +# applied before any other imports. Django's setup triggers the import of +# dispatcharr/__init__.py → celery.py, which initialises the Celery broker +# transport. If those imports happen before sockets are patched, Celery's +# Redis connection pool holds unpatched sockets that silently fail under +# gevent's cooperative scheduling. Calling patch_all() here—before the +# Django import chain—guarantees every socket (including pooled broker +# connections) is gevent-aware. The call is idempotent, so it's harmless +# if uWSGI's gevent plugin has already patched. +try: + from gevent import monkey + monkey.patch_all() +except ImportError: + pass + import os from django.core.wsgi import get_wsgi_application From dcb3cf8ea866a9e4928e476bbb5398b2e979bb6f Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 3 Mar 2026 11:56:06 -0600 Subject: [PATCH 2/2] changelog: Update changelog for monkey patch PR. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fadc44e8..55fac12c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Celery Redis connections using unpatched sockets under gevent: `dispatcharr/__init__.py` imports `celery.py` as part of `django.setup()`, initialising the Redis connection pool before uWSGI's gevent plugin patches stdlib sockets. Added `gevent.monkey.patch_all()` at the top of `wsgi.py` to guarantee all broker connections are gevent-aware. - Thanks [@endoze](https://github.com/endoze) - TS proxy streams dying after 30–200 seconds in multi-worker uWSGI/Celery deployments, caused by three interrelated bugs. (Fixes #992, #980) - Thanks [@PFalko](https://github.com/PFalko) - **Double ProxyServer instantiation**: `ProxyConfig.ready()` called `TSProxyServer()` directly while `TSProxyConfig.ready()` also called `TSProxyServer.get_instance()`, creating two instances per worker — each with its own cleanup thread. The orphaned thread could not extend ownership because it had no entries in `stream_managers`. Fixed by using `TSProxyServer.get_instance()` in `ProxyConfig.ready()`. - **`flushdb()` on every Redis client init**: `RedisClient.get_client()` called `client.flushdb()` whenever `_client` was `None`. Celery autoscale (`--autoscale=6,1`) spawning new workers mid-stream triggered this path, nuking all Redis keys including active ownership keys, client records, and channel metadata. Removed the `flushdb()` call entirely.