From 82a600747e4142f1c7073c2be57c520be6455a87 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 1 Dec 2020 22:09:57 +0100 Subject: Add function that allows tasks to sleep until AoC Our Advent of Code background tasks were written in such a way that they relied on the extension being loaded in December and only in December. However, since the deseasonification, the extension is loaded prior to December, with some commands being locked to that month with a check instead. This meant that our background tasks immediately cancelled themselves, as they observed themselves to be running outside of the boundaries of the event. As there was no mechanism for starting them back up again, these tasks would only start running again after redeployment of Sir Lancebot. To solve this issue, I've added a helper function that allows tasks to wait until a x hours before the event starts. This allows them to wake up in time to prepare for the release of the first puzzle. --- bot/exts/christmas/advent_of_code/_helpers.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index f4a20955..145fa30a 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -1,3 +1,4 @@ +import asyncio import collections import datetime import json @@ -362,3 +363,24 @@ def time_left_to_aoc_midnight() -> Tuple[datetime.datetime, datetime.timedelta]: # Calculate the timedelta between the current time and midnight return tomorrow, tomorrow - datetime.datetime.now(EST) + + +async def wait_for_advent_of_code(*, hours_before: int = 1) -> None: + """ + Wait for the Advent of Code event to start. + + This function returns `hours_before` (default: 1) before the Advent of Code + actually starts. This allows functions to schedule and execute code that + needs to run before the event starts. + """ + start = datetime.datetime(AdventOfCode.year, 12, 1, 0, 0, 0, tzinfo=EST) + target = start - datetime.timedelta(hours=hours_before) + now = datetime.datetime.now(EST) + + # If we've already reached or passed to target, we + # simply return immediately. + if now >= target: + return + + delta = target - now + await asyncio.sleep(delta.total_seconds()) -- cgit v1.2.3 From cb418139b19aae38d384746ee0d7e149094c05b1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 1 Dec 2020 22:22:29 +0100 Subject: Let status update task sleep until the AoC starts I've refactored the rich presence countdown task by making it hibernate until 2 hours before the next Advent of Code starts if the task starts up before the event has started. This ensures that the task will run when the event starts and allows it to countdown to the first challenge. After the event for the configured Advent of Code year has finished, the task will terminate. This also means that the task will terminate immediately in the year following the currently configured Advent of Code; it will only start hibernating again once we configure the bot for the next event. No unnecessary, year-long hibernation. --- bot/exts/christmas/advent_of_code/_cog.py | 32 +-------------- bot/exts/christmas/advent_of_code/_helpers.py | 59 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 31 deletions(-) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 2a1a776b..57043454 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -1,7 +1,6 @@ import asyncio import json import logging -import math from datetime import datetime, timedelta from pathlib import Path @@ -19,38 +18,9 @@ log = logging.getLogger(__name__) AOC_REQUEST_HEADER = {"user-agent": "PythonDiscord AoC Event Bot"} -COUNTDOWN_STEP = 60 * 5 - AOC_WHITELIST = WHITELISTED_CHANNELS + (Channels.advent_of_code,) -async def countdown_status(bot: commands.Bot) -> None: - """Set the playing status of the bot to the minutes & hours left until the next day's challenge.""" - while _helpers.is_in_advent(): - _, time_left = _helpers.time_left_to_aoc_midnight() - - aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP - hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60 - - if aligned_seconds == 0: - playing = "right now!" - elif aligned_seconds == COUNTDOWN_STEP: - playing = f"in less than {minutes} minutes" - elif hours == 0: - playing = f"in {minutes} minutes" - elif hours == 23: - playing = f"since {60 - minutes} minutes ago" - else: - playing = f"in {hours} hours and {minutes} minutes" - - # Status will look like "Playing in 5 hours and 30 minutes" - await bot.change_presence(activity=discord.Game(playing)) - - # Sleep until next aligned time or a full step if already aligned - delay = time_left.seconds % COUNTDOWN_STEP or COUNTDOWN_STEP - await asyncio.sleep(delay) - - async def day_countdown(bot: commands.Bot) -> None: """ Calculate the number of seconds left until the next day of Advent. @@ -124,7 +94,7 @@ class AdventOfCode(commands.Cog): countdown_coro = day_countdown(self.bot) self.countdown_task = self.bot.loop.create_task(countdown_coro) - status_coro = countdown_status(self.bot) + status_coro = _helpers.countdown_status(self.bot) self.status_task = self.bot.loop.create_task(status_coro) @commands.group(name="adventofcode", aliases=("aoc",)) diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index 145fa30a..57ad001a 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -3,6 +3,7 @@ import collections import datetime import json import logging +import math import operator import typing from typing import Tuple @@ -11,6 +12,7 @@ import aiohttp import discord import pytz +from bot.bot import Bot from bot.constants import AdventOfCode, Colours from bot.exts.christmas.advent_of_code import _caches @@ -48,6 +50,9 @@ AOC_EMBED_THUMBNAIL = ( # Create an easy constant for the EST timezone EST = pytz.timezone("EST") +# Step size for the challenge countdown status +COUNTDOWN_STEP = 60 * 5 + # Create namedtuple that combines a participant's name and their completion # time for a specific star. We're going to use this later to order the results # for each star to compute the rank score. @@ -384,3 +389,57 @@ async def wait_for_advent_of_code(*, hours_before: int = 1) -> None: delta = target - now await asyncio.sleep(delta.total_seconds()) + + +async def countdown_status(bot: Bot) -> None: + """ + Add the time until the next challenge is published to the bot's status. + + This function sleeps until 2 hours before the event and exists one hour + after the last challenge has been published. It will not start up again + automatically for next year's event, as it will wait for the environment + variable AOC_YEAR to be updated. + + This ensures that the task will only start sleeping again once the next + event approaches and we're making preparations for that event. + """ + log.debug("Initializing status countdown task.") + # We wait until 2 hours before the event starts. Then we + # set our first countdown status. + await wait_for_advent_of_code(hours_before=2) + + # Log that we're going to start with the countdown status. + log.info("The Advent of Code has started or will start soon, starting countdown status.") + + # Calculate when the task needs to stop running. To prevent the task from + # sleeping for the entire year, it will only wait in the currently + # configured year. This means that the task will only start hibernating once + # we start preparing the next event by changing environment variables. + last_challenge = datetime.datetime(AdventOfCode.year, 12, 25, 0, 0, 0, tzinfo=EST) + end = last_challenge + datetime.timedelta(hours=1) + + while datetime.datetime.now(EST) < end: + _, time_left = time_left_to_aoc_midnight() + + aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP + hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60 + + if aligned_seconds == 0: + playing = "right now!" + elif aligned_seconds == COUNTDOWN_STEP: + playing = f"in less than {minutes} minutes" + elif hours == 0: + playing = f"in {minutes} minutes" + elif hours == 23: + playing = f"since {60 - minutes} minutes ago" + else: + playing = f"in {hours} hours and {minutes} minutes" + + log.trace(f"Changing presence to {playing!r}") + # Status will look like "Playing in 5 hours and 30 minutes" + await bot.change_presence(activity=discord.Game(playing)) + + # Sleep until next aligned time or a full step if already aligned + delay = time_left.seconds % COUNTDOWN_STEP or COUNTDOWN_STEP + log.trace(f"The countdown status task will sleep for {delay} seconds.") + await asyncio.sleep(delay) -- cgit v1.2.3 From 575fedc0b4b22c712c541f7b49311ce2d02e9880 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 1 Dec 2020 22:28:16 +0100 Subject: Ensure status countdown waits for bot start-up Trying to change the rich presence status of the bot too early in the bot's start-up sequence will cause the task to fail. To make it wait, I've added a `bot.wait_until_guild_available` point before the main loop of the task starts. This seems to solve the issue reliably, despite the `guild_available` event not being directly related to when the bot's connection with the API is ready to accept presence updates. However, the `ready` event is know to fire too early, according to the discord.py community. --- bot/exts/christmas/advent_of_code/_helpers.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index 57ad001a..9ba4d9be 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -411,6 +411,10 @@ async def countdown_status(bot: Bot) -> None: # Log that we're going to start with the countdown status. log.info("The Advent of Code has started or will start soon, starting countdown status.") + # Trying to change status too early in the bot's startup sequence will fail + # the task. Waiting until this event seems to work well. + await bot.wait_until_guild_available() + # Calculate when the task needs to stop running. To prevent the task from # sleeping for the entire year, it will only wait in the currently # configured year. This means that the task will only start hibernating once -- cgit v1.2.3 From 3e5cff49f40158acb6417d948c2155daed2e4c29 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 1 Dec 2020 23:04:30 +0100 Subject: Let puzzle notification sleep until AoC starts Instead of cancelling the task when it starts up outside of the boundaries of the Advent of Code, the task will now hibernate until just before the event starts if starts up before December. This allows it to actually announce the first puzzle. After the announcement for the last day of the current event is made, it will terminate itself. It will only start hibernating again when we've updated the environment variables for next year's event, ensuring that it does not run unnecessarily. To prevent issues with the guild cache not being available, I've added our new `wait_until_guild_available` waiting function. I've also moved the calls that get teh role/channel to before the loop, as there's no need to get them each time the loop goes around. I've also changed the way we calculate the time we need to sleep, as the old way used truncated seconds, meaning that we would always wake up relatively early. Instead, I'm now using fractional seconds, which means we can reduce the safety padding to a fraction of second. More accurate announcement timing! --- bot/exts/christmas/advent_of_code/_cog.py | 58 +------------------- bot/exts/christmas/advent_of_code/_helpers.py | 77 ++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 58 deletions(-) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 57043454..902391b5 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -1,4 +1,3 @@ -import asyncio import json import logging from datetime import datetime, timedelta @@ -21,61 +20,6 @@ AOC_REQUEST_HEADER = {"user-agent": "PythonDiscord AoC Event Bot"} AOC_WHITELIST = WHITELISTED_CHANNELS + (Channels.advent_of_code,) -async def day_countdown(bot: commands.Bot) -> None: - """ - Calculate the number of seconds left until the next day of Advent. - - Once we have calculated this we should then sleep that number and when the time is reached, ping - the Advent of Code role notifying them that the new challenge is ready. - """ - while _helpers.is_in_advent(): - tomorrow, time_left = _helpers.time_left_to_aoc_midnight() - - # Prevent bot from being slightly too early in trying to announce today's puzzle - await asyncio.sleep(time_left.seconds + 1) - - channel = bot.get_channel(Channels.advent_of_code) - - if not channel: - log.error("Could not find the AoC channel to send notification in") - break - - aoc_role = channel.guild.get_role(AocConfig.role_id) - if not aoc_role: - log.error("Could not find the AoC role to announce the daily puzzle") - break - - puzzle_url = f"https://adventofcode.com/{AocConfig.year}/day/{tomorrow.day}" - - # Check if the puzzle is already available to prevent our members from spamming - # the puzzle page before it's available by making a small HEAD request. - for retry in range(1, 5): - log.debug(f"Checking if the puzzle is already available (attempt {retry}/4)") - async with bot.http_session.head(puzzle_url, raise_for_status=False) as resp: - if resp.status == 200: - log.debug("Puzzle is available; let's send an announcement message.") - break - log.debug(f"The puzzle is not yet available (status={resp.status})") - await asyncio.sleep(10) - else: - log.error("The puzzle does does not appear to be available at this time, canceling announcement") - break - - await channel.send( - f"{aoc_role.mention} Good morning! Day {tomorrow.day} is ready to be attempted. " - f"View it online now at {puzzle_url}. Good luck!", - allowed_mentions=discord.AllowedMentions( - everyone=False, - users=False, - roles=[discord.Object(AocConfig.role_id)], - ) - ) - - # Wait a couple minutes so that if our sleep didn't sleep enough - # time we don't end up announcing twice. - await asyncio.sleep(120) - - class AdventOfCode(commands.Cog): """Advent of Code festivities! Ho Ho Ho!""" @@ -91,7 +35,7 @@ class AdventOfCode(commands.Cog): self.countdown_task = None self.status_task = None - countdown_coro = day_countdown(self.bot) + countdown_coro = _helpers.day_countdown(self.bot) self.countdown_task = self.bot.loop.create_task(countdown_coro) status_coro = _helpers.countdown_status(self.bot) diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index 9ba4d9be..1c4a01ed 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -13,7 +13,7 @@ import discord import pytz from bot.bot import Bot -from bot.constants import AdventOfCode, Colours +from bot.constants import AdventOfCode, Channels, Colours from bot.exts.christmas.advent_of_code import _caches log = logging.getLogger(__name__) @@ -447,3 +447,78 @@ async def countdown_status(bot: Bot) -> None: delay = time_left.seconds % COUNTDOWN_STEP or COUNTDOWN_STEP log.trace(f"The countdown status task will sleep for {delay} seconds.") await asyncio.sleep(delay) + + +async def day_countdown(bot: Bot) -> None: + """ + Calculate the number of seconds left until the next day of Advent. + + Once we have calculated this we should then sleep that number and when the time is reached, ping + the Advent of Code role notifying them that the new challenge is ready. + """ + # We wake up one hour before the event starts to prepare the announcement + # of the release of the first puzzle. + await wait_for_advent_of_code(hours_before=1) + + log.info("The Advent of Code has started or will start soon, waking up notification task.") + + # Ensure that the guild cache is loaded so we can get the Advent of Code + # channel and role. + await bot.wait_until_guild_available() + aoc_channel = bot.get_channel(Channels.advent_of_code) + aoc_role = aoc_channel.guild.get_role(AdventOfCode.role_id) + + if not aoc_channel: + log.error("Could not find the AoC channel to send notification in") + return + + if not aoc_role: + log.error("Could not find the AoC role to announce the daily puzzle") + return + + # The last event day is 25 December, so we only have to schedule + # a reminder if the current day is before 25 December. + end = datetime.datetime(AdventOfCode.year, 12, 25, tzinfo=EST) + while datetime.datetime.now(EST) < end: + log.trace("Started puzzle notification loop.") + tomorrow, time_left = time_left_to_aoc_midnight() + + # Use fractional `total_seconds` to wake up very close to our target, with + # padding of 0.1 seconds to ensure that we actually pass midnight. + sleep_seconds = time_left.total_seconds() + 0.1 + log.trace(f"The puzzle notification task will sleep for {sleep_seconds} seconds") + await asyncio.sleep(sleep_seconds) + + puzzle_url = f"https://adventofcode.com/{AdventOfCode.year}/day/{tomorrow.day}" + + # Check if the puzzle is already available to prevent our members from spamming + # the puzzle page before it's available by making a small HEAD request. + for retry in range(1, 5): + log.debug(f"Checking if the puzzle is already available (attempt {retry}/4)") + async with bot.http_session.head(puzzle_url, raise_for_status=False) as resp: + if resp.status == 200: + log.debug("Puzzle is available; let's send an announcement message.") + break + log.debug(f"The puzzle is not yet available (status={resp.status})") + await asyncio.sleep(10) + else: + log.error( + "The puzzle does does not appear to be available " + "at this time, canceling announcement" + ) + break + + await aoc_channel.send( + f"{aoc_role.mention} Good morning! Day {tomorrow.day} is ready to be attempted. " + f"View it online now at {puzzle_url}. Good luck!", + allowed_mentions=discord.AllowedMentions( + everyone=False, + users=False, + roles=[aoc_role], + ) + ) + + # Ensure that we don't send duplicate announcements by sleeping to well + # over midnight. This means we're certain to calculate the time to the + # next midnight at the top of the loop. + await asyncio.sleep(120) -- cgit v1.2.3 From d9e98c32fed67e1c01cd496341ef196bba88ca32 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 1 Dec 2020 23:12:26 +0100 Subject: Clarify time_left_until_est_midnight function The helper function calculates the time left until the next midnight in the EST timezone, not necessarily the next midnight during and Advent of Code event. To prevent confusion, I've clarified its function by changing the name of the function and its docstring. --- bot/exts/christmas/advent_of_code/_cog.py | 2 +- bot/exts/christmas/advent_of_code/_helpers.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 902391b5..2f60e512 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -108,7 +108,7 @@ class AdventOfCode(commands.Cog): f"The next event will start in {delta_str}.") return - tomorrow, time_left = _helpers.time_left_to_aoc_midnight() + tomorrow, time_left = _helpers.time_left_to_est_midnight() hours, minutes = time_left.seconds // 3600, time_left.seconds // 60 % 60 diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index 1c4a01ed..c75f47fa 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -353,8 +353,8 @@ def is_in_advent() -> bool: return datetime.datetime.now(EST).day in range(1, 25) and datetime.datetime.now(EST).month == 12 -def time_left_to_aoc_midnight() -> Tuple[datetime.datetime, datetime.timedelta]: - """Calculates the amount of time left until midnight in UTC-5 (Advent of Code maintainer timezone).""" +def time_left_to_est_midnight() -> Tuple[datetime.datetime, datetime.timedelta]: + """Calculate the amount of time left until midnight EST/UTC-5.""" # Change all time properties back to 00:00 todays_midnight = datetime.datetime.now(EST).replace( microsecond=0, @@ -423,7 +423,7 @@ async def countdown_status(bot: Bot) -> None: end = last_challenge + datetime.timedelta(hours=1) while datetime.datetime.now(EST) < end: - _, time_left = time_left_to_aoc_midnight() + _, time_left = time_left_to_est_midnight() aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60 @@ -481,7 +481,7 @@ async def day_countdown(bot: Bot) -> None: end = datetime.datetime(AdventOfCode.year, 12, 25, tzinfo=EST) while datetime.datetime.now(EST) < end: log.trace("Started puzzle notification loop.") - tomorrow, time_left = time_left_to_aoc_midnight() + tomorrow, time_left = time_left_to_est_midnight() # Use fractional `total_seconds` to wake up very close to our target, with # padding of 0.1 seconds to ensure that we actually pass midnight. -- cgit v1.2.3 From d4c8c0f7184e5d494136cc2b7fc670e8ab7a8f93 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 2 Dec 2020 00:22:55 +0100 Subject: Update docstrings and fix grammar in comments I've updated some docstrings to include more information about the inner workings of some of the functions. In addition, I've also slightly reformulated some block comments to improve their grammar. Kaizen change: There was a redundant list comprehension in the Advent of Code section of the constants. I've removed it. --- bot/constants.py | 2 +- bot/exts/christmas/advent_of_code/_cog.py | 4 ++-- bot/exts/christmas/advent_of_code/_helpers.py | 24 +++++++++++++++++------- 3 files changed, 20 insertions(+), 10 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index e313e086..c696b202 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -63,7 +63,7 @@ class AdventOfCode: staff_leaderboard_id = environ.get("AOC_STAFF_LEADERBOARD_ID", "") # Other Advent of Code constants - ignored_days = [day for day in environ.get("AOC_IGNORED_DAYS", "").split(",")] + ignored_days = environ.get("AOC_IGNORED_DAYS", "").split(",") leaderboard_displayed_members = 10 leaderboard_cache_expiry_seconds = 1800 year = int(environ.get("AOC_YEAR", datetime.utcnow().year)) diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 2f60e512..29dcc3cf 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -35,8 +35,8 @@ class AdventOfCode(commands.Cog): self.countdown_task = None self.status_task = None - countdown_coro = _helpers.day_countdown(self.bot) - self.countdown_task = self.bot.loop.create_task(countdown_coro) + announcement_coro = _helpers.new_puzzle_announcement(self.bot) + self.new_puzzle_announcement_task = self.bot.loop.create_task(announcement_coro) status_coro = _helpers.countdown_status(self.bot) self.status_task = self.bot.loop.create_task(status_coro) diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index c75f47fa..7a6d873e 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -374,9 +374,16 @@ async def wait_for_advent_of_code(*, hours_before: int = 1) -> None: """ Wait for the Advent of Code event to start. - This function returns `hours_before` (default: 1) before the Advent of Code + This function returns `hours_before` (default: 1) the Advent of Code actually starts. This allows functions to schedule and execute code that needs to run before the event starts. + + If the event has already started, this function returns immediately. + + Note: The "next Advent of Code" is determined based on the current value + of the `AOC_YEAR` environment variable. This allows callers to exit early + if we're already past the Advent of Code edition the bot is currently + configured for. """ start = datetime.datetime(AdventOfCode.year, 12, 1, 0, 0, 0, tzinfo=EST) target = start - datetime.timedelta(hours=hours_before) @@ -449,12 +456,13 @@ async def countdown_status(bot: Bot) -> None: await asyncio.sleep(delay) -async def day_countdown(bot: Bot) -> None: +async def new_puzzle_announcement(bot: Bot) -> None: """ - Calculate the number of seconds left until the next day of Advent. + Announce the release of a new Advent of Code puzzle. - Once we have calculated this we should then sleep that number and when the time is reached, ping - the Advent of Code role notifying them that the new challenge is ready. + This background task hibernates until just before the Advent of Code starts + and will then start announcing puzzles as they are published. After the + event has finished, this task will terminate. """ # We wake up one hour before the event starts to prepare the announcement # of the release of the first puzzle. @@ -483,8 +491,10 @@ async def day_countdown(bot: Bot) -> None: log.trace("Started puzzle notification loop.") tomorrow, time_left = time_left_to_est_midnight() - # Use fractional `total_seconds` to wake up very close to our target, with - # padding of 0.1 seconds to ensure that we actually pass midnight. + # Use `total_seconds` to get the time left in fractional seconds This + # should wake us up very close to the target. As a safe guard, the sleep + # duration is padded with 0.1 second to make sure we wake up after + # midnight. sleep_seconds = time_left.total_seconds() + 0.1 log.trace(f"The puzzle notification task will sleep for {sleep_seconds} seconds") await asyncio.sleep(sleep_seconds) -- cgit v1.2.3 From fb7838c6165b6b32f6561a871428330f9d8f0c7c Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 13 Dec 2020 11:20:13 +0100 Subject: Clarify comment on AoC Status Task startup delay The Advent of Code Status Countdown task needs to wait for two things to happen to prevent it from failing during the startup sequence: 1. The Websocket instance discord.py creates needs to be available as an attribute of the bot, otherwise discord.py fails internally: Traceback (most recent call last): File "discord/client.py", line 1049, in change_presence await self.ws.change_presence( activity=activity, status=status, afk=afk ) File "advent_of_code/_cog.py", line 52, in countdown_status await bot.change_presence(activity=discord.Game(playing)) AttributeError: 'NoneType' object has no attribute 'change_presence' 2. Allegedly, according to the discord.py community, trying to change the status too early in the sequence to establish a connection with Discord may result ub the Discord API aborting the connection. To solve this, I've added a `wait_until_guild_available` waiter, as it guarantees that the websocket is available and the connections is mature. Kaizen: I've changed the name `new_puzzle_announcement` to `new_puzzle_notification` to better reflect its function. --- bot/exts/christmas/advent_of_code/_cog.py | 2 +- bot/exts/christmas/advent_of_code/_helpers.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 8c07cdb4..c3b87f96 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -39,7 +39,7 @@ class AdventOfCode(commands.Cog): self.countdown_task = None self.status_task = None - notification_coro = _helpers.new_puzzle_announcement(self.bot) + notification_coro = _helpers.new_puzzle_notification(self.bot) self.notification_task = self.bot.loop.create_task(notification_coro) self.notification_task.set_name("Daily AoC Notification") self.notification_task.add_done_callback(_helpers.background_task_callback) diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index f8c0dc22..b7adc895 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -464,7 +464,10 @@ async def countdown_status(bot: Bot) -> None: log.info("The Advent of Code has started or will start soon, starting countdown status.") # Trying to change status too early in the bot's startup sequence will fail - # the task. Waiting until this event seems to work well. + # the task because the websocket instance has not yet been created. Waiting + # for this event means that both the websocket instance has been initialized + # and that the connection to Discord is mature enough to change the presence + # of the bot. await bot.wait_until_guild_available() # Calculate when the task needs to stop running. To prevent the task from @@ -501,7 +504,7 @@ async def countdown_status(bot: Bot) -> None: await asyncio.sleep(delay) -async def new_puzzle_announcement(bot: Bot) -> None: +async def new_puzzle_notification(bot: Bot) -> None: """ Announce the release of a new Advent of Code puzzle. -- cgit v1.2.3