Перейти к содержанию

Фоновые задачи

Бости спит, пока звенит будильник

@bot.every запускает асинхронную функцию без аргументов по расписанию, пока бот готов.

import datetime as dt
import logging

import bostie
from bostie import activity
from bostie.events import TaskError

log = logging.getLogger("my-bot")
bot = bostie.Bot()
TOKYO = dt.timezone(dt.timedelta(hours=9))


@bot.every(seconds=300)
async def show_guild_count() -> None:
    count = len(bot.cache.guilds())
    await bot.update_presence(activities=[activity.watching(f"{count} servers")])


@bot.every(time=[dt.time(9, tzinfo=TOKYO), dt.time(21, tzinfo=TOKYO)])
async def remind() -> None:
    log.info("twice a day, at 9:00 and 21:00 in Tokyo")


@bot.every(time=dt.time(0, 0))
async def midnight_utc() -> None:
    log.info("every day at midnight UTC")


@bot.listen
async def task_failed(failure: TaskError) -> None:
    log.error("%s failed", failure.task.callback.__name__, exc_info=failure.error)

every(seconds=300) запускает функцию, как только бот готов, а затем каждые 300 секунд. Расписание не уплывает: каждый запуск начинается на следующем кратном seconds от первого, а запуск, длящийся дольше seconds, пропускает такты, на которые пришёлся.

every(time=...) запускает её каждый день в одно время суток или в каждое из нескольких. datetime.time без tzinfo задано в UTC. С фиксированным смещением, как выше, время остаётся с этим смещением; с zoneinfo.ZoneInfo оно следует за переходами на летнее и зимнее время в этой зоне. Передавайте seconds или time, но не оба сразу.

Управление задачей

import logging

import bostie
from bostie import Context
from bostie.checks import is_owner

log = logging.getLogger("my-bot")
bot = bostie.Bot()


@bot.every(seconds=60)
async def check_feeds() -> None:
    log.info("checking the feeds, run %d", check_feeds.count)


@check_feeds.before
async def open_feeds() -> None:
    log.info("feed checks started")


@check_feeds.after
async def close_feeds() -> None:
    log.info("feed checks stopped")


@bot.command(checks=[is_owner])
async def feeds(ctx: Context, minutes: int | None = None) -> None:
    """Pause feed checks, or check every so many minutes."""
    if minutes is None:
        check_feeds.stop()
        await ctx.respond("Feed checks paused.", ephemeral=True)
        return
    check_feeds.change_interval(seconds=minutes * 60)
    check_feeds.start()
    await ctx.respond(f"Checking feeds every {minutes} minutes.", ephemeral=True)

@bot.every возвращает Task, а ожидание check_feeds() по-прежнему один раз выполняет функцию. Его методы меняют расписание, пока бот работает:

Task Что делает
stop() отменяет задачу — во время запуска или между запусками — и не даёт ей стартовать вместе с ботом до start(); если вызвать до запуска бота, задача ждёт start()
start() запускает задачу, пока бот готов, — с этого момента, если он уже готов; уже работающая задача продолжает как прежде
restart() отменяет задачу и запускает её заново, так что задача с интервалом выполняется сразу
change_interval(seconds=...) или change_interval(time=...) переключает на новое расписание; ожидание отсчитывается заново с момента изменения или с конца текущего запуска
count сколько запусков завершилось с последнего старта задачи
seconds, times расписание, которому она следует
@task.before, @task.after регистрируют async def hook(), которого дожидаются при каждом старте задачи и при каждой её остановке — через stop(), restart(), исключённый роутер или close()

Исключение в хуке отправляется как TaskError, так же как исключение в запуске. Если хук before выбрасывает исключение, задача остаётся остановленной до restart().

Когда выполняются задачи

Задачи стартуют, когда бот отправляет Started, а close() отменяет их до закрытия HTTP-клиента. Задача, зарегистрированная на роутере, который включается позже, стартует, как только его включат, а задача, чей роутер исключён, отменяется. Приложение, которое обслуживает только ASGIApp, запускает свои задачи при старте приложения.

Функцию можно зарегистрировать как задачу только один раз. Чтобы выполнять один и тот же код по двум расписаниям, зарегистрируйте две функции.

Когда задача выбрасывает исключение

Исключение в запуске отправляется как событие TaskError с task — Task, который выбросил исключение, — и error, и задача снова выполняется в своё следующее время. Если слушателя TaskError нет, bostie пишет исключение в лог с трассировкой.