This commit is contained in:
2026-03-18 17:30:13 +01:00
parent 58a233236c
commit d80d912fb3
36 changed files with 3502 additions and 4 deletions
+81
View File
@@ -0,0 +1,81 @@
"""
Strategia backoff esponenziale per riconnessioni IMAP.
Parametri configurabili in WorkerSettings:
backoff_initial_seconds = 1.0 (primo wait)
backoff_multiplier = 2.0 (moltiplicatore)
backoff_max_seconds = 300.0 (tetto massimo: 5 minuti)
Sequenza di attesa: 1s → 2s → 4s → 8s → 16s → 32s → 64s → 128s → 256s → 300s → 300s → ...
"""
import asyncio
import logging
import random
from app.config import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
class ExponentialBackoff:
"""
Gestisce il backoff esponenziale con jitter opzionale.
Uso:
backoff = ExponentialBackoff(label="casella@pec.it")
while True:
try:
await connect()
backoff.reset() # connessione riuscita → resetta backoff
await run_loop()
except Exception as e:
await backoff.wait(e) # attende prima di ritentare
"""
def __init__(self, label: str = "", jitter: bool = True) -> None:
self.label = label
self.jitter = jitter
self._attempt = 0
self._current_wait = settings.backoff_initial_seconds
def reset(self) -> None:
"""Resetta il backoff dopo una connessione riuscita."""
if self._attempt > 0:
logger.info(
f"[{self.label}] Connessione ristabilita dopo {self._attempt} tentativi"
)
self._attempt = 0
self._current_wait = settings.backoff_initial_seconds
async def wait(self, error: Exception | None = None) -> None:
"""
Attende il tempo calcolato dal backoff, poi incrementa per il prossimo ciclo.
Aggiunge jitter ±10% per evitare thundering herd su N caselle.
"""
self._attempt += 1
wait_time = min(self._current_wait, settings.backoff_max_seconds)
if self.jitter:
jitter_range = wait_time * 0.1
wait_time += random.uniform(-jitter_range, jitter_range)
wait_time = max(0.5, wait_time)
logger.warning(
f"[{self.label}] Tentativo {self._attempt} fallito"
f"{f': {error}' if error else ''}. "
f"Riconnessione in {wait_time:.1f}s"
)
await asyncio.sleep(wait_time)
# Incrementa per il prossimo tentativo
self._current_wait = min(
self._current_wait * settings.backoff_multiplier,
settings.backoff_max_seconds,
)
@property
def attempt(self) -> int:
return self._attempt