feat(channel): add message send retry mechanism with exponential backoff
- Add send_max_retries config option (default: 3, range: 0-10) - Implement _send_with_retry in ChannelManager with 1s/2s/4s backoff - Propagate CancelledError for graceful shutdown - Fix telegram send_delta to raise exceptions for Manager retry - Add comprehensive tests for retry logic - Document channel settings in README
This commit is contained in:
@@ -7,10 +7,14 @@ from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.channels.base import BaseChannel
|
||||
from nanobot.config.schema import Config
|
||||
|
||||
# Retry delays for message sending (exponential backoff: 1s, 2s, 4s)
|
||||
_SEND_RETRY_DELAYS = (1, 2, 4)
|
||||
|
||||
|
||||
class ChannelManager:
|
||||
"""
|
||||
@@ -129,15 +133,7 @@ class ChannelManager:
|
||||
|
||||
channel = self.channels.get(msg.channel)
|
||||
if channel:
|
||||
try:
|
||||
if msg.metadata.get("_stream_delta") or msg.metadata.get("_stream_end"):
|
||||
await channel.send_delta(msg.chat_id, msg.content, msg.metadata)
|
||||
elif msg.metadata.get("_streamed"):
|
||||
pass
|
||||
else:
|
||||
await channel.send(msg)
|
||||
except Exception as e:
|
||||
logger.error("Error sending to {}: {}", msg.channel, e)
|
||||
await self._send_with_retry(channel, msg)
|
||||
else:
|
||||
logger.warning("Unknown channel: {}", msg.channel)
|
||||
|
||||
@@ -146,6 +142,41 @@ class ChannelManager:
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
async def _send_with_retry(self, channel: BaseChannel, msg: OutboundMessage) -> None:
|
||||
"""Send a message with retry on failure using exponential backoff.
|
||||
|
||||
Note: CancelledError is re-raised to allow graceful shutdown.
|
||||
"""
|
||||
max_attempts = max(self.config.channels.send_max_retries, 1)
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
if msg.metadata.get("_stream_delta") or msg.metadata.get("_stream_end"):
|
||||
await channel.send_delta(msg.chat_id, msg.content, msg.metadata)
|
||||
elif msg.metadata.get("_streamed"):
|
||||
pass
|
||||
else:
|
||||
await channel.send(msg)
|
||||
return # Send succeeded
|
||||
except asyncio.CancelledError:
|
||||
raise # Propagate cancellation for graceful shutdown
|
||||
except Exception as e:
|
||||
if attempt == max_attempts - 1:
|
||||
logger.error(
|
||||
"Failed to send to {} after {} attempts: {} - {}",
|
||||
msg.channel, max_attempts, type(e).__name__, e
|
||||
)
|
||||
return
|
||||
delay = _SEND_RETRY_DELAYS[min(attempt, len(_SEND_RETRY_DELAYS) - 1)]
|
||||
logger.warning(
|
||||
"Send to {} failed (attempt {}/{}): {}, retrying in {}s",
|
||||
msg.channel, attempt + 1, max_attempts, type(e).__name__, delay
|
||||
)
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
except asyncio.CancelledError:
|
||||
raise # Propagate cancellation during sleep
|
||||
|
||||
def get_channel(self, name: str) -> BaseChannel | None:
|
||||
"""Get a channel by name."""
|
||||
return self.channels.get(name)
|
||||
|
||||
Reference in New Issue
Block a user