Reconexão e novas tentativas: recuo exponencial, jitter e Retry-After | Blog eSeGeCe

Reconexão e novas tentativas: recuo exponencial, jitter e Retry-After

· Componentes

Reinicie um servidor WebSocket com dez mil clientes conectados e algo desagradável acontece. Todos os clientes percebem a desconexão praticamente no mesmo instante, todos esperam o mesmo número fixo de segundos e todos discam de volta no mesmo tique. O servidor sobe e é imediatamente atingido por dez mil handshakes TLS simultâneos. Ele cai, ou aplica limites, e os clientes tentam de novo em uníssono. Isso é a manada em disparada, e um intervalo fixo de reconexão a garante.

O sgcWebSockets 2026.7 trata dos dois pontos em que os clientes martelam um servidor que já está em apuros. O WatchDog agora pode reconectar com recuo exponencial mais jitter em vez de um intervalo fixo, e o cliente HTTP pode repetir sozinho uma requisição que falhou, respeitando a dica Retry-After do servidor, usando a mesma matemática de recuo. Ambos são opcionais e estão desativados por padrão, de modo que as aplicações existentes se comportam exatamente como antes até que você os habilite.

O problema com um intervalo fixo de reconexão

O WatchDog sempre foi a forma padrão de manter um cliente conectado. Você define Enabled, define um Interval em segundos, e quando a conexão cai o componente tenta de novo a cada Interval segundos até voltar.

Isso serve para um cliente em um link Wi-Fi instável. É o comportamento errado para uma frota. Com um intervalo fixo, cada cliente da frota é um metrônomo batendo no mesmo ritmo, e uma reinicialização do servidor sincroniza todos eles: todos desconectaram no mesmo instante, então todos reconectam no mesmo instante, para sempre. Pior, o intervalo não se adapta. Se o servidor ficar fora do ar por dez minutos, um cliente com Interval = 1 faz seiscentas tentativas de conexão inúteis, e todos os outros também.

O que você quer é o oposto. Tente de novo rapidamente da primeira vez, porque a maioria das desconexões é passageira e uma reconexão rápida é invisível para o usuário. Depois recue, para que um servidor que realmente está fora do ar não seja martelado. E espalhe os clientes, para que não cheguem todos juntos.

Recuo exponencial no WatchDog

O objeto de opções do WatchDog ganhou quatro propriedades. O tipo vive em sgcTCP_Classes:

type
  TsgcWatchDogBackoff = (wdbFixed, wdbExponential);

Em qualquer componente WebSocket as opções são acessadas como Client.WatchDog, e as configurações de recuo são estas:

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;

A matemática é deliberadamente banal. A primeira tentativa de reconexão espera Interval. Cada tentativa seguinte multiplica o atraso anterior por BackoffMultiplier, e o resultado é limitado a MaxInterval. Com os valores acima, os atrasos vão de 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s e assim por diante. O contador é reiniciado assim que o cliente está conectado de novo, então uma pequena falha de rede custa a você um segundo, não um minuto.

Interval e MaxInterval são ambos expressos em segundos, como o WatchDog sempre foi. Um MaxInterval de 0 significa sem teto, que é o padrão. Se você habilitar o recuo exponencial, defina um teto, caso contrário o atraso continua dobrando até ser medido em horas.

O detalhe importante para os usuários atuais: Backoff tem wdbFixed como padrão. Se você atualizar e não mudar nada, o seu cliente reconecta no mesmo intervalo fixo de sempre. O cálculo do atraso só é usado quando você muda para wdbExponential ou define um Jitter, e é o mesmo auxiliar que a nova tentativa HTTP usa, sgcNextBackoffMs em sgcBase_Helpers.

Jitter, e por que ele importa em escala

O recuo exponencial sozinho não quebra a manada. Ele só faz a manada chegar com menos frequência. Dez mil clientes que esperam todos 1s, depois 2s, depois 4s continuam sendo dez mil clientes chegando juntos, apenas em ondas cada vez maiores.

Jitter é o que de fato os espalha. É uma fração do atraso calculado, aplicada aleatoriamente por cliente e por tentativa. Com Jitter = 0.2 um atraso nominal de 8 segundos vira um valor aleatório em uma faixa de 20% em torno de 8 segundos, então o cliente A acorda aos 7,1s, o cliente B aos 8,6s, o cliente C aos 8,0s. A frota chega como uma mancha em vez de um pico, e a fila de aceitação do servidor consegue dar conta.

// 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

O jitter é ortogonal ao modo de recuo. Você pode deixar Backoff em wdbFixed e definir apenas Jitter, o que lhe dá o clássico intervalo fixo com os clientes dessincronizados. Isso muitas vezes já basta por si só, e é a menor mudança possível em uma aplicação existente.

Repetindo uma requisição HTTP automaticamente

A segunda metade do recurso está do lado do HTTP. Uma única requisição HTTP que falha com um 503 porque o servidor está sendo reimplantado, ou com um 429 porque você passou de um limite de taxa, não é realmente um erro. É uma requisição que deveria ser enviada de novo daqui a pouco. Escrever esse laço à mão é tedioso e todo mundo o escreve um pouco errado.

TsgcIdHTTP agora tem um objeto RetryOptions, declarado em sgcHTTP_Client como TsgcIdHTTPRetry_Options. Habilite-o e a requisição se repete sozinha:

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;

Os intervalos aqui são em milissegundos, não em segundos, porque as novas tentativas HTTP vivem em uma escala de tempo muito mais curta do que uma reconexão. Os atrasos seguem a mesma curva do WatchDog: 500ms, 1s, 2s, 4s, limitados por MaxInterval e espalhados por Jitter.

StatusCodes é a lista de códigos de status HTTP que contam como passíveis de nova tentativa, na forma de uma string separada por vírgulas. O padrão é 429,502,503,504, que são os códigos que significam "agora não, tente de novo" em vez de "sua requisição está errada". Um 400 ou um 401 nunca é repetido, porque enviar a mesma requisição quebrada mais quatro vezes não ajuda ninguém. Falhas no nível da conexão, como um socket recusado ou um tempo limite de leitura, também são repetidas.

O mesmo objeto está em TsgcHTTPAPI_client, em sgcHTTP_API, que é a classe base de todo cliente REST pronto para uso da biblioteca. Assim a política de novas tentativas está disponível em todos eles sem nenhuma ligação por cliente:

// 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: deixe o servidor ditar o ritmo

Calcular o seu próprio recuo é um chute. Quando um servidor devolve 429 ou 503 ele muitas vezes diz exatamente quanto tempo esperar, no cabeçalho de resposta Retry-After, seja como um número de segundos, seja como uma data HTTP. Esse número não é uma sugestão. É o servidor dizendo quando vai aceitar você de novo, e um cliente que o ignora e tenta de novo no seu próprio horário simplesmente vai ser rejeitado outra vez.

HonorRetryAfter é True por padrão. Quando a resposta traz um cabeçalho Retry-After, esse valor prevalece sobre o recuo calculado, e o cliente espera o tempo que o servidor pediu:

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.

As duas formas do cabeçalho são interpretadas, a forma de segundos de diferença e a forma de data HTTP. Isso é exatamente o que a OpenAI e a Anthropic pedem que seus clientes façam quando devolvem um 429, e é a diferença entre um cliente que se recupera de um limite de taxa e um cliente que continua limitado.

Novas tentativas nos clientes de IA

Os clientes de IA são onde as novas tentativas valem o seu preço, porque ali os limites de taxa são rotina em vez de exceção. Cada um deles expõe o seu próprio objeto de opções de nova tentativa na sua propriedade *Options, então as configurações ficam visíveis no Object Inspector ao lado do modelo e da chave de API.

Em TsgcHTTP_API_OpenAI o caminho é OpenAIOptions.RetryOptions, da 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;

A Anthropic tem o mesmo formato em AnthropicOptions.RetryOptions, e o Gemini em GeminiOptions.RetryOptions. O mesmo padrão existe nos clientes Grok, Mistral, DeepSeek e Ollama, e nos clientes Stripe e Paddle, onde uma chamada de pagamento repetida é um tipo de importância bem diferente.

// 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;

Note que os nomes das propriedades nos objetos de IA são Retries e Wait em vez de MaxRetries e InitialInterval, para manter a consistência com a nomenclatura já usada em outras partes desses componentes. O comportamento é idêntico, e internamente eles configuram o mesmo TsgcIdHTTPRetry_Options no cliente HTTP subjacente.

Saber por que falhou: Connect e LastError

Um laço de novas tentativas é tão bom quanto o seu tratamento de erros. Antes de poder decidir se uma falha vale uma nova tentativa, você precisa saber qual foi a falha, e até agora a única maneira de descobrir isso em um cliente WebSocket era ligar um manipulador OnError e guardar a mensagem em algum campo.

A 2026.7 adiciona uma sobrecarga de Connect que entrega o erro diretamente, em sgcWebSocket_Client_Base:

function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;

Assim um laço manual de reconexão pode inspecionar o motivo e decidir por conta própria:

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;

O último erro também é mantido no próprio componente. TsgcWSComponent_Base, em sgcWebSocket_Classes, publica uma propriedade somente leitura LastError: string e um método ClearLastError, então você pode ler o motivo depois do fato e reiniciá-lo antes de uma tentativa que queira medir:

sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
  ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);

Nada disso substitui o OnError, que continua disparando como sempre. Apenas significa que um trecho de código simples e síncrono não precisa mais montar um manipulador de eventos para responder a uma pergunta simples.

Disponibilidade

O recuo do WatchDog, o jitter, as novas tentativas HTTP e a nova sobrecarga de Connect estão disponíveis a partir do sgcWebSockets 2026.7 em Delphi 7 até 13 e C++Builder, em Win32/Win64, Linux64, macOS, Android e iOS. Tudo o que foi descrito aqui está desativado por padrão. Backoff é wdbFixed, Jitter é 0 e RetryOptions.Enabled é False, de modo que uma aplicação existente é atualizada sem nenhuma mudança de comportamento e você habilita o que fizer sentido.

Clientes com assinatura ativa podem baixar a nova versão na área de clientes. Usuários de avaliação podem obter o instalador atualizado em esegece.com/products/websockets/download.

Dúvidas, comentários ou ajuda para escolher um perfil de recuo para a sua frota? Entre em contato, você receberá uma resposta das pessoas que escreveram o código.