aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar MarkKoz <[email protected]>2020-02-24 18:26:09 -0800
committerGravatar MarkKoz <[email protected]>2020-03-22 15:54:37 -0700
commit96ed02a565feabcc9415ae8909792323b08f9b08 (patch)
treee6d3dd3d2cea5a31ad06fd9be755e9ee859b0833
parentHelpChannels: use a lock to prevent a channel from being processed twice (diff)
HelpChannels: add logging
-rw-r--r--bot/cogs/help_channels.py89
1 files changed, 83 insertions, 6 deletions
diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py
index fd5632d09..82dce4ee7 100644
--- a/bot/cogs/help_channels.py
+++ b/bot/cogs/help_channels.py
@@ -77,8 +77,10 @@ class HelpChannels(Scheduler, commands.Cog):
def cog_unload(self) -> None:
"""Cancel the init task and scheduled tasks when the cog unloads."""
+ log.trace("Cog unload: cancelling the cog_init task")
self.init_task.cancel()
+ log.trace("Cog unload: cancelling the scheduled tasks")
for task in self.scheduled_tasks.values():
task.cancel()
@@ -88,9 +90,12 @@ class HelpChannels(Scheduler, commands.Cog):
The channels are added to the queue in a random order.
"""
+ log.trace("Creating the channel queue.")
+
channels = list(self.get_category_channels(self.dormant_category))
random.shuffle(channels)
+ log.trace("Populating the channel queue with channels.")
queue = asyncio.Queue()
for channel in channels:
queue.put_nowait(channel)
@@ -105,26 +110,36 @@ class HelpChannels(Scheduler, commands.Cog):
Return None if no more channel names are available.
"""
+ log.trace("Getting a name for a new dormant channel.")
name = constants.HelpChannels.name_prefix
try:
name += self.name_queue.popleft()
except IndexError:
+ log.debug("No more names available for new dormant channels.")
return None
+ log.debug(f"Creating a new dormant channel named {name}.")
return await self.dormant_category.create_text_channel(name)
def create_name_queue(self) -> deque:
"""Return a queue of element names to use for creating new channels."""
+ log.trace("Creating the chemical element name queue.")
+
used_names = self.get_used_names()
+
+ log.trace("Determining the available names.")
available_names = (name for name in ELEMENTS if name not in used_names)
+ log.trace("Populating the name queue with names.")
return deque(available_names)
@commands.command(name="dormant")
@with_role(*constants.HelpChannels.cmd_whitelist)
async def dormant_command(self, ctx: commands.Context) -> None:
"""Make the current in-use help channel dormant."""
+ log.trace("dormant command invoked; checking if the channel is in-use.")
+
in_use = self.get_category_channels(self.in_use_category)
if ctx.channel in in_use:
await self.move_to_dormant(ctx.channel)
@@ -137,13 +152,16 @@ class HelpChannels(Scheduler, commands.Cog):
If no channel is available, wait indefinitely until one becomes available.
"""
+ log.trace("Getting an available channel candidate.")
+
try:
channel = self.channel_queue.get_nowait()
except asyncio.QueueEmpty:
+ log.info("No candidate channels in the queue; creating a new channel.")
channel = await self.create_dormant()
if not channel:
- # Wait for a channel to become available.
+ log.info("Couldn't create a candidate channel; waiting to get one from the queue.")
channel = await self.channel_queue.get()
return channel
@@ -151,6 +169,8 @@ class HelpChannels(Scheduler, commands.Cog):
@staticmethod
def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]:
"""Yield the text channels of the `category` in an unsorted manner."""
+ log.trace(f"Getting text channels in the category '{category.name}' ({category.id}).")
+
# This is faster than using category.channels because the latter sorts them.
for channel in category.guild.channels:
if channel.category_id == category.id and isinstance(channel, discord.TextChannel):
@@ -158,6 +178,8 @@ class HelpChannels(Scheduler, commands.Cog):
def get_used_names(self) -> t.Set[str]:
"""Return channels names which are already being used."""
+ log.trace("Getting channel names which are already being used.")
+
start_index = len(constants.HelpChannels.name_prefix)
names = set()
@@ -166,6 +188,7 @@ class HelpChannels(Scheduler, commands.Cog):
name = channel.name[start_index:]
names.add(name)
+ log.trace(f"Got {len(names)} used names: {names}")
return names
@staticmethod
@@ -175,23 +198,35 @@ class HelpChannels(Scheduler, commands.Cog):
Return None if the channel has no messages.
"""
+ log.trace(f"Getting the idle time for #{channel.name} ({channel.id}).")
+
try:
msg = await channel.history(limit=1).next() # noqa: B305
except discord.NoMoreItems:
+ log.debug(f"No idle time available; #{channel.name} ({channel.id}) has no messages.")
return None
- return (datetime.utcnow() - msg.created_at).seconds
+ idle_time = (datetime.utcnow() - msg.created_at).seconds
+
+ log.trace(f"#{channel.name} ({channel.id}) has been idle for {idle_time} seconds.")
+ return idle_time
async def init_available(self) -> None:
"""Initialise the Available category with channels."""
+ log.trace("Initialising the Available category with channels.")
+
channels = list(self.get_category_channels(self.available_category))
missing = constants.HelpChannels.max_available - len(channels)
+ log.trace(f"Moving {missing} missing channels to the Available category.")
+
for _ in range(missing):
await self.move_to_available()
async def init_categories(self) -> None:
"""Get the help category objects. Remove the cog if retrieval fails."""
+ log.trace("Getting the CategoryChannel objects for the help categories.")
+
try:
self.available_category = await self.try_get_channel(
constants.Categories.help_available
@@ -204,8 +239,10 @@ class HelpChannels(Scheduler, commands.Cog):
async def init_cog(self) -> None:
"""Initialise the help channel system."""
+ log.trace("Waiting for the guild to be available before initialisation.")
await self.bot.wait_until_guild_available()
+ log.trace("Initialising the cog.")
await self.init_categories()
self.channel_queue = self.create_channel_queue()
@@ -213,9 +250,11 @@ class HelpChannels(Scheduler, commands.Cog):
await self.init_available()
+ log.trace("Moving or rescheduling in-use channels.")
for channel in self.get_category_channels(self.in_use_category):
await self.move_idle_channel(channel)
+ log.info("Cog is ready!")
self.ready.set()
async def move_idle_channel(self, channel: discord.TextChannel) -> None:
@@ -224,10 +263,17 @@ class HelpChannels(Scheduler, commands.Cog):
If a task to make the channel dormant already exists, it will first be cancelled.
"""
+ log.trace(f"Handling in-use channel #{channel.name} ({channel.id}).")
+
idle_seconds = constants.HelpChannels.idle_minutes * 60
time_elapsed = await self.get_idle_time(channel)
if time_elapsed is None or time_elapsed > idle_seconds:
+ log.info(
+ f"#{channel.name} ({channel.id}) is idle longer than {idle_seconds} seconds "
+ f"and will be made dormant."
+ )
+
await self.move_to_dormant(channel)
else:
# Cancel the existing task, if any.
@@ -235,15 +281,28 @@ class HelpChannels(Scheduler, commands.Cog):
self.cancel_task(channel.id)
data = ChannelTimeout(channel, idle_seconds - time_elapsed)
+
+ log.info(
+ f"#{channel.name} ({channel.id}) is still active; "
+ f"scheduling it to be moved after {data.timeout} seconds."
+ )
+
self.schedule_task(self.bot.loop, channel.id, data)
async def move_to_available(self) -> None:
"""Make a channel available."""
+ log.trace("Making a channel available.")
+
channel = await self.get_available_candidate()
embed = discord.Embed(description=AVAILABLE_MSG)
+ log.info(f"Making #{channel.name} ({channel.id}) available.")
+
# TODO: edit or delete the dormant message
+ log.trace(f"Sending available message for #{channel.name} ({channel.id}).")
await channel.send(embed=embed)
+
+ log.trace(f"Moving #{channel.name} ({channel.id}) to the Available category.")
await channel.edit(
category=self.available_category,
sync_permissions=True,
@@ -252,40 +311,53 @@ class HelpChannels(Scheduler, commands.Cog):
async def move_to_dormant(self, channel: discord.TextChannel) -> None:
"""Make the `channel` dormant."""
+ log.info(f"Making #{channel.name} ({channel.id}) dormant.")
+
+ log.trace(f"Moving #{channel.name} ({channel.id}) to the Dormant category.")
await channel.edit(
category=self.dormant_category,
sync_permissions=True,
topic=DORMANT_TOPIC,
)
+ log.trace(f"Sending dormant message for #{channel.name} ({channel.id}).")
embed = discord.Embed(description=DORMANT_MSG)
await channel.send(embed=embed)
async def move_to_in_use(self, channel: discord.TextChannel) -> None:
"""Make a channel in-use and schedule it to be made dormant."""
- # Move the channel to the In Use category.
+ log.info(f"Making #{channel.name} ({channel.id}) in-use.")
+
+ log.trace(f"Moving #{channel.name} ({channel.id}) to the In Use category.")
await channel.edit(
category=self.in_use_category,
sync_permissions=True,
topic=IN_USE_TOPIC,
)
- # Schedule the channel to be moved to the Dormant category.
- data = ChannelTimeout(channel, constants.HelpChannels.idle_minutes * 60)
+ timeout = constants.HelpChannels.idle_minutes * 60
+
+ log.trace(f"Scheduling #{channel.name} ({channel.id}) to become dormant in {timeout} sec.")
+ data = ChannelTimeout(channel, timeout)
self.schedule_task(self.bot.loop, channel.id, data)
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
"""Move an available channel to the In Use category and replace it with a dormant one."""
+ log.trace("Waiting for the cog to be ready before processing messages.")
await self.ready.wait()
- # Use a lock to prevent a channel from being processed twice.
+ log.trace("Acquiring lock to prevent a channel from being processed twice...")
with self.on_message_lock.acquire():
+ log.trace("on_message lock acquired.")
+ log.trace("Checking if the message was sent in an available channel.")
+
available_channels = self.get_category_channels(self.available_category)
if message.channel not in available_channels:
return # Ignore messages outside the Available category.
await self.move_to_in_use(message.channel)
+ log.trace("Releasing on_message lock.")
# Move a dormant channel to the Available category to fill in the gap.
# This is done last and outside the lock because it may wait indefinitely for a channel to
@@ -294,14 +366,19 @@ class HelpChannels(Scheduler, commands.Cog):
async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel:
"""Attempt to get or fetch a channel and return it."""
+ log.trace(f"Getting the channel {channel_id}.")
+
channel = self.bot.get_channel(channel_id)
if not channel:
+ log.debug(f"Channel {channel_id} is not in cache; fetching from API.")
channel = await self.bot.fetch_channel(channel_id)
+ log.trace(f"Channel #{channel.name} ({channel_id}) retrieved.")
return channel
async def _scheduled_task(self, data: ChannelTimeout) -> None:
"""Make a channel dormant after specified timeout or reschedule if it's still active."""
+ log.trace(f"Waiting {data.timeout} before making #{data.channel.name} dormant.")
await asyncio.sleep(data.timeout)
# Use asyncio.shield to prevent move_idle_channel from cancelling itself.