Redémarrez un serveur WebSocket auquel dix mille clients sont attachés et il se produit quelque chose de désagréable. Chaque client remarque la déconnexion à peu près au même instant, chaque client attend le même nombre fixe de secondes, et chaque client rappelle au même top d'horloge. Le serveur redémarre et se voit immédiatement frappé par dix mille poignées de main TLS simultanées. Il s'effondre, ou il limite le débit, et les clients réessaient à nouveau en rangs serrés. C'est le troupeau tonnant, et un intervalle de reconnexion fixe le garantit.
sgcWebSockets 2026.7 s'attaque aux deux endroits où les clients martèlent un serveur déjà en difficulté. Le WatchDog peut désormais se reconnecter avec une temporisation exponentielle assortie de gigue au lieu d'un intervalle fixe, et le client HTTP peut réessayer tout seul une requête échouée, en respectant l'indication Retry-After du serveur, avec les mêmes calculs de temporisation. Les deux sont optionnels et désactivés par défaut, les applications existantes se comportent donc exactement comme avant tant que vous ne les activez pas.
Le problème d'un intervalle de reconnexion fixe
Le WatchDog a toujours été la manière standard de maintenir un client connecté. Vous définissez Enabled, vous définissez un Interval en secondes, et lorsque la connexion tombe le composant réessaie toutes les Interval secondes jusqu'à ce qu'elle soit rétablie.
C'est très bien pour un client unique sur une liaison Wi-Fi capricieuse. C'est le mauvais comportement pour une flotte. Avec un intervalle fixe, chaque client de la flotte est un métronome qui bat au même rythme, et un redémarrage du serveur les synchronise tous : ils se sont tous déconnectés au même instant, donc ils se reconnectent tous au même instant, indéfiniment. Pire, l'intervalle ne s'adapte pas. Si le serveur est indisponible pendant dix minutes, un client avec Interval = 1 effectue six cents tentatives de connexion inutiles, et tous les autres en font autant.
Ce que vous voulez, c'est l'inverse. Réessayer rapidement la première fois, car la plupart des déconnexions sont passagères et une reconnexion rapide est invisible pour l'utilisateur. Puis se retirer progressivement, afin qu'un serveur réellement indisponible ne soit pas pilonné. Et étaler les clients, pour qu'ils n'arrivent pas tous ensemble.
Temporisation exponentielle sur le WatchDog
L'objet d'options du WatchDog a gagné quatre propriétés. Le type vit dans sgcTCP_Classes :
type
TsgcWatchDogBackoff = (wdbFixed, wdbExponential);
Sur n'importe quel composant WebSocket, les options s'atteignent via Client.WatchDog, et les réglages de temporisation sont les suivants :
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;
Le calcul est délibérément ennuyeux. La première tentative de reconnexion attend Interval. Chaque tentative suivante multiplie le délai précédent par BackoffMultiplier, et le résultat est borné à MaxInterval. Avec les valeurs ci-dessus, les délais sont de 1 s, 2 s, 4 s, 8 s, 16 s, 32 s, 60 s, 60 s, 60 s et ainsi de suite. Le compteur se réinitialise dès que le client est reconnecté, si bien qu'une brève coupure réseau vous coûte une seconde, pas une minute.
Interval et MaxInterval sont tous deux exprimés en secondes, comme cela a toujours été le cas pour le WatchDog. Un MaxInterval de 0 signifie pas de plafond, ce qui est la valeur par défaut. Si vous activez la temporisation exponentielle, fixez un plafond, sinon le délai continue de doubler jusqu'à se mesurer en heures.
Le détail important pour les utilisateurs existants : Backoff vaut wdbFixed par défaut. Si vous mettez à jour et ne changez rien, votre client se reconnecte selon le même intervalle fixe qu'auparavant. Le calcul du délai n'est utilisé que lorsque vous passez à wdbExponential ou que vous définissez une Jitter, et c'est le même utilitaire que celui employé par les nouvelles tentatives HTTP, sgcNextBackoffMs dans sgcBase_Helpers.
La gigue, et pourquoi elle compte à grande échelle
La temporisation exponentielle seule ne disperse pas le troupeau. Elle fait seulement en sorte qu'il arrive moins souvent. Dix mille clients qui attendent tous 1 s, puis 2 s, puis 4 s restent dix mille clients qui arrivent ensemble, simplement par vagues de plus en plus grosses.
Jitter est ce qui les étale réellement. C'est une fraction du délai calculé, appliquée aléatoirement par client et par tentative. Avec Jitter = 0.2, un délai nominal de 8 secondes devient une valeur aléatoire dans une bande de 20 % autour de 8 secondes, si bien que le client A se réveille à 7,1 s, le client B à 8,6 s, le client C à 8,0 s. La flotte arrive comme une traînée plutôt que comme un pic, et la file d'attente d'acceptation du serveur peut suivre.
// 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
La gigue est orthogonale au mode de temporisation. Vous pouvez laisser Backoff sur wdbFixed et ne définir que Jitter, ce qui vous donne l'intervalle fixe classique avec des clients désynchronisés. Cela suffit souvent à soi seul, et c'est le plus petit changement possible dans une application existante.
Réessayer automatiquement une requête HTTP
La seconde moitié de la fonctionnalité se trouve du côté HTTP. Une requête HTTP unique qui échoue avec un 503 parce que le serveur est en cours de redéploiement, ou un 429 parce que vous avez dépassé une limite de débit, n'est pas vraiment une erreur. C'est une requête qui devrait être renvoyée dans un instant. Écrire cette boucle à la main est fastidieux et tout le monde l'écrit légèrement de travers.
TsgcIdHTTP possède désormais un objet RetryOptions, déclaré dans sgcHTTP_Client sous le nom TsgcIdHTTPRetry_Options. Activez-le et la requête se réessaie d'elle-même :
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;
Les intervalles sont ici exprimés en millisecondes, et non en secondes, car les nouvelles tentatives HTTP vivent sur une échelle de temps bien plus courte qu'une reconnexion. Les délais suivent la même courbe que le WatchDog : 500 ms, 1 s, 2 s, 4 s, plafonnés à MaxInterval et étalés par Jitter.
StatusCodes est la liste des codes de statut HTTP considérés comme réessayables, sous forme de chaîne séparée par des virgules. La valeur par défaut est 429,502,503,504, qui sont les codes signifiant « pas maintenant, réessayez » plutôt que « votre requête est incorrecte ». Un 400 ou un 401 n'est jamais réessayé, car envoyer quatre fois de plus la même requête cassée n'aide personne. Les échecs au niveau de la connexion, comme une socket refusée ou un délai de lecture dépassé, sont réessayés eux aussi.
Le même objet est présent sur TsgcHTTPAPI_client dans sgcHTTP_API, qui est la classe de base de tous les clients REST prêts à l'emploi de la bibliothèque. La politique de nouvelles tentatives est donc disponible sur tous, sans aucune plomberie propre à chaque 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 : laissez le serveur donner le tempo
Calculer votre propre temporisation, c'est deviner. Lorsqu'un serveur renvoie 429 ou 503, il vous dit souvent exactement combien de temps attendre, dans l'en-tête de réponse Retry-After, soit sous forme d'un nombre de secondes, soit sous forme d'une date HTTP. Ce nombre n'est pas une suggestion. C'est le serveur qui vous dit quand il vous acceptera de nouveau, et un client qui l'ignore et réessaie selon son propre calendrier va tout simplement être rejeté à nouveau.
HonorRetryAfter vaut True par défaut. Lorsque la réponse porte un en-tête Retry-After, cette valeur l'emporte sur la temporisation calculée, et le client attend aussi longtemps que le serveur l'a demandé :
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.
Les deux formes de l'en-tête sont analysées, la forme en secondes d'écart et la forme date HTTP. C'est exactement ce qu'OpenAI et Anthropic demandent à leurs clients de faire lorsqu'ils renvoient un 429, et c'est la différence entre un client qui se remet d'une limitation de débit et un client qui y reste coincé.
Nouvelles tentatives sur les clients IA
Les clients IA sont l'endroit où les nouvelles tentatives justifient leur existence, car les limites de débit y sont routinières plutôt qu'exceptionnelles. Chacun expose son propre objet d'options de nouvelles tentatives sur sa propriété *Options, de sorte que les réglages sont visibles dans l'Inspecteur d'objets à côté du modèle et de la clé d'API.
Sur TsgcHTTP_API_OpenAI le chemin est OpenAIOptions.RetryOptions, de 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 présente la même forme sous AnthropicOptions.RetryOptions, et Gemini sous GeminiOptions.RetryOptions. Le même schéma existe sur les clients Grok, Mistral, DeepSeek et Ollama, ainsi que sur les clients Stripe et Paddle, où un appel de paiement réessayé revêt une importance d'une tout autre nature.
// 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;
Notez que les noms de propriétés sur les objets IA sont Retries et Wait plutôt que MaxRetries et InitialInterval, afin de rester cohérents avec la nomenclature déjà employée ailleurs sur ces composants. Le comportement est identique, et en interne ils configurent le même TsgcIdHTTPRetry_Options sur le client HTTP sous-jacent.
Savoir pourquoi cela a échoué : Connect et LastError
Une boucle de nouvelles tentatives ne vaut que ce que vaut sa gestion des erreurs. Avant de pouvoir décider si un échec mérite une nouvelle tentative, vous devez savoir quel était l'échec, et jusqu'à présent le seul moyen de le découvrir sur un client WebSocket était de câbler un gestionnaire OnError et de ranger le message dans un champ quelque part.
2026.7 ajoute une surcharge de Connect qui vous remet l'erreur directement, dans sgcWebSocket_Client_Base :
function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;
Ainsi une boucle de reconnexion manuelle peut inspecter la raison et décider par elle-même :
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;
La dernière erreur est également conservée sur le composant lui-même. TsgcWSComponent_Base dans sgcWebSocket_Classes publie une propriété LastError: string en lecture seule et une méthode ClearLastError, ce qui vous permet de lire la raison après coup, et de la réinitialiser avant une tentative que vous voulez mesurer :
sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);
Rien de tout cela ne remplace OnError, qui se déclenche toujours comme il l'a toujours fait. Cela signifie simplement qu'un morceau de code simple et synchrone n'a plus besoin de mettre en place un gestionnaire d'événements pour répondre à une question simple.
Disponibilité
La temporisation du WatchDog, la gigue, les nouvelles tentatives HTTP et la nouvelle surcharge de Connect sont disponibles à partir de sgcWebSockets 2026.7 sur Delphi 7 à 13 et C++Builder, sur Win32/Win64, Linux64, macOS, Android et iOS. Tout ce qui est décrit ici est désactivé par défaut. Backoff vaut wdbFixed, Jitter vaut 0 et RetryOptions.Enabled vaut False, si bien qu'une application existante se met à jour sans le moindre changement de comportement et que vous activez ce qui a du sens pour vous.
Les clients disposant d'un abonnement actif peuvent télécharger la nouvelle version depuis l'espace client. Les utilisateurs de la version d'essai peuvent récupérer l'installateur mis à jour sur esegece.com/products/websockets/download.
Des questions, des remarques, ou besoin d'aide pour choisir un profil de temporisation pour votre flotte ? Contactez-nous, vous recevrez une réponse des personnes qui ont écrit le code.
