Ensure backwards compatibility when fetching a redis client

This commit is contained in:
Erik Kalkoken 2022-07-07 07:37:21 +00:00 committed by Ariel Rin
parent 113977b19f
commit 8dec242a93
12 changed files with 94 additions and 33 deletions

View File

@ -5,7 +5,7 @@ from typing import List, Optional
from pytz import utc from pytz import utc
from redis import Redis, RedisError from redis import Redis, RedisError
from django.core.cache import cache from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -39,7 +39,7 @@ class EventSeries:
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None: def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = cache.get_master_client() if not redis else redis self._redis = get_redis_client() if not redis else redis
try: try:
if not self._redis.ping(): if not self._redis.ping():
raise RuntimeError() raise RuntimeError()

View File

@ -18,7 +18,7 @@ MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase): class TestEventSeries(TestCase):
def test_should_abort_without_redis_client(self): def test_should_abort_without_redis_client(self):
# when # when
with patch(MODULE_PATH + ".cache.get_master_client") as mock: with patch(MODULE_PATH + ".get_redis_client") as mock:
mock.return_value = None mock.return_value = None
events = EventSeries("dummy") events = EventSeries("dummy")
# then # then
@ -27,7 +27,7 @@ class TestEventSeries(TestCase):
def test_should_disable_itself_if_redis_not_available_1(self): def test_should_disable_itself_if_redis_not_available_1(self):
# when # when
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client: with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.side_effect = RedisError mock_get_master_client.return_value.ping.side_effect = RedisError
events = EventSeries("dummy") events = EventSeries("dummy")
# then # then
@ -36,7 +36,7 @@ class TestEventSeries(TestCase):
def test_should_disable_itself_if_redis_not_available_2(self): def test_should_disable_itself_if_redis_not_available_2(self):
# when # when
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client: with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.return_value = False mock_get_master_client.return_value.ping.return_value = False
events = EventSeries("dummy") events = EventSeries("dummy")
# then # then

View File

@ -8,7 +8,7 @@ from uuid import uuid1
from redis import Redis from redis import Redis
import requests import requests
from django.core.cache import caches from allianceauth.utils.cache import get_redis_client
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__ from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
@ -103,8 +103,7 @@ class DiscordClient:
self._access_token = str(access_token) self._access_token = str(access_token)
self._is_rate_limited = bool(is_rate_limited) self._is_rate_limited = bool(is_rate_limited)
if not redis: if not redis:
default_cache = caches['default'] self._redis = get_redis_client()
self._redis = default_cache.get_master_client()
if not isinstance(self._redis, Redis): if not isinstance(self._redis, Redis):
raise RuntimeError( raise RuntimeError(
'This class requires a Redis client, but none was provided ' 'This class requires a Redis client, but none was provided '

View File

@ -85,29 +85,18 @@ class TestBasicsAndHelpers(TestCase):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True) client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True)
self.assertTrue(client.is_rate_limited) self.assertTrue(client.is_rate_limited)
@patch(MODULE_PATH + '.caches') def test_use_default_redis_if_none_provided(self):
def test_use_default_redis_if_none_provided(self, mock_caches):
my_redis = MagicMock(spec=Redis)
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
client = DiscordClient(TEST_BOT_TOKEN) client = DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called) self.assertIsInstance(client._redis, Redis)
self.assertEqual(client._redis, my_redis)
@patch(MODULE_PATH + '.caches')
def test_raise_exception_if_default_cache_is_not_redis(self, mock_caches):
my_redis = MagicMock()
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
@patch(MODULE_PATH + '.get_redis_client')
def test_raise_exception_if_redis_client_not_found(self, mock_get_redis_client):
# given
mock_get_redis_client.return_value = None
# when
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
DiscordClient(TEST_BOT_TOKEN) DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called)
@requests_mock.Mocker() @requests_mock.Mocker()
class TestOtherMethods(TestCase): class TestOtherMethods(TestCase):

View File

@ -35,17 +35,17 @@ import logging
from uuid import uuid1 from uuid import uuid1
import random import random
from django.core.cache import caches
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from allianceauth.services.modules.discord.models import DiscordUser from allianceauth.services.modules.discord.models import DiscordUser
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger('allianceauth') logger = logging.getLogger('allianceauth')
MAX_RUNS = 3 MAX_RUNS = 3
def clear_cache(): def clear_cache():
default_cache = caches['default'] redis = get_redis_client()
redis = default_cache.get_master_client()
redis.flushall() redis.flushall()
logger.info('Cache flushed') logger.info('Cache flushed')

View File

@ -14,7 +14,6 @@ from requests.exceptions import HTTPError
import requests_mock import requests_mock
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.cache import caches
from django.shortcuts import reverse from django.shortcuts import reverse
from django.test import TransactionTestCase, TestCase from django.test import TransactionTestCase, TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
@ -23,6 +22,7 @@ from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.notifications.models import Notification from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.utils.cache import get_redis_client
from . import ( from . import (
TEST_GUILD_ID, TEST_GUILD_ID,
@ -87,8 +87,7 @@ remove_guild_member_request = DiscordRequest(
def clear_cache(): def clear_cache():
default_cache = caches['default'] redis = get_redis_client()
redis = default_cache.get_master_client()
redis.flushall() redis.flushall()
logger.info('Cache flushed') logger.info('Cache flushed')
@ -109,7 +108,6 @@ def reset_testdata():
class TestServiceFeatures(TransactionTestCase): class TestServiceFeatures(TransactionTestCase):
fixtures = ['disable_analytics.json'] fixtures = ['disable_analytics.json']
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()

View File

View File

@ -0,0 +1,21 @@
from redis import Redis
from django.core.cache import caches
try:
import django_redis
except ImportError:
django_redis = None
def get_redis_client() -> Redis:
"""Get the configured redis client used by Django for caching.
This function is a wrapper designed to work for both AA2 and AA3
and should always be used to ensure backwards compatibility.
"""
try:
return django_redis.get_redis_connection("default")
except AttributeError:
default_cache = caches["default"]
return default_cache.get_master_client()

View File

View File

@ -0,0 +1,39 @@
from unittest.mock import MagicMock, patch
from django.test import TestCase
from allianceauth.utils.cache import get_redis_client
MODULE_PATH = "allianceauth.utils.cache"
class RedisClientStub:
"""Substitute for a Redis client."""
pass
class TestGetRedisClient(TestCase):
def test_should_work_with_aa2_api(self):
# given
mock_django_cache = MagicMock()
mock_django_cache.get_master_client.return_value = RedisClientStub()
# when
with patch(MODULE_PATH + ".django_redis", None), patch.dict(
MODULE_PATH + ".caches", {"default": mock_django_cache}
):
client = get_redis_client()
# then
self.assertIsInstance(client, RedisClientStub)
def test_should_work_with_aa3_api(self):
# given
mock_django_redis = MagicMock()
mock_django_redis.get_redis_connection.return_value = RedisClientStub()
# when
with patch(MODULE_PATH + ".django_redis", mock_django_redis), patch.dict(
MODULE_PATH + ".caches", {"default": None}
):
client = get_redis_client()
# then
self.assertIsInstance(client, RedisClientStub)

View File

@ -11,4 +11,5 @@ To reduce redundancy and help speed up development we encourage developers to ut
eveonline eveonline
notifications notifications
testutils testutils
utils
``` ```

View File

@ -0,0 +1,14 @@
=============================
utils
=============================
Utilities and helper functions.
Location: ``allianceauth.utils``
cache
===========
.. automodule:: allianceauth.utils.cache
:members:
:undoc-members: