跳转至

后台任务

Bostie 在闹钟响个不停时呼呼大睡

@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=...) 会在每天的某个时刻运行它,或者在若干个时刻各运行一次。没有 tzinfo 的 datetime.time 按 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,await check_feeds() 仍然会运行一次该函数。它的方法可以在机器人运行期间修改计划:

Task 作用
stop() 取消任务,无论正在运行还是处于两次运行之间,并让它在 start() 之前不再随机器人启动;在机器人运行之前调用时,任务会等待 start()
start() 在机器人就绪期间运行任务,若已就绪则从现在开始;已在运行的任务保持原样
restart() 取消任务并重新启动,因此间隔任务会立即运行
change_interval(seconds=...) 或 change_interval(time=...) 切换到新的计划;等待时间从修改时,或从正在进行的那次运行结束时重新计算
count 自任务上次启动以来已结束的运行次数
seconds、times 它所遵循的计划
@task.before、@task.after 注册 async def hook(),在任务每次启动和每次停止时 await,无论停止是由 stop()、restart()、路由器被移除还是 close() 引起的

钩子中的异常和运行中的异常一样,会作为 TaskError 发出。当 before 钩子抛出异常时,任务会保持停止状态,直到调用 restart()。

任务何时运行

任务在机器人发出 Started 时启动,close() 会在关闭 HTTP 客户端之前取消它们。注册在一个稍后才被引入的路由器上的任务,会在路由器被引入后立即启动;所属路由器被移除的任务会被取消。只由 ASGIApp 提供服务的应用,会在应用启动时启动其任务。

一个函数只能注册为一个任务。要按两种计划运行同一段代码,请注册两个函数。

任务抛出异常时

运行中的异常会作为一个 TaskError 事件发出,带有 task(抛出异常的 Task)和 error,任务会在下一个时刻再次运行。如果没有 TaskError 监听器,bostie 会记录该异常及其回溯信息。