Skip to content

Commit

Permalink
Merge pull request #1 from MartinUrban/redis-sentinel
Browse files Browse the repository at this point in the history
Add support for Redis Sentinel
  • Loading branch information
MartinUrban authored Aug 22, 2024
2 parents bc2fe67 + 1972572 commit cea4384
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 33 deletions.
27 changes: 1 addition & 26 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +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: actions/checkout@v4
- uses: supercharge/[email protected]
- uses: niden/actions-memcached@v7
- uses: hoverkraft-tech/[email protected]
- name: Install testing requirements
run: pip3 install -r requirements/dev.txt
- name: Run tests
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def get():

## Supported Storage Types

- Redis
- Redis (standalone and Sentinel)
- Memcached
- FileSystem
- MongoDB
Expand Down
152 changes: 148 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
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
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions docs/config_example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions docs/config_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/flask_session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/flask_session/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/flask_session/redis/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .redis import RedisSession, RedisSessionInterface # noqa: F401
from .redis_sentinel import RedisSentinelSession, RedisSentinelSessionInterface
57 changes: 57 additions & 0 deletions src/flask_session/redis/redis_sentinel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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
super().__init__(
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
48 changes: 46 additions & 2 deletions tests/test_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from contextlib import contextmanager

import flask
from flask_session.redis import RedisSession
from redis import Redis
from flask_session.defaults import Defaults
from flask_session.redis import RedisSession, RedisSentinelSession
from redis import Redis, Sentinel


class TestRedisSession:
Expand Down Expand Up @@ -40,3 +41,46 @@ 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", 26379), ("127.0.0.1", 26380), ("127.0.0.1", 26381)],
# sentinel_kwargs={"password": "redispassword"},
# socket_timeout=1
)
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(
{"SESSION_TYPE": "redissentinel", "SESSION_REDIS_SENTINEL": self.sentinel}
)

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"

0 comments on commit cea4384

Please sign in to comment.