From a3bf5aee2d7db27e3a0a79c040a877fce91989be Mon Sep 17 00:00:00 2001 From: nanobot Date: Tue, 28 Jul 2026 12:15:46 +0800 Subject: [PATCH] feat: retry channel startup on transient failures (max 5 attempts, exponential backoff) Previously, a single transient network error during channel startup (e.g., proxy blip causing Telegram get_me() to fail) would leave the channel permanently dead until the next gateway restart. Now _start_channel retries up to 5 times with exponential backoff starting at 2s (2s, 4s, 8s, 16s, 32s = ~62s total max wait). --- nanobot/channels/manager.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 3fbe32ed..31da7bba 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -330,19 +330,40 @@ class ChannelManager: value = getattr(section, key, None) return value if isinstance(value, bool) else default + # Retry settings for channel startup (transient failures like network blips) + _CHANNEL_START_MAX_RETRIES: int = 5 + _CHANNEL_START_BASE_DELAY: float = 2.0 # seconds, doubled each retry + async def _start_channel(self, name: str, channel: BaseChannel) -> None: - """Start a channel and log any exceptions.""" + """Start a channel with retries for transient failures.""" errors = getattr(self, "_channel_errors", None) if errors is None: errors = self._channel_errors = {} errors.pop(name, None) - try: - await channel.start() - except asyncio.CancelledError: - raise - except Exception: - errors[name] = "Channel failed to start. Check gateway logs." - logger.exception("Failed to start channel {}", name) + + last_exc: Exception | None = None + for attempt in range(self._CHANNEL_START_MAX_RETRIES + 1): + try: + await channel.start() + if attempt > 0: + logger.info("Channel {} started on attempt {}", name, attempt + 1) + return + except asyncio.CancelledError: + raise + except Exception as exc: + last_exc = exc + if attempt < self._CHANNEL_START_MAX_RETRIES: + delay = self._CHANNEL_START_BASE_DELAY * (2 ** attempt) + logger.warning( + "Channel {} start attempt {}/{} failed: {}. Retrying in {:.0f}s...", + name, attempt + 1, self._CHANNEL_START_MAX_RETRIES + 1, + exc, delay, + ) + await asyncio.sleep(delay) + + errors[name] = "Channel failed to start. Check gateway logs." + logger.exception("Failed to start channel {} after {} attempts", + name, self._CHANNEL_START_MAX_RETRIES + 1) def _start_channel_task(self, name: str, channel: BaseChannel) -> asyncio.Task: logger.info("Starting {} channel...", name)