コンテンツにスキップ

バックグラウンドタスク

目覚まし時計が鳴る中で眠っている Bostie

@bot.every は、Bot の準備ができている間、引数のない非同期関数をスケジュールに従って実行します。

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) は、Bot の準備ができた時点で関数を実行し、その後 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 を返し、check_feeds() を await すると、これまでどおり関数が一度実行されます。そのメソッドで、Bot の実行中にスケジュールを変更できます。

Task 動作
stop() 実行中でも実行の合間でもタスクをキャンセルし、start() が呼ばれるまで Bot と一緒に開始されないようにします。Bot の実行前に呼び出すと、タスクは start() を待ちます
start() Bot の準備ができている間タスクを実行します。準備ができていれば今すぐ開始します。実行中のタスクはそのまま続きます
restart() タスクをキャンセルして新たに開始します。そのため、間隔指定のタスクはすぐに実行されます
change_interval(seconds=...) または change_interval(time=...) 新しいスケジュールに切り替えます。待機は変更の時点から、または実行中の回が終わった時点から改めて始まります
count タスクが最後に開始されてから終了した実行の回数
seconds、times タスクが従うスケジュール
@task.before、@task.after async def hook() を登録します。タスクが開始するたび、および停止するたびに await されます。停止の理由が stop()、restart()、ルーターの除外、close() のどれであっても同じです

フック内の例外は、実行中の例外と同じく TaskError として送られます。before フックが例外を送出した場合、タスクは restart() まで停止したままになります。

タスクが実行されるとき

タスクは Bot が Started を送ったときに開始され、close() は HTTP クライアントを閉じる前にタスクをキャンセルします。後から組み込まれるルーターに登録されたタスクは組み込まれた時点で開始し、ルーターが除外されたタスクはキャンセルされます。ASGIApp だけで提供されるアプリは、アプリの起動時にタスクを開始します。

1 つの関数をタスクとして登録できるのは一度だけです。同じコードを 2 つのスケジュールで実行するには、2 つの関数を登録してください。

タスクが例外を送出したとき

実行中の例外は、task(例外を送出した Task)と error を持つ TaskError イベントとして送られ、タスクは次の時刻に再び実行されます。TaskError のリスナーがなければ、bostie はその例外をトレースバック付きでログに出力します。