Repository: pretix/pretix

Issue:
Support redis sentinel
Redis Sentinel provides high availability for Redis \[[redis docs](https://redis.io/topics/sentinel)\].
django-sentinel does not yet support redis sentinel \[[open issue](https://github.com/jazzband/django-redis/issues/426)\]
Therefore, afaik pretix does not support redis sentinel.

It would be great if this support could be added.
Maybe https://pypi.org/project/django-sentinel/ helps with that.


Discussion:
Sadly https://pypi.org/project/django-sentinel/ requires an old version of django-redis.
As far as i can tell https://pypi.org/project/django-sentinel-sifter/ fixes this.
Working on a PR. Will submit once ready.
Hmm.

On one hand, I'm personally not really keen on introducing another small, unclear-whether-maintained dependency to support a setup that very few pretix users use and is therefore likely to break :\ In principle, I'd rather wait on django-redis to support this, although it's not clear to me whether that will happen. 

On the other hand, the implementation of django-sentinel-sifter is a mere 125-line wrapper around redis-py, so it's hopefully unlikely to break often.

At pretix.eu, we do use redis sentinel for our production setup, but we use a haproxy installation in front of it that just redirects the TCP connections to the current master, which adds complexity in the server administration but keeps the complexity out of pretix/django. It's unclear to me whether doing this in python instead would have a significant upside or downside.
Yes, I understand that. I would prefer a solution directly in django-sentinel as well.
Thanks for pointing out the solution with haproxy, I didn't think of doing something like that.
But I think the benefit of doing it in python is reducing the complexity of deploying pretix in self hosted scenarios,
especially in cloud infrastructure, where Redis Sentinel is a common thing.

Resolving patch:
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -38,6 +38,7 @@
 import os
 import sys
 from urllib.parse import urlparse
+from json import loads
 
 import django.conf.locale
 from django.utils.crypto import get_random_string
@@ -247,23 +248,35 @@
     }
 
 HAS_REDIS = config.has_option('redis', 'location')
+USE_REDIS_SENTINEL = config.has_option('redis', 'sentinels')
+HAS_REDIS_PASSWORD = config.has_option('redis', 'password')
 if HAS_REDIS:
+    OPTIONS = {
+        "CLIENT_CLASS": "django_redis.client.DefaultClient",
+        "REDIS_CLIENT_KWARGS": {"health_check_interval": 30}
+    }
+
+    if USE_REDIS_SENTINEL:
+        DJANGO_REDIS_CONNECTION_FACTORY = "django_redis.pool.SentinelConnectionFactory"
+        OPTIONS["CLIENT_CLASS"] = "django_redis.client.SentinelClient"
+        OPTIONS["CONNECTION_POOL_CLASS"] = "redis.sentinel.SentinelConnectionPool"
+        # See https://github.com/jazzband/django-redis/issues/540
+        OPTIONS["SENTINEL_KWARGS"] = {"socket_timeout": 1}
+        OPTIONS["SENTINELS"] = [tuple(sentinel) for sentinel in loads(config.get('redis', 'sentinels'))]
+
+    if HAS_REDIS_PASSWORD:
+        OPTIONS["PASSWORD"] = config.get('redis', 'password')
+
     CACHES['redis'] = {
         "BACKEND": "django_redis.cache.RedisCache",
         "LOCATION": config.get('redis', 'location'),
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.DefaultClient",
-            "REDIS_CLIENT_KWARGS": {"health_check_interval": 30}
-        }
+        "OPTIONS": OPTIONS
     }
     CACHES['redis_sessions'] = {
         "BACKEND": "django_redis.cache.RedisCache",
         "LOCATION": config.get('redis', 'location'),
         "TIMEOUT": 3600 * 24 * 30,
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.DefaultClient",
-            "REDIS_CLIENT_KWARGS": {"health_check_interval": 30}
-        }
+        "OPTIONS": OPTIONS
     }
     if not HAS_MEMCACHED:
         CACHES['default'] = CACHES['redis']
@@ -279,9 +292,15 @@
         SESSION_ENGINE = "django.contrib.sessions.backends.db"
 
 HAS_CELERY = config.has_option('celery', 'broker')
+HAS_CELERY_BROKER_TRANSPORT_OPTS = config.has_option('celery', 'broker_transport_options')
+HAS_CELERY_BACKEND_TRANSPORT_OPTS = config.has_option('celery', 'backend_transport_options')
 if HAS_CELERY:
     CELERY_BROKER_URL = config.get('celery', 'broker')
     CELERY_RESULT_BACKEND = config.get('celery', 'backend')
+    if HAS_CELERY_BROKER_TRANSPORT_OPTS:
+        CELERY_BROKER_TRANSPORT_OPTIONS = loads(config.get('celery', 'broker_transport_options'))
+    if HAS_CELERY_BACKEND_TRANSPORT_OPTS:
+        CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = loads(config.get('celery', 'backend_transport_options'))
 else:
     CELERY_TASK_ALWAYS_EAGER = True
 
diff --git a/src/setup.py b/src/setup.py
--- a/src/setup.py
+++ b/src/setup.py
@@ -186,7 +186,7 @@ def run(self):
         'django-oauth-toolkit==1.2.*',
         'django-otp==0.7.*,>=0.7.5',
         'django-phonenumber-field==4.0.*',
-        'django-redis==4.11.*',
+        'django-redis==5.0.*',
         'django-scopes==1.2.*',
         'django-statici18n==1.9.*',
         'djangorestframework==3.12.*',


Test patch: