Riavvia un server WebSocket a cui sono collegati diecimila client e succede qualcosa di spiacevole. Ogni client si accorge della disconnessione più o meno nello stesso momento, ogni client aspetta lo stesso numero fisso di secondi e ogni client richiama allo stesso istante. Il server torna su e viene immediatamente colpito da diecimila handshake TLS simultanei. Crolla, oppure limita il traffico, e i client ritentano di nuovo tutti insieme. Questo è il thundering herd, e un intervallo di riconnessione fisso lo garantisce.
sgcWebSockets 2026.7 affronta i due punti in cui i client martellano un server che è già in difficoltà. Il WatchDog può ora riconnettersi con backoff esponenziale più jitter invece che a intervallo fisso, e il client HTTP può ritentare da solo una richiesta fallita, rispettando il suggerimento Retry-After del server, usando la stessa matematica di backoff. Entrambi sono opzionali e disattivati per impostazione predefinita, quindi le applicazioni esistenti si comportano esattamente come prima finché non li abiliti.
Il problema di un intervallo di riconnessione fisso
Il WatchDog è sempre stato il modo standard per mantenere un client connesso. Imposti Enabled, imposti un Interval in secondi, e quando la connessione cade il componente ritenta ogni Interval secondi finché non è di nuovo attiva.
Va bene per un singolo client su un collegamento Wi-Fi instabile. È il comportamento sbagliato per una flotta. Con un intervallo fisso ogni client della flotta è un metronomo che scandisce lo stesso ritmo, e un riavvio del server li sincronizza tutti: si sono disconnessi tutti nello stesso istante, quindi si riconnettono tutti nello stesso istante, per sempre. Peggio ancora, l'intervallo non si adatta. Se il server resta giù per dieci minuti, un client con Interval = 1 effettua seicento tentativi di connessione inutili, e lo stesso fanno tutti gli altri.
Quello che vuoi è l'opposto. Riprova rapidamente la prima volta, perché la maggior parte delle disconnessioni è transitoria e una riconnessione veloce è invisibile all'utente. Poi rallenta, così un server che è davvero giù non viene tempestato. E distribuisci i client nel tempo, così non arrivano tutti insieme.
Backoff esponenziale sul WatchDog
L'oggetto delle opzioni del WatchDog ha guadagnato quattro proprietà. Il tipo risiede in sgcTCP_Classes:
type
TsgcWatchDogBackoff = (wdbFixed, wdbExponential);
Su qualunque componente WebSocket le opzioni si raggiungono come Client.WatchDog, e le impostazioni di backoff sono queste:
uses
sgcWebSocket, sgcTCP_Classes;
begin
sgcWebSocketClient1.WatchDog.Enabled := True;
sgcWebSocketClient1.WatchDog.Attempts := 0; // 0 = keep trying forever
sgcWebSocketClient1.WatchDog.Interval := 1; // seconds, the first delay
sgcWebSocketClient1.WatchDog.Backoff := wdbExponential;
sgcWebSocketClient1.WatchDog.BackoffMultiplier := 2.0; // double it every attempt
sgcWebSocketClient1.WatchDog.MaxInterval := 60; // seconds, the ceiling
sgcWebSocketClient1.WatchDog.Jitter := 0.2; // up to 20% random spread
end;
La matematica è volutamente noiosa. Il primo tentativo di riconnessione aspetta Interval. Ogni tentativo successivo moltiplica il ritardo precedente per BackoffMultiplier, e il risultato viene limitato a MaxInterval. Con i valori qui sopra i ritardi vanno 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s e così via. Il contatore si azzera non appena il client è di nuovo connesso, quindi un breve calo di rete ti costa un secondo, non un minuto.
Interval e MaxInterval sono entrambi espressi in secondi, come è sempre stato per il WatchDog. Un MaxInterval pari a 0 significa nessun tetto massimo, che è il valore predefinito. Se abiliti il backoff esponenziale, imposta un tetto, altrimenti il ritardo continua a raddoppiare finché non si misura in ore.
Il dettaglio importante per gli utenti esistenti: Backoff vale per impostazione predefinita wdbFixed. Se aggiorni e non cambi nulla, il tuo client si riconnette con lo stesso intervallo fisso di sempre. Il calcolo del ritardo viene usato solo quando passi a wdbExponential o imposti un Jitter, ed è lo stesso helper usato dal retry HTTP, sgcNextBackoffMs in sgcBase_Helpers.
Il jitter, e perché conta su larga scala
Il backoff esponenziale da solo non spezza il branco. Fa solo sì che il branco arrivi meno spesso. Diecimila client che aspettano tutti 1s, poi 2s, poi 4s sono comunque diecimila client che arrivano insieme, solo in ondate sempre più grandi.
Jitter è ciò che li distribuisce davvero. È una frazione del ritardo calcolato, applicata in modo casuale per ogni client e per ogni tentativo. Con Jitter = 0.2 un ritardo nominale di 8 secondi diventa un valore casuale in una banda del 20% attorno agli 8 secondi, così il client A si sveglia a 7,1s, il client B a 8,6s, il client C a 8,0s. La flotta arriva come una macchia diffusa invece che come un picco, e la coda di accettazione del server riesce a stare al passo.
// A conservative fleet profile: fast first retry, hard ceiling, generous spread.
sgcWebSocketClient1.WatchDog.Enabled := True;
sgcWebSocketClient1.WatchDog.Interval := 2;
sgcWebSocketClient1.WatchDog.Backoff := wdbExponential;
sgcWebSocketClient1.WatchDog.BackoffMultiplier := 1.5; // gentler than doubling
sgcWebSocketClient1.WatchDog.MaxInterval := 120;
sgcWebSocketClient1.WatchDog.Jitter := 0.5; // spread over half the delay
Il jitter è ortogonale alla modalità di backoff. Puoi lasciare Backoff su wdbFixed e impostare solo Jitter, ottenendo il classico intervallo fisso con i client desincronizzati. Spesso questo basta da solo, ed è la modifica più piccola possibile a un'applicazione esistente.
Ritentare automaticamente una richiesta HTTP
La seconda metà della funzionalità è sul lato HTTP. Una singola richiesta HTTP che fallisce con un 503 perché il server sta effettuando un rilascio, o con un 429 perché hai superato un limite di frequenza, non è davvero un errore. È una richiesta che dovrebbe essere inviata di nuovo tra poco. Scrivere quel ciclo a mano è noioso e tutti lo scrivono in modo leggermente sbagliato.
TsgcIdHTTP ha ora un oggetto RetryOptions, dichiarato in sgcHTTP_Client come TsgcIdHTTPRetry_Options. Abilitalo e la richiesta si ritenta da sola:
uses
sgcHTTP_Client;
var
oHTTP: TsgcIdHTTP;
begin
oHTTP := TsgcIdHTTP.Create(nil);
try
oHTTP.RetryOptions.Enabled := True; // default False
oHTTP.RetryOptions.MaxRetries := 3;
oHTTP.RetryOptions.InitialInterval := 500; // milliseconds
oHTTP.RetryOptions.MaxInterval := 30000; // milliseconds
oHTTP.RetryOptions.Multiplier := 2.0;
oHTTP.RetryOptions.Jitter := 0.2;
oHTTP.RetryOptions.StatusCodes := '429,502,503,504';
ShowMessage(oHTTP.Get('https://api.example.com/v1/orders'));
finally
oHTTP.Free;
end;
end;
Qui gli intervalli sono in millisecondi, non in secondi, perché i retry HTTP vivono su una scala temporale molto più breve di una riconnessione. I ritardi seguono la stessa curva del WatchDog: 500ms, 1s, 2s, 4s, limitati a MaxInterval e distribuiti dal Jitter.
StatusCodes è l'elenco dei codici di stato HTTP che contano come ritentabili, sotto forma di stringa separata da virgole. Il valore predefinito è 429,502,503,504, che sono i codici che significano "non adesso, riprova" anziché "la tua richiesta è sbagliata". Un 400 o un 401 non viene mai ritentato, perché inviare la stessa richiesta difettosa altre quattro volte non aiuta nessuno. Anche i guasti a livello di connessione, come un socket rifiutato o un timeout di lettura, vengono ritentati.
Lo stesso oggetto è presente su TsgcHTTPAPI_client in sgcHTTP_API, che è la classe base di ogni client REST già pronto della libreria. Quindi la policy di retry è disponibile su tutti loro senza alcun lavoro di collegamento per ogni singolo client:
// Any API client that descends from TsgcHTTPAPI_client
sgcHTTPAPI_Client1.RetryOptions.Enabled := True;
sgcHTTPAPI_Client1.RetryOptions.MaxRetries := 5;
sgcHTTPAPI_Client1.RetryOptions.StatusCodes := '429,500,502,503,504';
Retry-After: lascia che sia il server a dettare il ritmo
Calcolare il proprio backoff è un'ipotesi. Quando un server restituisce 429 o 503 spesso ti dice esattamente quanto aspettare, nell'header di risposta Retry-After, o come numero di secondi o come data HTTP. Quel numero non è un suggerimento. È il server che ti dice quando ti accetterà di nuovo, e un client che lo ignora e ritenta secondo la propria pianificazione verrà semplicemente rifiutato di nuovo.
HonorRetryAfter vale True per impostazione predefinita. Quando la risposta porta con sé un header Retry-After, quel valore prevale sul backoff calcolato e il client aspetta tutto il tempo richiesto dal server:
oHTTP.RetryOptions.Enabled := True;
oHTTP.RetryOptions.HonorRetryAfter := True; // default True
oHTTP.RetryOptions.InitialInterval := 500;
oHTTP.RetryOptions.MaxRetries := 3;
// Server replies 429 with "Retry-After: 12".
// The computed backoff would have been 500ms, the client waits 12 seconds instead.
Vengono interpretate entrambe le forme dell'header, quella con i secondi di delta e quella con la data HTTP. È esattamente ciò che OpenAI e Anthropic chiedono ai loro client di fare quando restituiscono un 429, ed è la differenza tra un client che si riprende da un limite di frequenza e un client che resta bloccato dal limite.
Retry sui client AI
I client AI sono il posto dove i retry si guadagnano lo stipendio, perché lì i limiti di frequenza sono la norma anziché l'eccezione. Ognuno di essi espone il proprio oggetto di opzioni di retry sulla rispettiva proprietà *Options, così le impostazioni sono visibili nell'Object Inspector accanto al modello e alla chiave API.
Su TsgcHTTP_API_OpenAI il percorso è OpenAIOptions.RetryOptions, di classe TsgcHTTPOpenAIRetry_Options:
uses
sgcHTTP_API_OpenAI;
begin
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Enabled := True;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Retries := 3;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Wait := 10000; // ms, first delay
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.MaxInterval := 30000; // ms, ceiling
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Multiplier := 2.0;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Jitter := 0.2;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.HonorRetryAfter := True;
end;
Anthropic ha la stessa struttura sotto AnthropicOptions.RetryOptions, e Gemini sotto GeminiOptions.RetryOptions. Lo stesso schema esiste sui client Grok, Mistral, DeepSeek e Ollama, e sui client Stripe e Paddle, dove una chiamata di pagamento ritentata è un tipo di cosa importante molto diverso.
// Anthropic returns 429 and 529 under load, both are treated as retryable.
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.Enabled := True;
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.Retries := 3;
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.HonorRetryAfter := True;
Nota che i nomi delle proprietà sugli oggetti AI sono Retries e Wait anziché MaxRetries e InitialInterval, per restare coerenti con la nomenclatura già usata altrove su quei componenti. Il comportamento è identico, e internamente configurano le stesse TsgcIdHTTPRetry_Options sul client HTTP sottostante.
Sapere perché è fallito: Connect e LastError
Un ciclo di retry vale solo quanto la sua gestione degli errori. Prima di poter decidere se vale la pena ritentare dopo un fallimento, devi sapere quale sia stato il fallimento, e fino ad ora l'unico modo per scoprirlo su un client WebSocket era collegare un gestore OnError e conservare il messaggio in un campo da qualche parte.
La 2026.7 aggiunge un overload di Connect che ti consegna l'errore direttamente, in sgcWebSocket_Client_Base:
function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;
Così un ciclo di riconnessione manuale può ispezionare il motivo e decidere da sé:
var
vError: string;
begin
if not sgcWebSocketClient1.Connect(vError, 5000) then
begin
// A DNS failure or a refused socket is worth retrying.
// A rejected certificate or a 401 on the handshake is not.
Memo1.Lines.Add('connect failed: ' + vError);
Exit;
end;
end;
L'ultimo errore viene conservato anche sul componente stesso. TsgcWSComponent_Base in sgcWebSocket_Classes pubblica una proprietà di sola lettura LastError: string e un metodo ClearLastError, così puoi leggere il motivo a posteriori e azzerarlo prima di un tentativo che vuoi misurare:
sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);
Niente di tutto questo sostituisce OnError, che continua a scattare come ha sempre fatto. Significa solo che un pezzo di codice semplice e sincrono non deve più predisporre un gestore di eventi per rispondere a una domanda semplice.
Disponibilità
Il backoff del WatchDog, il jitter, i retry HTTP e il nuovo overload di Connect sono disponibili da sgcWebSockets 2026.7 su Delphi da 7 a 13 e C++Builder, su Win32/Win64, Linux64, macOS, Android e iOS. Tutto quanto descritto qui è disattivato per impostazione predefinita. Backoff vale wdbFixed, Jitter vale 0 e RetryOptions.Enabled vale False, quindi un'applicazione esistente si aggiorna senza alcun cambiamento di comportamento e tu attivi le opzioni dove ha senso.
I clienti con un abbonamento attivo possono scaricare la nuova build dall'area clienti. Gli utenti della versione di prova possono scaricare l'installer aggiornato su esegece.com/products/websockets/download.
Domande, commenti o bisogno di aiuto per scegliere un profilo di backoff per la tua flotta? Contattaci, riceverai una risposta dalle persone che hanno scritto il codice.
