cat swift/retry-timeout-cancellazione-networking.md
Retry, timeout e cancellazione nel networking
Una rete instabile richiede retry e timeout, ma il codice deve rispettare cancellazione e non riprovare errori non recuperabili.
Il networking fallisce. Una richiesta può fallire per rete instabile, timeout, server temporaneamente non disponibile, rate limit o dati corrotti.
Non tutti gli errori meritano retry.
Retry generico
func retry<T: Sendable>(
attempts: Int,
delay: Duration,
operation: () async throws -> T
) async throws -> T {
var lastError: Error?
for attempt in 0..<attempts {
try Task.checkCancellation()
do {
return try await operation()
} catch is CancellationError {
throw CancellationError()
} catch {
lastError = error
if attempt < attempts - 1 {
try await Task.sleep(for: delay)
}
}
}
throw lastError ?? APIError.invalidResponse
}
Questo è un punto di partenza. Ma riprova tutto, anche errori non recuperabili.
Decidere se ritentare
func shouldRetry(_ error: Error) -> Bool {
guard let apiError = error as? APIError else {
return true
}
switch apiError {
case .statusCode(500..<600, _):
return true
case .transport:
return true
default:
return false
}
}
Swift non permette pattern 500..<600 direttamente così su associated value se non lo modelli bene. Una forma più esplicita:
case .statusCode(let code, _):
return (500..<600).contains(code)
Non ritentare:
- 400 validation error;
- 401 auth;
- 404 not found;
- decoding error;
- mapping error.
Backoff
Invece di delay fisso:
let delay = Duration.milliseconds(200 * (attempt + 1))
O exponential backoff:
let milliseconds = 200 * Int(pow(2.0, Double(attempt)))
try await Task.sleep(for: .milliseconds(milliseconds))
Mantieni limiti ragionevoli. Non martellare il server.
Timeout
Puoi impostare timeout su URLRequest:
request.timeoutInterval = 15
Oppure configurare URLSessionConfiguration.
Il timeout deve essere coerente col contesto:
- ricerca: breve;
- upload grande: più lungo;
- background sync: diverso.
Timeout con task group
Puoi costruire timeout concorrendo tra operazione è sleep, ma è facile complicare. Parti dalle API di URLSession e configura correttamente.
Se implementi timeout manuale, rispetta cancellazione.
Cancellazione
URLSession con async/await rispetta cancellazione del task.
Nel tuo retry:
catch is CancellationError {
throw CancellationError()
}
Non devi trasformarla in errore di rete.
Retry nel client
func sendWithRetry<T: Decodable & Sendable>(_ endpoint: Endpoint) async throws -> T {
try await retry(attempts: 3, delay: .milliseconds(250)) {
try await send(endpoint)
}
}
Poi raffina retry con shouldRetry.
Idempotenza
Ritentare GET è spesso sicuro.
Ritentare POST può creare effetti doppi se il server non è idempotente.
Non fare retry cieco su operazioni che modificano stato:
- creare ordine;
- pagare;
- inviare messaggio;
- confermare prenotazione.
Serve supporto server: idempotency key o semantica chiara.
Checklist
Prima di aggiungere retry:
- l’operazione è idempotente?
- quali errori sono recuperabili?
- quanti tentativi?
- quale delay/backoff?
- il retry rispetta cancellazione?
- il timeout è configurato?
- gli errori finali restano diagnostici?
Esercizio
Implementa:
func retry<T: Sendable>(
attempts: Int,
delay: Duration,
shouldRetry: (Error) -> Bool,
operation: () async throws -> T
) async throws -> T
Poi usalo solo per richieste GET.
Scrivi tre casi:
- 500: ritenta;
- 404: non ritenta;
- cancellazione: non ritenta e rilancia
CancellationError.