Discord Botの再接続
今回扱うDiscord Bot
- コマンドでBotがメッセージを投稿
- Botが投稿したメッセージに対するリアクションでメッセージの文面を変更したり削除したりできる
使用ライブラリ
インフラ
Google Compute Engine(GCE)の無料枠でDockerイメージを動かしている。
Discord BotはHTTPSサーバーではなく、Web Socketクライアントとして動作するので、いつものGoogle App Engine スタンダード環境は使えない。
問題
- 突然オフラインになる。
- 私がGCEインスタンス再起動という対応をするまでは自動でオンラインに復旧しない
目指したい状態
- 突然のオフラインは許容する
- Web Socketクライアントという性質上、それが単一障害点になることは避けられない。
- オフラインになったら、10分以内に自動でオンラインに復旧する 。
原因と考えられる現象
ネットワークが切断されるとDiscord Botはオフラインになる。
オフラインになったと思われる時刻にネットワークの問題と思われるログが残っていた。
ネットワーク切断をローカルで再現する
BotをローカルPCのDockerで動かす。
docker compose run discord_atumaru_bot poetry run python main.py
NETWORK IDを確認する
docker network ls
NETWORK ID NAME DRIVER SCOPE
128719f5aaf1 discord_atumaru_bot_default bridge local
CONTAINER IDを確認する
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e3c3912fe563 discord_atumaru_bot_discord_atumaru_bot "poetry run python m…" About a minute ago Up About a minute discord_atumaru_bot_discord_atumaru_bot_run_6762d136a433
ネットワークを切断する
# docker network disconnect <NETWORK ID> <CONTAINER ID>
docker network disconnect 128719f5aaf1 e3c3912fe563
ネットワークを切断したらどうなるか
Botがコマンドに反応しなくなる。
しばらくすると、Botがオフラインになる。
Pythonスクリプトは終了しない。
ネットワークが切断されたら、Pythonスクリプトが終了されるようにする
discord.pyにreconnect引数があり、デフォルトTrueになっていることを発見
reconnect引数をFalseにした。
client.run(token, reconnect=False)
ネットワーク切断を起こすと、Pythonスクリプトが終了されるようになった。
in connect
proto = await self._create_connection(req, traces, timeout)
File "/root/.cache/pypoetry/virtualenvs/discord-atumaru-bot-9TtSrW0h-py3.9/lib/python3.9/site-packages/aiohttp/connector.py", line 892, in _create_connection
_, proto = await self._create_direct_connection(req, traces, timeout)
File "/root/.cache/pypoetry/virtualenvs/discord-atumaru-bot-9TtSrW0h-py3.9/lib/python3.9/site-packages/aiohttp/connector.py", line 1011, in _create_direct_connection
raise ClientConnectorError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
Exponential Backoffを実装する
Botがネットワークエラーになったら、自動で再接続するようにしたい。しかし、DiscordやGCPの障害の可能性もあるため、再接続するインターバルは回数毎に長くしたい。
Pythonスクリプトの改修
再接続の時は、Clientを再度作成する。作成するときは asyncio.new_event_loop()
で作成したイベントループをClientに渡す。理由はdiscord.Client.run()は標準だと内部でasyncio.get_event_loop()を使うが、asyncio.get_event_loop()は1スレッド1つしかイベントループを作らず、discord.py内部ではclose処理が行われているため、新しく作らないと Event loop is closed
エラーが発生してしまう。
class BotTask:
"ネットワーク接続が切れたときの再接続対応クラス"
def __init__(self):
# 再接続を行った回数
self.retry_count = 0
def start_with_retry(self):
"再接続対応でBotを開始する"
while True:
try:
# Botを開始する
self._start()
# 正常終了の時はスクリプトを終了する
break
except Exception as e:
self.retry_count += 1
print(e)
# エラーが発生したときは
print("retry %d" % self.retry_count)
# 2のリトライ回数乗×5秒待つ
interval = 5*2**self.retry_count
print("interval %d" % interval)
# 待ち時間が4時間超えたら、スクリプトを終了する
if interval >= 4*60*60:
break
time.sleep(interval)
def _start(self):
# クライアントを作成
client = self._make_client()
# 環境変数からトークンを得る
token = os.environ['DISCORD_TOKEN']
# Botを実行
client.run(token, reconnect=False)
def _make_client(self):
"Discordのクライアントを作成する"
# リアクション削除の取得に必要
intents = discord.Intents.default()
intents.members = True
# Clientクラス内部で作られるイベントループは1スレッド1つ。
# そしてエラーになると、それが閉じられるので、毎回新しく作成する
loop = asyncio.new_event_loop()
# それをクライアント作成時に渡す
client = discord.Client(intents=intents, loop=loop)
@client.event
async def on_ready():
# 接続出来たのでretry_countを戻す
self.retry_count = 0
print('We have logged in as {0.user}'.format(client))
# 省略
return client
task = BotTask()
task.start_with_retry()
動作確認
ネットワークが切断されてエラー落ちしたが、ネットワーク普及したので再接続されたケース
We have logged in as 【テスト】集まるBot#3704
ここでdisconnect
Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
retry 1
interval 10
ここでconnect
We have logged in as 【テスト】集まるBot#3704
ずっとネットワークが切断されたケース
最大再接続インターバルを30秒に設定
We have logged in as 【テスト】集まるBot#3704
Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
retry 1
interval 10
Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
retry 2
interval 20
Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
retry 3
interval 40