Added support for Redis sentinel

This commit is contained in:
Antoine Nguyen
2025-03-26 13:22:39 +01:00
parent 6cef7553c8
commit 1ce66dc407
7 changed files with 119 additions and 21 deletions
+22 -2
View File
@@ -108,8 +108,9 @@ jobs:
run: |
pip install -r mysql-requirements.txt
echo "DB=mysql" >> $GITHUB_ENV
- name: Test with pytest
- name: Run tests
if: ${{ matrix.python-version != '3.12' || matrix.database != 'postgres' }}
uses: hoverkraft-tech/compose-action@v2.0.1
run: |
python ./tests.py
cd test_project
@@ -126,7 +127,26 @@ jobs:
LDAP_SERVER_PORT: ${{ job.services.openldap.ports[389] }}
OIDC_RSA_PRIVATE_KEY: ${{ vars.OIDC_RSA_PRIVATE_KEY }}
- name: Test with pytest and coverage
- name: Start redis sentinel
if: ${{ matrix.python-version == '3.12' || matrix.database == 'postgres' }}
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./docker/compose.redis-sentinel.yml"
- name: Run tests againt redis sentinel
if: ${{ matrix.python-version == '3.12' || matrix.database == 'postgres' }}
run: |
cd test_project
coverage run manage.py test modoboa.policyd
env:
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
REDIS_SENTINEL: 1
REDIS_SENTINEL_HOST: localhost
REDIS_SENTINEL_PORT: 26379
REDIS_MASTER: mymaster
OIDC_RSA_PRIVATE_KEY: ${{ vars.OIDC_RSA_PRIVATE_KEY }}
- name: Run tests and coverage
if: ${{ matrix.python-version == '3.12' && matrix.database == 'postgres' }}
run: |
python ./tests.py
+32
View File
@@ -1,3 +1,7 @@
networks:
redis-net:
external: true
services:
redis:
image: redis:5-alpine
@@ -41,6 +45,9 @@ services:
python3 manage.py runserver_plus 0.0.0.0:8000 --cert-file cert.crt"
volumes:
- .:/code
networks:
- default
- redis-net
api-unsecured:
container_name: modoboa_api_unsecured
@@ -89,3 +96,28 @@ services:
- api
volumes:
- ./docker/dovecot_config:/etc/dovecot/conf.d
policyd:
container_name: modoboa_policyd
restart: unless-stopped
build:
context: .
dockerfile: docker/Dockerfile.dev
ports:
- 9999:9999
env_file:
- test_project/oidc_key
environment:
DB: "SQLITE"
DEBUG: "True"
REDIS_HOST: redis
PYTHONPATH: ".."
PYTHONUNBUFFERED: 1
command: >
sh -c "cd test_project
python3 manage.py policy_daemon --host 0.0.0.0 --port 9999 --debug"
volumes:
- .:/code
networks:
- default
- redis-net
+25
View File
@@ -0,0 +1,25 @@
services:
redis-master:
image: redis:7-alpine
redis-slave:
image: redis:7-alpine
command: redis-server --slaveof redis-master 6379
depends_on:
- redis-master
redis-sentinel:
image: redis:7-alpine
depends_on:
- redis-master
- redis-slave
ports:
- "26379:26379"
command: >
sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
echo "sentinel monitor mymaster redis-master 6379 2" >> /etc/sentinel.conf &&
echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
redis-sentinel /etc/sentinel.conf'
+19 -16
View File
@@ -31,6 +31,15 @@ SUCCESS_ACTION = b"dunno"
FAILURE_ACTION = b"defer_if_permit Daily limit reached, retry later"
def get_redis_client():
if not getattr(settings, "REDIS_SENTINEL", False):
return aioredis.from_url(
settings.REDIS_URL, encoding="utf-8", decode_responses=True
)
sentinel = aioredis.sentinel.Sentinel(settings.REDIS_SENTINELS, socket_timeout=0.1)
return sentinel.master_for(settings.REDIS_MASTER, socket_timeout=0.1)
def close_db_connections(func, *args, **kwargs):
"""
Make sure to close all connections to DB.
@@ -144,9 +153,7 @@ async def apply_policies(attributes):
sasl_username = attributes.get("sasl_username")
if not sasl_username:
return SUCCESS_ACTION
rclient = aioredis.from_url(
settings.REDIS_URL, encoding="utf-8", decode_responses=True
)
rclient = get_redis_client()
decr_domain = False
decr_user = False
localpart, domain = split_mailbox(sasl_username)
@@ -200,16 +207,14 @@ async def handle_connection(reader, writer):
async def new_connection(reader, writer):
try:
await asyncio.wait_for(handle_connection(reader, writer), timeout=5)
except asyncio.TimeoutError as err:
logger.warning("Timeout received while handling connection: %s", err)
finally:
writer.close()
if hasattr(writer, "wait_closed"):
# Python 3.7+ only
await writer.wait_closed()
logger.info("exit")
# try:
await asyncio.wait_for(handle_connection(reader, writer), timeout=5)
# except asyncio.TimeoutError as err:
# logger.warning(f"Timeout received while handling connection: {err}")
# finally:
writer.close()
await writer.wait_closed()
logger.info("exit")
def get_next_execution_dt():
@@ -255,9 +260,7 @@ def get_mailboxes_to_reset():
async def reset_counters():
"""Reset all counters."""
rclient = aioredis.from_url(
settings.REDIS_URL, encoding="utf-8", decode_responses=True
)
rclient = get_redis_client()
logger.info("Resetting all counters")
for domain in await get_domains_to_reset():
await rclient.hset(constants.REDIS_HASHNAME, domain.name, domain.message_limit)
+10 -3
View File
@@ -16,9 +16,16 @@ def set_message_limit(instance, key):
old_message_limit = instance._loaded_values.get("message_limit")
if old_message_limit == instance.message_limit:
return
rclient = redis.Redis(
host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_QUOTA_DB
)
if not getattr(settings, "REDIS_SENTINEL", False):
rclient = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_QUOTA_DB,
)
else:
sentinel = redis.sentinel.Sentinel(settings.REDIS_SENTINELS, socket_timeout=0.1)
rclient = sentinel.master_for(settings.REDIS_MASTER, socket_timeout=0.1)
if instance.message_limit is None:
# delete existing key
if rclient.hexists(constants.REDIS_HASHNAME, key):
+2
View File
@@ -0,0 +1,2 @@
WEBMAIL_DEV_USERNAME=tonio@ngyn.org
WEBMAIL_DEV_PASSWORD=Aka/12ctogael
+9
View File
@@ -256,6 +256,15 @@ PID_FILE_STORAGE_PATH = "/tmp"
# REDIS
REDIS_SENTINEL = bool(os.environ.get("REDIS_SENTINEL", False))
REDIS_SENTINELS = [
(
os.environ.get("REDIS_SENTINEL_HOST", "127.0.0.1"),
os.environ.get("REDIS_SENTINEL_PORT", 26379),
)
]
REDIS_MASTER = os.environ.get("REDIS_MASTER", "mymaster")
REDIS_HOST = os.environ.get("REDIS_HOST", "127.0.0.1")
REDIS_PORT = os.environ.get("REDIS_PORT", 6379)
REDIS_QUOTA_DB = 0