Discordは3秒以上待てない

DiscordのAPIの仕様で、リクエストに対しては3秒以内にレスポンスを返さなければなりません。これはアプリケーションにとっては大きな課題になることがあります。

ダイスロールをするだけのボットであれば、高速に動作しますし、3秒以内にレスポンスができないなんてことはないでしょう。しかし、高度なアプリケーションや特定の環境下では3秒以上かかることもあります。考えられることをいくつか挙げてみましょう。

  • ネットワーク負荷が高く遅延した
  • ストレージへのアクセスに時間がかかった
  • 内部的にネットワークAPIを利用している
  • 効率の悪い・スケールの大きい計算をしている

通常アプリケーションは一度起動すると、極めて高速に動作します。これはコンピュータのCPUとメモリ上で計算されるからです。しかし、ディスクアクセスやネットワークアクセスがあると、そこがボトルネックとなります。読み書きの要求をしても、相手側がデータを準備して返事をしてくるまで待たされるからです。

仮に生成AIを使ったチャットボットを作ったとしましょう。GeminiやCopilot, OpenAIといったサービスを少し使えば、質問をしてから答えを返してくるまで少し時間がかかることがわかります。ボットの裏でこうしたサービスを使うことも考えられますが、3秒以内で返事が返ってこないとボットの動作は失敗してしまいます。

  • リクエストには3秒以内に反応しよう
  • 様々な要因によって遅延するかもしれないことを把握しておこう

タイムアウトを試してみる

では、実際にタイムアウトすることを試してみましょう。以前書いたコードに5秒だけ待つための一文を追加します。

client.on(Events.InteractionCreate, async interaction => {
    if (interaction.isCommand()) {
        if (interaction.commandName == 'ping') {
            // ここで5秒待つ
            await new Promise(resolve => setTimeout(resolve, 5000))
            interaction.reply('pong')
        } else {
            throw new Error('未定義のコマンドです')
        }
    }
})

await new Promise(resolve => setTimeout(resolve, 5000)) は初めて見る人にとっては理解が難しいコードと言えますが、簡単に解説します。

JavaScriptではユーザーが新しく非同期処理を作り出すことができ、それがPromiseと呼ばれるものです。new Promise() の形で新しいPromiseを生成でき、引数としてresolveとrejectを受け取る関数を指定します(rejectは省略しても構いません)。

この引数resolveは関数で、これを実行するまでPromiseは結果を待機することになります。また、setTimeout()は与えられた関数を、指定したミリ秒後に実行します。このふたつを組み合わせ、await キーワードを使うことで、指定した秒数だけ待機する処理を実現できます。

このコードで /ping コマンドを実行してみると、確かに「アプリケーションが応答しませんでした」と表示されるはずです。

3秒ルールに対する対抗案

場合によっては割と厳しいとも言える3秒ルールに対して、我々はどういうアプローチを取ればいいのでしょうか?方法はふたつ考えられます。

  • 爆速で返信する
  • 3秒以上待ってもらう

爆速で返信するのはひとつ考えられる案です。プログラミング経験が豊富であれば、いい設計をして効率的なコードが書けますし、速いレスポンスはユーザー経験(UX)もいいものとなるでしょう。シンプルなアプリケーションであればこれで十分です。

しかし、3秒以上待ってもらうことも考慮に値します。なぜならネットワークやストレージ、APIアクセスの結果が3秒以内に返ってくるという保証はないからです。ネットワーク障害などで回線状況が不安定になったときに、3秒ルールに抵触してボットが動いたり動かなくなったりすることは避けたいものです。

Deferレスポンス

Discordにはこれを解決するDefer (遅延)レスポンスという機能があります。これは一旦返事をしておいて、あとからその内容を書き換えて確定するというものです。

最初のDeferレスポンスをするまでには3秒ルールがありますが、その後の確定までは15分待ってくれます。なので、実質Discordの応答には15分の猶予があるとも言えます。

Deferレスポンスを使って少しのんびりした返事を返してみるために、先ほどのコードをさらに修正を加えます。

client.on(Events.InteractionCreate, async interaction => {
    if (interaction.isCommand()) {
        if (interaction.commandName == 'ping') {
            // 一旦返事をしておく
            await interaction.deferReply()
            // ここで5秒待つ
            await new Promise(resolve => setTimeout(resolve, 5000))
            // 正式な返事をする
            await interaction.editReply('pong')
        } else {
            throw new Error('未定義のコマンドです')
        }
    }
})

deferReply()を使うとここで一時応答をします。このときDiscordの画面上では「〇〇が考え中…」と表示され、処理中であることが視覚的にわかるようになります。その後、editReply() を実行すると内容が確定します。

第7回 ユーザーのメッセージに反応する