fix: Telegram 409 충돌 시 WARNING 로그 + 30초 백오프 적용 (#180)
Some checks failed
CI / test (pull_request) Has been cancelled
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:
@@ -876,6 +876,59 @@ class TestGetUpdates:
|
||||
|
||||
assert updates == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_updates_409_sets_conflict_backoff(self) -> None:
|
||||
"""409 Conflict response sets conflict_backoff_until and returns empty list."""
|
||||
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||
handler = TelegramCommandHandler(client)
|
||||
|
||||
mock_resp = AsyncMock()
|
||||
mock_resp.status = 409
|
||||
mock_resp.text = AsyncMock(
|
||||
return_value='{"ok":false,"error_code":409,"description":"Conflict"}'
|
||||
)
|
||||
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("aiohttp.ClientSession.post", return_value=mock_resp):
|
||||
updates = await handler._get_updates()
|
||||
|
||||
assert updates == []
|
||||
assert handler._conflict_backoff_until > 0 # backoff was set
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_loop_skips_during_conflict_backoff(self) -> None:
|
||||
"""_poll_loop skips _get_updates while conflict backoff is active."""
|
||||
import asyncio as _asyncio
|
||||
|
||||
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||
handler = TelegramCommandHandler(client)
|
||||
|
||||
# Set an active backoff (far in the future)
|
||||
handler._conflict_backoff_until = _asyncio.get_event_loop().time() + 600
|
||||
|
||||
get_updates_called = []
|
||||
|
||||
async def mock_get_updates() -> list[dict]:
|
||||
get_updates_called.append(True)
|
||||
return []
|
||||
|
||||
handler._get_updates = mock_get_updates # type: ignore[method-assign]
|
||||
|
||||
# Run one iteration of the poll loop then stop
|
||||
handler._running = True
|
||||
task = _asyncio.create_task(handler._poll_loop())
|
||||
await _asyncio.sleep(0.05)
|
||||
handler._running = False
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except _asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# _get_updates should NOT have been called while backoff is active
|
||||
assert get_updates_called == []
|
||||
|
||||
|
||||
class TestCommandWithArgs:
|
||||
"""Test register_command_with_args and argument dispatch."""
|
||||
|
||||
Reference in New Issue
Block a user