diff --git a/src/tg/sender.py b/src/tg/sender.py index 77a1e40..0280696 100644 --- a/src/tg/sender.py +++ b/src/tg/sender.py @@ -176,7 +176,15 @@ async def _send_to_chat_id_async( ) return True except telegram.error.TelegramError as e: - logger.error(f"Could not send a message to {chat_id}", exc_info=e) + # skip_broadcast: this failure is already surfaced explicitly + # below (redirected to error_logs_recipients with the message + # content); broadcasting it again via ErrorBroadcastHandler + # would re-enter this same send path. + logger.error( + f"Could not send a message to {chat_id}", + exc_info=e, + extra={"skip_broadcast": True}, + ) # Captured now: the "except ... as e" binding is cleared by Python # when its except block exits, and the inner except below rebinds # its own "e" -- reading e.message after the loop would otherwise @@ -200,10 +208,15 @@ async def _send_to_chat_id_async( **kwargs, ) except telegram.error.TelegramError as redirect_error: + # skip_broadcast: broadcasting this would try to + # re-notify the very error_logs_recipients that just + # failed, which is how a single flood-control hit + # turns into a runaway retry storm. logger.error( "Could not redirect unsended message " f"to error_logs_recipients {error_logs_recipient}", exc_info=redirect_error, + extra={"skip_broadcast": True}, ) # HTML parse error isn't a separate class in Telegram @@ -225,9 +238,12 @@ async def _send_to_chat_id_async( ) return True except telegram.error.TelegramError as e: + # skip_broadcast: same rationale as above -- this is a + # failure of the error-notification path itself. logger.error( f"Could not send a plain-text message to {chat_id}", exc_info=e, + extra={"skip_broadcast": True}, ) return False diff --git a/src/utils/log_handler.py b/src/utils/log_handler.py index 55fdfc0..7f51d04 100644 --- a/src/utils/log_handler.py +++ b/src/utils/log_handler.py @@ -43,7 +43,15 @@ def emit(self, record: LogRecord): exc_info=e, ) ) - if record.levelno >= ERROR and not self.is_muted: + # skip_broadcast lets TelegramSender's own send-failure logs opt out: + # broadcasting a Telegram-delivery failure over Telegram is circular + # and, under flood control, snowballs into a self-sustaining error + # storm (each failed broadcast attempt logs another ERROR). + if ( + record.levelno >= ERROR + and not self.is_muted + and not getattr(record, "skip_broadcast", False) + ): error_message = f"{record.levelname} - {record.module} - {record.message}" if record.exc_text: error_message += f" - {record.exc_text}"