fix: Telegram 409 충돌 시 WARNING 로그 + 30초 백오프 적용 (#180)
Some checks failed
CI / test (pull_request) Has been cancelled

다중 인스턴스 실행 시 Telegram getUpdates 409 응답을 ERROR가 아닌 WARNING으로
처리하고, 30초 동안 polling을 일시 중단하여 충돌을 완화.

- _conflict_backoff_until 속성 추가
- 409 감지 시 명확한 "another instance is polling" 메시지 출력
- poll_loop에서 백오프 활성 중 polling 스킵
- TestGetUpdates에 409 관련 테스트 2개 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-20 09:31:04 +09:00
parent bd2b3241b2
commit 77577f3f4d
2 changed files with 75 additions and 3 deletions

View File

@@ -516,6 +516,7 @@ class TelegramCommandHandler:
self._last_update_id = 0
self._polling_task: asyncio.Task[None] | None = None
self._running = False
self._conflict_backoff_until: float = 0.0 # epoch time; skip polling until then
def register_command(
self, command: str, handler: Callable[[], Awaitable[None]]
@@ -574,6 +575,12 @@ class TelegramCommandHandler:
async def _poll_loop(self) -> None:
"""Main polling loop that fetches updates."""
while self._running:
# Skip this iteration while a conflict backoff is active
now = asyncio.get_event_loop().time()
if now < self._conflict_backoff_until:
await asyncio.sleep(self._polling_interval)
continue
try:
updates = await self._get_updates()
for update in updates:
@@ -604,9 +611,21 @@ class TelegramCommandHandler:
async with session.post(url, json=payload) as resp:
if resp.status != 200:
error_text = await resp.text()
logger.error(
"getUpdates API error (status=%d): %s", resp.status, error_text
)
if resp.status == 409:
# Another bot instance is already polling — back off to reduce conflict.
_conflict_backoff_secs = 30.0
self._conflict_backoff_until = (
asyncio.get_event_loop().time() + _conflict_backoff_secs
)
logger.warning(
"Telegram conflict (409): another instance is polling. "
"Backing off %.0fs. Ensure only one bot instance runs at a time.",
_conflict_backoff_secs,
)
else:
logger.error(
"getUpdates API error (status=%d): %s", resp.status, error_text
)
return []
data = await resp.json()