From 70ee4f3d9522ba556b838812ac09a8dc4178311b Mon Sep 17 00:00:00 2001 From: Martin Baier Date: Wed, 21 Aug 2024 16:50:17 +0200 Subject: [PATCH 1/5] First tests to implement redis sentinel --- docker-compose-redis-sentinel.yml | 225 ++++++++++++++++++++++ src/flask_session/__init__.py | 11 ++ src/flask_session/defaults.py | 4 + src/flask_session/redis/__init__.py | 1 + src/flask_session/redis/redis_sentinel.py | 52 +++++ tests/test_redis.py | 31 ++- 6 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 docker-compose-redis-sentinel.yml create mode 100644 src/flask_session/redis/redis_sentinel.py diff --git a/docker-compose-redis-sentinel.yml b/docker-compose-redis-sentinel.yml new file mode 100644 index 00000000..42f80c42 --- /dev/null +++ b/docker-compose-redis-sentinel.yml @@ -0,0 +1,225 @@ +version: '3.8' + +services: + # redis-master: + # container_name: redis-master + # image: 'bitnami/redis:latest' + # environment: + # - REDIS_REPLICATION_MODE=master + # - ALLOW_EMPTY_PASSWORD=yes + # # - REDIS_PASSWORD=redispassword + # # we can not use the default port 6379 here as the redis standalone test uses this port already + # - REDIS_PORT_NUMBER=6380 + # ports: + # - "6380:6380" + # redis-slave: + # container_name: redis-slave + # image: 'bitnami/redis:latest' + # environment: + # - REDIS_REPLICATION_MODE=slave + # - REDIS_MASTER_HOST=redis-master + # - ALLOW_EMPTY_PASSWORD=yes + # # - REDIS_MASTER_PASSWORD=redispassword + # - REDIS_PASSWORD=redispassword + # - REDIS_MASTER_PORT_NUMBER=6380 + # - REDIS_PORT_NUMBER=6380 + # ports: + # - "16380:6380" + # depends_on: + # - redis-master + # redis-sentinel-1: + # image: 'bitnami/redis-sentinel:latest' + # container_name: redis-sentinel-1 + # environment: + # - REDIS_MASTER_SET=mymaster + # - REDIS_MASTER_HOST=redis-master + # - ALLOW_EMPTY_PASSWORD=yes + # # - REDIS_MASTER_PASSWORD=redispassword + # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 + # - REDIS_MASTER_PORT_NUMBER=6380 + # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 + # # - REDIS_SENTINEL_ANNOUNCE_PORT=36379 + # depends_on: + # - redis-master + # - redis-slave + # ports: + # - "36379:26379" + # redis-sentinel-2: + # image: 'bitnami/redis-sentinel:latest' + # container_name: redis-sentinel-2 + # environment: + # - REDIS_MASTER_SET=mymaster + # - REDIS_MASTER_HOST=redis-master + # - ALLOW_EMPTY_PASSWORD=yes + # # - REDIS_MASTER_PASSWORD=redispassword + # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 + # - REDIS_MASTER_PORT_NUMBER=6380 + # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 + # # - REDIS_SENTINEL_ANNOUNCE_PORT=36380 + # depends_on: + # - redis-master + # - redis-slave + # ports: + # - "36380:26379" + # redis-sentinel-3: + # image: 'bitnami/redis-sentinel:latest' + # container_name: redis-sentinel-3 + # environment: + # - REDIS_MASTER_SET=mymaster + # - REDIS_MASTER_HOST=redis-master + # - ALLOW_EMPTY_PASSWORD=yes + # # - REDIS_MASTER_PASSWORD=redispassword + # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 + # - REDIS_MASTER_PORT_NUMBER=6380 + # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 + # # - REDIS_SENTINEL_ANNOUNCE_PORT=36381 + # depends_on: + # - redis-master + # - redis-slave + # ports: + # - "36381:26379" + redis-master: + image: redis:latest + container_name: redis-master + command: + [ + "redis-server", + "--port", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_master:/data + + + redis-slave-1: + image: redis:latest + container_name: redis-slave-1 + depends_on: + - redis-master + command: + [ + "redis-server", + "--port", + "6381", + "--replicaof", + "127.0.0.1", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_slave_1:/data + + + redis-slave-2: + image: redis:latest + container_name: redis-slave-2 + depends_on: + - redis-master + command: + [ + "redis-server", + "--port", + "6382", + "--replicaof", + "127.0.0.1", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_slave_2:/data + + + sentinel-1: + image: redis:latest + container_name: sentinel-1 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26379", + "--sentinel", + ] + network_mode: host + ports: + - "26379:26379" + volumes: + - sentinel_1:/data + + + sentinel-2: + image: redis:latest + container_name: sentinel-2 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26380", + "--sentinel", + ] + network_mode: host + volumes: + - sentinel_2:/data + + + sentinel-3: + image: redis:latest + container_name: sentinel-3 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26381", + "--sentinel", + ] + network_mode: host + volumes: + - sentinel_3:/data + +volumes: + redis_master: + redis_slave_1: + redis_slave_2: + sentinel_1: + sentinel_2: + sentinel_3: + +configs: + sentinel: + content: | + bind 0.0.0.0 + sentinel monitor mymaster 127.0.0.1 6380 2 + sentinel resolve-hostnames yes + sentinel down-after-milliseconds mymaster 5000 + sentinel failover-timeout mymaster 5000 + sentinel parallel-syncs mymaster 1 \ No newline at end of file diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index 5d4caee9..96e5ca87 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -61,6 +61,9 @@ def _get_interface(self, app): # Redis settings SESSION_REDIS = config.get("SESSION_REDIS", Defaults.SESSION_REDIS) + SESSION_REDIS_SENTINEL = config.get("SESSION_REDIS_SENTINEL", Defaults.SESSION_REDIS_SENTINEL) + SESSION_REDIS_SENTINEL_MASTER_SET = config.get("SESSION_REDIS_SENTINEL_MASTER_SET", Defaults.SESSION_REDIS_SENTINEL_MASTER_SET) + # Memcached settings SESSION_MEMCACHED = config.get("SESSION_MEMCACHED", Defaults.SESSION_MEMCACHED) @@ -144,6 +147,14 @@ def _get_interface(self, app): **common_params, client=SESSION_REDIS, ) + elif SESSION_TYPE == "redissentinel": + from .redis import RedisSentinelSessionInterface + + session_interface = RedisSentinelSessionInterface( + **common_params, + client=SESSION_REDIS_SENTINEL, + master=SESSION_REDIS_SENTINEL_MASTER_SET, + ) elif SESSION_TYPE == "memcached": from .memcached import MemcachedSessionInterface diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index f1bc1501..e2b5ad89 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -16,6 +16,10 @@ class Defaults: # Redis settings SESSION_REDIS = None + # Redis Sentinal settings + SESSION_REDIS_SENTINEL = None + SESSION_REDIS_SENTINEL_MASTER_SET = "mymaster" + # Memcached settings SESSION_MEMCACHED = None diff --git a/src/flask_session/redis/__init__.py b/src/flask_session/redis/__init__.py index 65b6323a..84b0ae76 100644 --- a/src/flask_session/redis/__init__.py +++ b/src/flask_session/redis/__init__.py @@ -1 +1,2 @@ from .redis import RedisSession, RedisSessionInterface # noqa: F401 +from .redis_sentinel import RedisSentinelSession, RedisSentinelSessionInterface diff --git a/src/flask_session/redis/redis_sentinel.py b/src/flask_session/redis/redis_sentinel.py new file mode 100644 index 00000000..195e40c9 --- /dev/null +++ b/src/flask_session/redis/redis_sentinel.py @@ -0,0 +1,52 @@ +from typing import Optional + +from flask import Flask +from redis import Sentinel + +from .redis import RedisSessionInterface +from ..base import ServerSideSession +from ..defaults import Defaults + + +class RedisSentinelSession(ServerSideSession): + pass + + +class RedisSentinelSessionInterface(RedisSessionInterface): + """Uses the Redis key-value store deployed in a high availability mode as a session storage. (`redis-py` required) + + :param client: A ``redis.Sentinel`` instance. + :param master: The name of the master node. + :param key_prefix: A prefix that is added to all storage keys. + :param use_signer: Whether to sign the session id cookie or not. + :param permanent: Whether to use permanent session or not. + :param sid_length: The length of the generated session id in bytes. + :param serialization_format: The serialization format to use for the session data. + """ + + session_class = RedisSentinelSession + ttl = True + + def __init__( + self, + app: Flask, + client: Optional[Sentinel] = Defaults.SESSION_REDIS_SENTINEL, + master: str = Defaults.SESSION_REDIS_SENTINEL_MASTER_SET, + key_prefix: str = Defaults.SESSION_KEY_PREFIX, + use_signer: bool = Defaults.SESSION_USE_SIGNER, + permanent: bool = Defaults.SESSION_PERMANENT, + sid_length: int = Defaults.SESSION_ID_LENGTH, + serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, + ): + if client is None or not isinstance(client, Sentinel): + raise TypeError("No valid Sentinel instance provided.") + self.sentinel = client + self.master = master + self.client = client + super().__init__( + app, key_prefix, use_signer, permanent, sid_length, serialization_format + ) + + @property + def client(self): + return self.sentinel.master_for(self.master) diff --git a/tests/test_redis.py b/tests/test_redis.py index 7c59964b..1e93fa69 100644 --- a/tests/test_redis.py +++ b/tests/test_redis.py @@ -2,8 +2,9 @@ from contextlib import contextmanager import flask +from flask_session.defaults import Defaults from flask_session.redis import RedisSession -from redis import Redis +from redis import Redis, Sentinel class TestRedisSession: @@ -40,3 +41,31 @@ def test_redis_default(self, app_utils): json.loads(byte_string.decode("utf-8")) if byte_string else {} ) assert stored_session.get("value") == "44" + + +class TestRedisSentinelSession: + """This requires package: redis""" + + @contextmanager + def setup_sentinel(self): + self.sentinel = Sentinel( + [("127.0.0.1", 36379), ("127.0.0.1", 36380), ("127.0.0.1", 36381)], + # sentinel_kwargs={"password": "redispassword"}, + # socket_timeout=1 + ) + host, port = self.sentinel.discover_master("mymaster") + self.master: Redis = self.sentinel.master_for( + Defaults.SESSION_REDIS_SENTINEL_MASTER_SET + + ) + self.master.flushall() + yield + self.master.flushall() + + def test_redis_default(self, app_utils): + with self.setup_sentinel(): + app = app_utils.create_app( + {"SESSION_TYPE": "redissentinel", "SESSION_REDIS_SENTINEL": self.sentinel} + ) + + with app.test_request_context(): From 57f595424b8b47b4a63ba1a074d46fc512ccaee3 Mon Sep 17 00:00:00 2001 From: Martin Baier Date: Wed, 21 Aug 2024 18:46:52 +0200 Subject: [PATCH 2/5] fix redis sentinel --- .github/workflows/test.yaml | 3 + docker-compose.yml | 152 +++++++++++++++++++++- src/flask_session/redis/redis_sentinel.py | 9 +- tests/test_redis.py | 22 +++- 4 files changed, 177 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2351b7d4..df740cca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,6 +31,9 @@ jobs: --health-timeout 5s --health-retries 5 steps: + - uses: hoverkraft-tech/compose-action@v0.0.0 + with: + compose-file: ./docker-compose - uses: actions/checkout@v4 - uses: supercharge/redis-github-action@1.5.0 - uses: niden/actions-memcached@v7 diff --git a/docker-compose.yml b/docker-compose.yml index 3463412d..79969e61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,8 +41,152 @@ services: volumes: - postgres_data:/var/lib/postgresql/data + redis-master: + image: redis:latest + container_name: redis-master + command: + [ + "redis-server", + "--port", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_ha_master_data:/data + + + redis-slave-1: + image: redis:latest + container_name: redis-slave-1 + depends_on: + - redis-master + command: + [ + "redis-server", + "--port", + "6381", + "--replicaof", + "127.0.0.1", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_ha_slave_1_data:/data + + + redis-slave-2: + image: redis:latest + container_name: redis-slave-2 + depends_on: + - redis-master + command: + [ + "redis-server", + "--port", + "6382", + "--replicaof", + "127.0.0.1", + "6380", + "--protected-mode", + "no" + ] + network_mode: host + volumes: + - redis_ha_slave_2_data:/data + + + sentinel-1: + image: redis:latest + container_name: sentinel-1 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26379", + "--sentinel", + ] + network_mode: host + ports: + - "26379:26379" + volumes: + - redis_ha_sentinel_1_data:/data + + + sentinel-2: + image: redis:latest + container_name: sentinel-2 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26380", + "--sentinel", + ] + network_mode: host + volumes: + - redis_ha_sentinel_2_data:/data + + + sentinel-3: + image: redis:latest + container_name: sentinel-3 + depends_on: + - redis-master + configs: + - source: sentinel + target: /data/sentinel.conf + mode: 0660 + uid: "999" + command: + [ + "redis-server", + "sentinel.conf", + "--port", + "26381", + "--sentinel", + ] + network_mode: host + volumes: + - redis_ha_sentinel_3_data:/data + volumes: - postgres_data: - mongo_data: - redis_data: - dynamodb_data: \ No newline at end of file + postgres_data: + mongo_data: + redis_data: + dynamodb_data: + redis_ha_master_data: + redis_ha_slave_1_data: + redis_ha_slave_2_data: + redis_ha_sentinel_1_data: + redis_ha_sentinel_2_data: + redis_ha_sentinel_3_data: + +configs: + sentinel: + content: | + bind 0.0.0.0 + sentinel monitor mymaster 127.0.0.1 6380 2 + sentinel resolve-hostnames yes + sentinel down-after-milliseconds mymaster 5000 + sentinel failover-timeout mymaster 5000 + sentinel parallel-syncs mymaster 1 \ No newline at end of file diff --git a/src/flask_session/redis/redis_sentinel.py b/src/flask_session/redis/redis_sentinel.py index 195e40c9..d94310b7 100644 --- a/src/flask_session/redis/redis_sentinel.py +++ b/src/flask_session/redis/redis_sentinel.py @@ -42,11 +42,16 @@ def __init__( raise TypeError("No valid Sentinel instance provided.") self.sentinel = client self.master = master - self.client = client super().__init__( - app, key_prefix, use_signer, permanent, sid_length, serialization_format + app, self.client, key_prefix, use_signer, permanent, sid_length, serialization_format ) + self._client = None @property def client(self): return self.sentinel.master_for(self.master) + + @client.setter + def client(self, value): + # the _client is only needed and the setter only needed for the inheritance to work + self._client = value diff --git a/tests/test_redis.py b/tests/test_redis.py index 1e93fa69..3e3aeea2 100644 --- a/tests/test_redis.py +++ b/tests/test_redis.py @@ -3,7 +3,7 @@ import flask from flask_session.defaults import Defaults -from flask_session.redis import RedisSession +from flask_session.redis import RedisSession, RedisSentinelSession from redis import Redis, Sentinel @@ -49,19 +49,24 @@ class TestRedisSentinelSession: @contextmanager def setup_sentinel(self): self.sentinel = Sentinel( - [("127.0.0.1", 36379), ("127.0.0.1", 36380), ("127.0.0.1", 36381)], + [("127.0.0.1", 26379), ("127.0.0.1", 26380), ("127.0.0.1", 26381)], # sentinel_kwargs={"password": "redispassword"}, # socket_timeout=1 ) host, port = self.sentinel.discover_master("mymaster") self.master: Redis = self.sentinel.master_for( Defaults.SESSION_REDIS_SENTINEL_MASTER_SET - ) self.master.flushall() yield self.master.flushall() + def retrieve_stored_session(self, key): + master = self.sentinel.master_for( + Defaults.SESSION_REDIS_SENTINEL_MASTER_SET + ) + return master.get(key) + def test_redis_default(self, app_utils): with self.setup_sentinel(): app = app_utils.create_app( @@ -69,3 +74,14 @@ def test_redis_default(self, app_utils): ) with app.test_request_context(): + assert isinstance(flask.session, RedisSentinelSession) + app_utils.test_session(app) + + # Check if the session is stored in Redis + cookie = app_utils.test_session_with_cookie(app) + session_id = cookie.split(";")[0].split("=")[1] + byte_string = self.retrieve_stored_session(f"session:{session_id}") + stored_session = ( + json.loads(byte_string.decode("utf-8")) if byte_string else {} + ) + assert stored_session.get("value") == "44" From d3c85fea5ebef24df3f2740a1a11f5a9b13dfc9e Mon Sep 17 00:00:00 2001 From: Martin Baier Date: Thu, 22 Aug 2024 09:11:36 +0200 Subject: [PATCH 3/5] Use the docker compose file in github actions tests to provide same experience as in local testing --- .github/workflows/test.yaml | 30 +----------------------------- README.md | 2 +- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index df740cca..5f2c4dc5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,37 +6,9 @@ jobs: strategy: matrix: python-version: [3.8, 3.9, 3.10, 3.11, 3.12] - services: - mongodb: - image: mongo - ports: - - 27017:27017 - dynamodb: - image: amazon/dynamodb-local - ports: - - 8000:8000 - - postgresql: - image: postgres:latest - ports: - - 5433:5432 - env: - POSTGRES_PASSWORD: pwd - POSTGRES_USER: root - POSTGRES_DB: dummy - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: - - uses: hoverkraft-tech/compose-action@v0.0.0 - with: - compose-file: ./docker-compose - uses: actions/checkout@v4 - - uses: supercharge/redis-github-action@1.5.0 - - uses: niden/actions-memcached@v7 + - uses: hoverkraft-tech/compose-action@v2.0.1 - name: Install testing requirements run: pip3 install -r requirements/dev.txt - name: Run tests diff --git a/README.md b/README.md index 54187793..9a7ed91c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def get(): ## Supported Storage Types -- Redis +- Redis (standalone and Sentinel) - Memcached - FileSystem - MongoDB From 5f49c835b19fe6907f42db71c00e670a2b2fb83e Mon Sep 17 00:00:00 2001 From: Martin Baier Date: Thu, 22 Aug 2024 09:23:52 +0200 Subject: [PATCH 4/5] remove unneeded code --- docker-compose-redis-sentinel.yml | 225 ------------------------------ tests/test_redis.py | 1 - 2 files changed, 226 deletions(-) delete mode 100644 docker-compose-redis-sentinel.yml diff --git a/docker-compose-redis-sentinel.yml b/docker-compose-redis-sentinel.yml deleted file mode 100644 index 42f80c42..00000000 --- a/docker-compose-redis-sentinel.yml +++ /dev/null @@ -1,225 +0,0 @@ -version: '3.8' - -services: - # redis-master: - # container_name: redis-master - # image: 'bitnami/redis:latest' - # environment: - # - REDIS_REPLICATION_MODE=master - # - ALLOW_EMPTY_PASSWORD=yes - # # - REDIS_PASSWORD=redispassword - # # we can not use the default port 6379 here as the redis standalone test uses this port already - # - REDIS_PORT_NUMBER=6380 - # ports: - # - "6380:6380" - # redis-slave: - # container_name: redis-slave - # image: 'bitnami/redis:latest' - # environment: - # - REDIS_REPLICATION_MODE=slave - # - REDIS_MASTER_HOST=redis-master - # - ALLOW_EMPTY_PASSWORD=yes - # # - REDIS_MASTER_PASSWORD=redispassword - # - REDIS_PASSWORD=redispassword - # - REDIS_MASTER_PORT_NUMBER=6380 - # - REDIS_PORT_NUMBER=6380 - # ports: - # - "16380:6380" - # depends_on: - # - redis-master - # redis-sentinel-1: - # image: 'bitnami/redis-sentinel:latest' - # container_name: redis-sentinel-1 - # environment: - # - REDIS_MASTER_SET=mymaster - # - REDIS_MASTER_HOST=redis-master - # - ALLOW_EMPTY_PASSWORD=yes - # # - REDIS_MASTER_PASSWORD=redispassword - # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 - # - REDIS_MASTER_PORT_NUMBER=6380 - # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 - # # - REDIS_SENTINEL_ANNOUNCE_PORT=36379 - # depends_on: - # - redis-master - # - redis-slave - # ports: - # - "36379:26379" - # redis-sentinel-2: - # image: 'bitnami/redis-sentinel:latest' - # container_name: redis-sentinel-2 - # environment: - # - REDIS_MASTER_SET=mymaster - # - REDIS_MASTER_HOST=redis-master - # - ALLOW_EMPTY_PASSWORD=yes - # # - REDIS_MASTER_PASSWORD=redispassword - # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 - # - REDIS_MASTER_PORT_NUMBER=6380 - # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 - # # - REDIS_SENTINEL_ANNOUNCE_PORT=36380 - # depends_on: - # - redis-master - # - redis-slave - # ports: - # - "36380:26379" - # redis-sentinel-3: - # image: 'bitnami/redis-sentinel:latest' - # container_name: redis-sentinel-3 - # environment: - # - REDIS_MASTER_SET=mymaster - # - REDIS_MASTER_HOST=redis-master - # - ALLOW_EMPTY_PASSWORD=yes - # # - REDIS_MASTER_PASSWORD=redispassword - # - REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000 - # - REDIS_MASTER_PORT_NUMBER=6380 - # # - REDIS_SENTINEL_ANNOUNCE_IP=127.0.0.1 - # # - REDIS_SENTINEL_ANNOUNCE_PORT=36381 - # depends_on: - # - redis-master - # - redis-slave - # ports: - # - "36381:26379" - redis-master: - image: redis:latest - container_name: redis-master - command: - [ - "redis-server", - "--port", - "6380", - "--protected-mode", - "no" - ] - network_mode: host - volumes: - - redis_master:/data - - - redis-slave-1: - image: redis:latest - container_name: redis-slave-1 - depends_on: - - redis-master - command: - [ - "redis-server", - "--port", - "6381", - "--replicaof", - "127.0.0.1", - "6380", - "--protected-mode", - "no" - ] - network_mode: host - volumes: - - redis_slave_1:/data - - - redis-slave-2: - image: redis:latest - container_name: redis-slave-2 - depends_on: - - redis-master - command: - [ - "redis-server", - "--port", - "6382", - "--replicaof", - "127.0.0.1", - "6380", - "--protected-mode", - "no" - ] - network_mode: host - volumes: - - redis_slave_2:/data - - - sentinel-1: - image: redis:latest - container_name: sentinel-1 - depends_on: - - redis-master - configs: - - source: sentinel - target: /data/sentinel.conf - mode: 0660 - uid: "999" - command: - [ - "redis-server", - "sentinel.conf", - "--port", - "26379", - "--sentinel", - ] - network_mode: host - ports: - - "26379:26379" - volumes: - - sentinel_1:/data - - - sentinel-2: - image: redis:latest - container_name: sentinel-2 - depends_on: - - redis-master - configs: - - source: sentinel - target: /data/sentinel.conf - mode: 0660 - uid: "999" - command: - [ - "redis-server", - "sentinel.conf", - "--port", - "26380", - "--sentinel", - ] - network_mode: host - volumes: - - sentinel_2:/data - - - sentinel-3: - image: redis:latest - container_name: sentinel-3 - depends_on: - - redis-master - configs: - - source: sentinel - target: /data/sentinel.conf - mode: 0660 - uid: "999" - command: - [ - "redis-server", - "sentinel.conf", - "--port", - "26381", - "--sentinel", - ] - network_mode: host - volumes: - - sentinel_3:/data - -volumes: - redis_master: - redis_slave_1: - redis_slave_2: - sentinel_1: - sentinel_2: - sentinel_3: - -configs: - sentinel: - content: | - bind 0.0.0.0 - sentinel monitor mymaster 127.0.0.1 6380 2 - sentinel resolve-hostnames yes - sentinel down-after-milliseconds mymaster 5000 - sentinel failover-timeout mymaster 5000 - sentinel parallel-syncs mymaster 1 \ No newline at end of file diff --git a/tests/test_redis.py b/tests/test_redis.py index 3e3aeea2..5c964cef 100644 --- a/tests/test_redis.py +++ b/tests/test_redis.py @@ -53,7 +53,6 @@ def setup_sentinel(self): # sentinel_kwargs={"password": "redispassword"}, # socket_timeout=1 ) - host, port = self.sentinel.discover_master("mymaster") self.master: Redis = self.sentinel.master_for( Defaults.SESSION_REDIS_SENTINEL_MASTER_SET ) From 1972572c695283122b3b721c7b1f6527517b5651 Mon Sep 17 00:00:00 2001 From: Martin Baier Date: Thu, 22 Aug 2024 09:41:02 +0200 Subject: [PATCH 5/5] Add documentation --- docs/api.rst | 1 + docs/config_example.rst | 15 +++++++++++++++ docs/config_reference.rst | 1 + 3 files changed, 17 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index faae610d..da80a3ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,6 +15,7 @@ Anything documented here is part of the public API that Flask-Session provides, :members: regenerate .. autoclass:: flask_session.redis.RedisSessionInterface +.. autoclass:: flask_session.redis.RedisSentinelSessionInterface .. autoclass:: flask_session.memcached.MemcachedSessionInterface .. autoclass:: flask_session.filesystem.FileSystemSessionInterface .. autoclass:: flask_session.cachelib.CacheLibSessionInterface diff --git a/docs/config_example.rst b/docs/config_example.rst index 9960da07..cfc492af 100644 --- a/docs/config_example.rst +++ b/docs/config_example.rst @@ -17,6 +17,21 @@ If you do not set ``SESSION_REDIS``, Flask-Session will assume you are developin :meth:`redis.Redis` instance for you. It is expected you supply an instance of :meth:`redis.Redis` in production. +Similarly, if you use a high-availability setup for Redis using Sentinel you can use the following setup + +.. code-block:: python + + from redis import Sentinel + app.config['SESSION_TYPE'] = 'redissentinel' + app.config['SESSION_REDIS_SENTINEL'] = Sentinel( + [("127.0.0.1", 26379), ("127.0.0.1", 26380), ("127.0.0.1", 26381)], + ) + +It is expected that you set ``SESSION_REDIS_SENTINEL`` to your own :meth:`redis.Sentinel` instance. +The name of the master set is obtained via the config ``SESSION_REDIS_SENTINEL_MASTER_SET`` which defaults to ``mymaster``. + + + .. note:: By default, sessions in Flask-Session are permanent with an expiration of 31 days. \ No newline at end of file diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 9fef8fd5..19761a9b 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -13,6 +13,7 @@ These are specific to Flask-Session. Specifies which type of session interface to use. Built-in session types: - **redis**: RedisSessionInterface + - **redissentinel**: RedisSentinelSessionInterface - **memcached**: MemcachedSessionInterface - **filesystem**: FileSystemSessionInterface (Deprecated in 0.7.0, will be removed in 1.0.0 in favor of CacheLibSessionInterface) - **cachelib**: CacheLibSessionInterface