Skip to content

Commit 57f9fc6

Browse files
committed
Retry Discord bot startup with backoff
1 parent 2cdf2c2 commit 57f9fc6

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

server/pychess_global_app_state.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
from aiohttp import web
1212
from aiohttp.web_ws import WebSocketResponse
1313
import aiohttp_jinja2
14+
from tenacity import (
15+
AsyncRetrying,
16+
before_sleep_log,
17+
retry_if_exception_type,
18+
wait_exponential_jitter,
19+
)
1420

1521
from pythongettext.msgfmt import Msgfmt, PoSyntaxError
1622
from sortedcollections import ValueSortedDict
@@ -509,14 +515,32 @@ def __init_fishnet_monitor(self) -> dict:
509515
def __init_discord(self):
510516
if self.db is None:
511517
self.discord = FakeDiscordBot()
518+
return
512519

513520
# create Discord bot
514521
if DEV:
515522
self.discord = FakeDiscordBot()
516523
else:
524+
if DISCORD_TOKEN == "":
525+
log.warning("DISCORD_TOKEN is missing/empty; Discord bot disabled")
526+
self.discord = FakeDiscordBot()
527+
return
528+
517529
bot = DiscordBot(self)
518530
self.discord = bot
519-
asyncio.create_task(bot.start(DISCORD_TOKEN), name="Discord-BOT")
531+
asyncio.create_task(self.__run_discord_bot(bot, DISCORD_TOKEN), name="Discord-BOT")
532+
533+
async def __run_discord_bot(self, bot: DiscordBot, token: str) -> None:
534+
# Keep retrying startup/login on transient failures so relay can recover
535+
# automatically after brief network/API hiccups during dyno boot.
536+
async for attempt in AsyncRetrying(
537+
retry=retry_if_exception_type(Exception),
538+
wait=wait_exponential_jitter(initial=1, max=120),
539+
reraise=True,
540+
before_sleep=before_sleep_log(log, logging.WARNING),
541+
):
542+
with attempt:
543+
await bot.start(token)
520544

521545
def __init_twitch(self) -> Twitch:
522546
result = Twitch(self.app)

tests/test_discord_startup.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import unittest
5+
6+
from pychess_global_app_state import PychessGlobalAppState
7+
8+
9+
class DiscordStartupTestCase(unittest.IsolatedAsyncioTestCase):
10+
async def test_run_discord_bot_retries_after_error_until_cancelled(self):
11+
class FlakyBot:
12+
def __init__(self) -> None:
13+
self.calls = 0
14+
15+
async def start(self, token: str) -> None:
16+
self.calls += 1
17+
if self.calls == 1:
18+
raise RuntimeError("transient startup failure")
19+
raise asyncio.CancelledError()
20+
21+
holder = type("Holder", (), {})()
22+
bot = FlakyBot()
23+
24+
run_discord_bot = PychessGlobalAppState._PychessGlobalAppState__run_discord_bot
25+
with self.assertRaises(asyncio.CancelledError):
26+
await run_discord_bot(holder, bot, "token-123")
27+
28+
self.assertEqual(bot.calls, 2)
29+
30+
async def test_run_discord_bot_returns_when_start_exits_cleanly(self):
31+
class CleanBot:
32+
def __init__(self) -> None:
33+
self.calls = 0
34+
35+
async def start(self, token: str) -> None:
36+
self.calls += 1
37+
return
38+
39+
holder = type("Holder", (), {})()
40+
bot = CleanBot()
41+
42+
run_discord_bot = PychessGlobalAppState._PychessGlobalAppState__run_discord_bot
43+
await run_discord_bot(holder, bot, "token-123")
44+
self.assertEqual(bot.calls, 1)
45+
46+
47+
if __name__ == "__main__":
48+
unittest.main()

0 commit comments

Comments
 (0)