Repository: python-discord/bot
Issue: Review uses of `on_ready` in cogs

**Originally posted by Scragly:**

There are 12 cogs that use the event `on_ready` to perform some form of preparation for that cog.

While that's fine if the extension is loaded before the bot is started, any cases where loading occurs after this stage will result in the preparatory tasks not being done at all, resulting in missing data the cogs rely on to function. This includes cases where a cog may be reloaded during runtime, which often happens during development to refresh to code.

Instead of running when the bot dispatches the ready event, it would be best to instead have it prepare every time a cog is loaded, but have it wait for the bot's ready flag:  
```python
class Cog:
    def __init__(self, bot):
        self.bot = bot
        ...
        bot.loop.create_task(self.prepare_method())

    async def prepare_method(self):
        await self.bot.wait_until_ready()
        # do prep stuff
        ...
```

This is a job for after the Github migration.

Changed files:
bot/cogs/defcon.py, bot/cogs/reddit.py, bot/cogs/reminders.py, bot/cogs/logging.py, bot/cogs/doc.py, bot/cogs/sync/cog.py, bot/cogs/off_topic_names.py, bot/cogs/antispam.py

diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py
index fd7e4edb..1b394048 100644
--- a/bot/cogs/antispam.py
+++ b/bot/cogs/antispam.py
@@ -107,14 +107,16 @@ class AntiSpam(Cog):
         self.message_deletion_queue = dict()
         self.queue_consumption_tasks = dict()
 
+        self.bot.loop.create_task(self.alert_on_validation_error())
+
     @property
     def mod_log(self) -> ModLog:
         """Allows for easy access of the ModLog cog."""
         return self.bot.get_cog("ModLog")
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+    async def alert_on_validation_error(self) -> None:
         """Unloads the cog and alerts admins if configuration validation failed."""
+        await self.bot.wait_until_ready()
         if self.validation_errors:
             body = "**The following errors were encountered:**\\n"
             body += "\\n".join(f"- {error}" for error in self.validation_errors.values())
diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py
index ae033268..70e101ba 100644
--- a/bot/cogs/defcon.py
+++ b/bot/cogs/defcon.py
@@ -35,14 +35,16 @@ class Defcon(Cog):
         self.channel = None
         self.days = timedelta(days=0)
 
+        self.bot.loop.create_task(self.sync_settings())
+
     @property
     def mod_log(self) -> ModLog:
         """Get currently loaded ModLog cog instance."""
         return self.bot.get_cog("ModLog")
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+    async def sync_settings(self) -> None:
         """On cog load, try to synchronize DEFCON settings to the API."""
+        await self.bot.wait_until_ready()
         self.channel = await self.bot.fetch_channel(Channels.defcon)
         try:
             response = await self.bot.api_client.get('bot/bot-settings/defcon')
diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py
index 0c5a8fce..a13464bf 100644
--- a/bot/cogs/doc.py
+++ b/bot/cogs/doc.py
@@ -126,9 +126,11 @@ class Doc(commands.Cog):
         self.bot = bot
         self.inventories = {}
 
-    @commands.Cog.listener()
-    async def on_ready(self) -> None:
-        """Refresh documentation inventory."""
+        self.bot.loop.create_task(self.init_refresh_inventory())
+
+    async def init_refresh_inventory(self) -> None:
+        """Refresh documentation inventory on cog initialization."""
+        await self.bot.wait_until_ready()
         await self.refresh_inventory()
 
     async def update_single(
diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py
index 8e47bcc3..c92b619f 100644
--- a/bot/cogs/logging.py
+++ b/bot/cogs/logging.py
@@ -15,9 +15,11 @@ class Logging(Cog):
     def __init__(self, bot: Bot):
         self.bot = bot
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+        self.bot.loop.create_task(self.startup_greeting())
+
+    async def startup_greeting(self) -> None:
         """Announce our presence to the configured devlog channel."""
+        await self.bot.wait_until_ready()
         log.info("Bot connected!")
 
         embed = Embed(description="Connected!")
diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py
index 16717d52..2977e4eb 100644
--- a/bot/cogs/off_topic_names.py
+++ b/bot/cogs/off_topic_names.py
@@ -75,14 +75,16 @@ class OffTopicNames(Cog):
         self.bot = bot
         self.updater_task = None
 
+        self.bot.loop.create_task(self.init_offtopic_updater())
+
     def cog_unload(self) -> None:
         """Cancel any running updater tasks on cog unload."""
         if self.updater_task is not None:
             self.updater_task.cancel()
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+    async def init_offtopic_updater(self) -> None:
         """Start off-topic channel updating event loop if it hasn't already started."""
+        await self.bot.wait_until_ready()
         if self.updater_task is None:
             coro = update_names(self.bot)
             self.updater_task = self.bot.loop.create_task(coro)
diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py
index 6880aab8..0f575cec 100644
--- a/bot/cogs/reddit.py
+++ b/bot/cogs/reddit.py
@@ -34,6 +34,8 @@ class Reddit(Cog):
         self.new_posts_task = None
         self.top_weekly_posts_task = None
 
+        self.bot.loop.create_task(self.init_reddit_polling())
+
     async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]:
         """A helper method to fetch a certain amount of Reddit posts at a given route."""
         # Reddit's JSON responses only provide 25 posts at most.
@@ -262,9 +264,9 @@ class Reddit(Cog):
             max_lines=15
         )
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+    async def init_reddit_polling(self) -> None:
         """Initiate reddit post event loop."""
+        await self.bot.wait_until_ready()
         self.reddit_channel = await self.bot.fetch_channel(Channels.reddit)
 
         if self.reddit_channel is not None:
diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py
index 6e91d2c0..b5462230 100644
--- a/bot/cogs/reminders.py
+++ b/bot/cogs/reminders.py
@@ -30,9 +30,11 @@ class Reminders(Scheduler, Cog):
         self.bot = bot
         super().__init__()
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+        self.bot.loop.create_task(self.reschedule_reminders())
+
+    async def reschedule_reminders(self) -> None:
         """Get all current reminders from the API and reschedule them."""
+        await self.bot.wait_until_ready()
         response = await self.bot.api_client.get(
             'bot/reminders',
             params={'active': 'true'}
diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py
index b75fb26c..aaa581f9 100644
--- a/bot/cogs/sync/cog.py
+++ b/bot/cogs/sync/cog.py
@@ -29,9 +29,11 @@ class Sync(Cog):
     def __init__(self, bot: Bot) -> None:
         self.bot = bot
 
-    @Cog.listener()
-    async def on_ready(self) -> None:
+        self.bot.loop.create_task(self.sync_guild())
+
+    async def sync_guild(self) -> None:
         """Syncs the roles/users of the guild with the database."""
+        await self.bot.wait_until_ready()
         guild = self.bot.get_guild(self.SYNC_SERVER_ID)
         if guild is not None:
             for syncer in self.ON_READY_SYNCERS: