FastAPI: add redis integration

This includes the addition of the python-fakeredis package,
used for stubbing python-redis when a user does not have a
configured cache.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-06-28 08:49:02 -07:00
parent 96d1af9363
commit 91e769f603
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
7 changed files with 123 additions and 3 deletions

View file

@ -57,7 +57,7 @@ read the instructions below.
(FastAPI-Specific)
# pacman -S redis python-redis
# pacman -S redis python-redis python-fakeredis
# systemctl enable --now redis
5) Create a new MySQL database and a user and import the aurweb SQL schema:

57
aurweb/redis.py Normal file
View file

@ -0,0 +1,57 @@
import logging
import fakeredis
from redis import ConnectionPool, Redis
import aurweb.config
logger = logging.getLogger(__name__)
pool = None
class FakeConnectionPool:
""" A fake ConnectionPool class which holds an internal reference
to a fakeredis handle.
We normally deal with Redis by keeping its ConnectionPool globally
referenced so we can persist connection state through different calls
to redis_connection(), and since FakeRedis does not offer a ConnectionPool,
we craft one up here to hang onto the same handle instance as long as the
same instance is alive; this allows us to use a similar flow from the
redis_connection() user's perspective.
"""
def __init__(self):
self.handle = fakeredis.FakeStrictRedis()
def disconnect(self):
pass
def redis_connection(): # pragma: no cover
global pool
disabled = aurweb.config.get("options", "cache") != "redis"
# If we haven't initialized redis yet, construct a pool.
if disabled:
logger.debug("Initializing fake Redis instance.")
if pool is None:
pool = FakeConnectionPool()
return pool.handle
else:
logger.debug("Initializing real Redis instance.")
if pool is None:
redis_addr = aurweb.config.get("options", "redis_address")
pool = ConnectionPool.from_url(redis_addr)
# Create a connection to the pool.
return Redis(connection_pool=pool)
def kill_redis():
global pool
if pool:
pool.disconnect()
pool = None

View file

@ -36,11 +36,13 @@ enable-maintenance = 1
maintenance-exceptions = 127.0.0.1
render-comment-cmd = /usr/local/bin/aurweb-rendercomment
localedir = /srv/http/aurweb/aur.git/web/locale/
# memcache or apc
; memcache, apc, or redis
; memcache/apc are supported in PHP, redis is supported in Python.
cache = none
cache_pkginfo_ttl = 86400
memcache_servers = 127.0.0.1:11211
salt_rounds = 12
redis_address = redis://localhost
[ratelimit]
request_limit = 4000

View file

@ -28,10 +28,13 @@ enable-maintenance = 0
localedir = YOUR_AUR_ROOT/web/locale
; In production, salt_rounds should be higher; suggested: 12.
salt_rounds = 4
; See config.defaults comment about cache.
cache = none
; In docker, the memcached host is available. On a user's system,
; this should be set to localhost (most likely).
memcache_servers = memcached:11211
; If cache = 'redis' this address is used to connect to Redis.
redis_address = redis://127.0.0.1
[notifications]
; For development/testing, use /usr/bin/sendmail

View file

@ -15,6 +15,6 @@ pacman -Syu --noconfirm --noprogressbar \
python-email-validator openssh python-lxml mariadb mariadb-libs \
python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \
python-asgiref uvicorn python-feedgen memcached php-memcached \
python-redis redis
python-redis redis python-fakeredis
exec "$@"

View file

@ -9,6 +9,24 @@ from fastapi import HTTPException
import aurweb.asgi
import aurweb.config
import aurweb.redis
@pytest.mark.asyncio
async def test_asgi_startup_session_secret_exception(monkeypatch):
""" Test that we get an IOError on app_startup when we cannot
connect to options.redis_address. """
redis_addr = aurweb.config.get("options", "redis_address")
def mock_get(section: str, key: str):
if section == "fastapi" and key == "session_secret":
return None
return redis_addr
with mock.patch("aurweb.config.get", side_effect=mock_get):
with pytest.raises(Exception):
await aurweb.asgi.app_startup()
@pytest.mark.asyncio

40
test/test_redis.py Normal file
View file

@ -0,0 +1,40 @@
from unittest import mock
import pytest
import aurweb.config
from aurweb.redis import redis_connection
@pytest.fixture
def rediss():
""" Create a RedisStub. """
def mock_get(section, key):
return "none"
with mock.patch("aurweb.config.get", side_effect=mock_get):
aurweb.config.rehash()
redis = redis_connection()
aurweb.config.rehash()
yield redis
def test_redis_stub(rediss):
# We don't yet have a test key set.
assert rediss.get("test") is None
# Set the test key to abc.
rediss.set("test", "abc")
assert rediss.get("test").decode() == "abc"
# Test expire.
rediss.expire("test", 0)
assert rediss.get("test") is None
# Now, set the test key again and use delete() on it.
rediss.set("test", "abc")
assert rediss.get("test").decode() == "abc"
rediss.delete("test")
assert rediss.get("test") is None