Repository: python-discord/bot
Issue: RedisCache: _redis alias is assigned too soon

The `RedisCache` wrapper currently suffers from a bug which can cause an `AttributeError` if a value is looked up before the bot's `redis_session` attribute is set.

### Problem

Let's imagine that a cog using the `RedisCache` schedules a task which will perform a lookup. There is no guarantee that the cache is ready, and so the cache API methods always wait for the ready event to be set.

However, as soon as the cache attribute is accessed, the `__get__` method assigns the `self._redis` attribute:
https://github.com/python-discord/bot/blob/239fa5f43ab79435151657671ebcf21eac706fc6/bot/utils/redis_cache.py#L226-L230

This can happen before the cache is ready, and so there is no guarantee that `self.bot.redis_session` is not `None`.

It defaults to `None` here:
https://github.com/python-discord/bot/blob/239fa5f43ab79435151657671ebcf21eac706fc6/bot/bot.py#L34

And only here will the attribute be set:
https://github.com/python-discord/bot/blob/239fa5f43ab79435151657671ebcf21eac706fc6/bot/bot.py#L76-L83

At this point, the cache will report ready status, but it's `self._redis` attribute has already been assigned the `None`.

Since the cache is now ready, the lookup will proceed, and fail with the following error:
```python
2020-08-05 12:31:15 | bot.cogs.verification | ERROR | Failed to get message
Traceback (most recent call last):
  File "/bot/bot/cogs/verification.py", line 325, in _before_first_ping
    last_reminder: t.Optional[int] = await self.reminder_cache.get("last_reminder")
  File "/bot/bot/utils/redis_cache.py", line 263, in get
    value = await self._redis.hget(self._namespace, key)
AttributeError: 'NoneType' object has no attribute 'hget'
```

### Solution

The solution seems to be fairly trivial: the `self._redis` alias simply cannot be safely set before the cache is ready, and we therefore have to lookup the `redis_session` instance via the `bot` attribute.

For example, instead of:
```python
await self._redis.redismethod(...)
```
we should have:
```python
await self.bot.redis_session.redismethod(...)
```
This delays the `redis_session` resolution until after the cache is ready. The `self._redis` alias should probably be removed.

Changed files:
bot/utils/redis_cache.py

diff --git a/bot/utils/redis_cache.py b/bot/utils/redis_cache.py
index 58cfe1df..52b689b4 100644
--- a/bot/utils/redis_cache.py
+++ b/bot/utils/redis_cache.py
@@ -226,7 +226,6 @@ class RedisCache:
         for attribute in vars(instance).values():
             if isinstance(attribute, Bot):
                 self.bot = attribute
-                self._redis = self.bot.redis_session
                 return self
         else:
             error_message = (
@@ -251,7 +250,7 @@ class RedisCache:
         value = self._value_to_typestring(value)
 
         log.trace(f"Setting {key} to {value}.")
-        await self._redis.hset(self._namespace, key, value)
+        await self.bot.redis_session.hset(self._namespace, key, value)
 
     async def get(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> Optional[RedisValueType]:
         """Get an item from the Redis cache."""
@@ -259,7 +258,7 @@ class RedisCache:
         key = self._key_to_typestring(key)
 
         log.trace(f"Attempting to retrieve {key}.")
-        value = await self._redis.hget(self._namespace, key)
+        value = await self.bot.redis_session.hget(self._namespace, key)
 
         if value is None:
             log.trace(f"Value not found, returning default value {default}")
@@ -281,7 +280,7 @@ class RedisCache:
         key = self._key_to_typestring(key)
 
         log.trace(f"Attempting to delete {key}.")
-        return await self._redis.hdel(self._namespace, key)
+        return await self.bot.redis_session.hdel(self._namespace, key)
 
     async def contains(self, key: RedisKeyType) -> bool:
         """
@@ -291,7 +290,7 @@ class RedisCache:
         """
         await self._validate_cache()
         key = self._key_to_typestring(key)
-        exists = await self._redis.hexists(self._namespace, key)
+        exists = await self.bot.redis_session.hexists(self._namespace, key)
 
         log.trace(f"Testing if {key} exists in the RedisCache - Result is {exists}")
         return exists
@@ -314,7 +313,7 @@ class RedisCache:
         """
         await self._validate_cache()
         items = self._dict_from_typestring(
-            await self._redis.hgetall(self._namespace)
+            await self.bot.redis_session.hgetall(self._namespace)
         ).items()
 
         log.trace(f"Retrieving all key/value pairs from cache, total of {len(items)} items.")
@@ -323,7 +322,7 @@ class RedisCache:
     async def length(self) -> int:
         """Return the number of items in the Redis cache."""
         await self._validate_cache()
-        number_of_items = await self._redis.hlen(self._namespace)
+        number_of_items = await self.bot.redis_session.hlen(self._namespace)
         log.trace(f"Returning length. Result is {number_of_items}.")
         return number_of_items
 
@@ -335,7 +334,7 @@ class RedisCache:
         """Deletes the entire hash from the Redis cache."""
         await self._validate_cache()
         log.trace("Clearing the cache of all key/value pairs.")
-        await self._redis.delete(self._namespace)
+        await self.bot.redis_session.delete(self._namespace)
 
     async def pop(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> RedisValueType:
         """Get the item, remove it from the cache, and provide a default if not found."""
@@ -364,7 +363,7 @@ class RedisCache:
         """
         await self._validate_cache()
         log.trace(f"Updating the cache with the following items:\\n{items}")
-        await self._redis.hmset_dict(self._namespace, self._dict_to_typestring(items))
+        await self.bot.redis_session.hmset_dict(self._namespace, self._dict_to_typestring(items))
 
     async def increment(self, key: RedisKeyType, amount: Optional[int, float] = 1) -> None:
         """