Retry Pattern em Zig
O padrão Retry automatiza a repetição de operações que falharam, com estratégias inteligentes de espera (backoff) para evitar sobrecarga. Em Zig, implementamos esse padrão com generics, comptime e controle explícito de timing.
Quando Usar
- Chamadas de rede que podem falhar temporariamente
- Acesso a recursos compartilhados com contenção
- Operações de I/O em dispositivos que podem estar ocupados
- Interação com APIs rate-limited
Implementação com Backoff Exponencial
const std = @import("std");
fn RetryConfig(comptime T: type, comptime E: type) type {
return struct {
const Self = @This();
max_tentativas: u32 = 3,
delay_inicial_ms: u64 = 100,
delay_maximo_ms: u64 = 30_000,
multiplicador: f64 = 2.0,
com_jitter: bool = true,
pub fn executar(self: Self, operacao: *const fn () E!T) E!T {
var tentativa: u32 = 0;
var delay_ms = self.delay_inicial_ms;
while (tentativa < self.max_tentativas) : (tentativa += 1) {
if (operacao()) |resultado| {
if (tentativa > 0) {
std.debug.print("[RETRY] Sucesso na tentativa {d}\n", .{tentativa + 1});
}
return resultado;
} else |err| {
if (tentativa + 1 >= self.max_tentativas) {
std.debug.print("[RETRY] Falha final após {d} tentativas\n", .{tentativa + 1});
return err;
}
std.debug.print("[RETRY] Tentativa {d} falhou, esperando {d}ms\n", .{
tentativa + 1, delay_ms,
});
// Esperar com backoff
var delay_real = delay_ms;
if (self.com_jitter) {
// Adicionar jitter aleatório (0-50% do delay)
var prng = std.Random.DefaultPrng.init(@intCast(std.time.nanoTimestamp()));
const jitter = prng.random().intRangeAtMost(u64, 0, delay_ms / 2);
delay_real += jitter;
}
std.time.sleep(delay_real * std.time.ns_per_ms);
// Aumentar delay para próxima tentativa
delay_ms = @min(
@as(u64, @intFromFloat(@as(f64, @floatFromInt(delay_ms)) * self.multiplicador)),
self.delay_maximo_ms,
);
}
}
unreachable;
}
};
}
// Uso
var contador_falhas: u32 = 0;
fn operacaoInstavel() ![]const u8 {
contador_falhas += 1;
if (contador_falhas < 3) return error.TemporariamenteIndisponivel;
return "dados recebidos";
}
pub fn main() !void {
const config = RetryConfig([]const u8, anyerror){
.max_tentativas = 5,
.delay_inicial_ms = 100,
.multiplicador = 2.0,
};
const resultado = try config.executar(operacaoInstavel);
std.debug.print("Resultado: {s}\n", .{resultado});
}
Retry com Filtro de Erros
const std = @import("std");
fn retrySeRetentavel(
comptime T: type,
operacao: anytype,
args: anytype,
max_tentativas: u32,
) !T {
var tentativa: u32 = 0;
while (tentativa < max_tentativas) : (tentativa += 1) {
if (@call(.auto, operacao, args)) |resultado| {
return resultado;
} else |err| {
// Só retentar erros temporários
switch (err) {
error.Timeout,
error.ConexaoRecusada,
error.ServicoIndisponivel,
=> {
if (tentativa + 1 < max_tentativas) {
const delay = std.math.shl(u64, 100, @intCast(tentativa));
std.time.sleep(delay * std.time.ns_per_ms);
continue;
}
},
// Erros permanentes — não retentar
else => return err,
}
return err;
}
}
unreachable;
}
Retry com Deadline
Em vez de limitar pelo número de tentativas, você pode limitar pelo tempo total máximo:
const std = @import("std");
fn retryComDeadline(
comptime T: type,
operacao: *const fn () anyerror!T,
deadline_ns: i128,
delay_inicial_ms: u64,
) !T {
var delay_ms = delay_inicial_ms;
while (true) {
if (operacao()) |resultado| return resultado else |_| {}
const agora = std.time.nanoTimestamp();
if (agora >= deadline_ns) return error.DeadlineExcedido;
// Calcular quanto tempo resta
const restante_ns = deadline_ns - agora;
const delay_ns = delay_ms * std.time.ns_per_ms;
if (delay_ns >= @as(u64, @intCast(restante_ns))) return error.DeadlineExcedido;
std.time.sleep(delay_ns);
delay_ms = @min(delay_ms * 2, 10_000); // backoff com cap de 10s
}
}
pub fn main() !void {
const deadline = std.time.nanoTimestamp() + 30 * std.time.ns_per_s; // 30s
const resultado = try retryComDeadline([]const u8, minhaOperacao, deadline, 100);
_ = resultado;
}
fn minhaOperacao() ![]const u8 {
return error.Falhou;
}
Considerações de Performance
std.time.sleepé uma syscall: em sistemas com muitas retentativas simultâneas, considere agendar as retentativas em uma fila de eventos em vez de bloquear uma thread por operação.- Jitter é essencial em sistemas distribuídos: sem jitter, todos os clientes que falharam ao mesmo tempo vão retentar ao mesmo tempo, causando uma nova onda de falhas no serviço. O jitter distribui as retentativas aleatoriamente no tempo.
- Backoff exponencial com cap: sem cap (
delay_maximo_ms), o delay pode crescer para horas após muitas falhas. Sempre limite o delay máximo a um valor razoável para o seu caso de uso (tipicamente 30s a 5min). - Evite retentar se a operação não for idempotente: se a operação pode ter efeitos colaterais (débito em conta, envio de email), um retry pode executar a operação mais de uma vez. Use idempotency keys ou verifique o estado antes de retentar.
Erros Comuns
Retentar todos os erros indiscriminadamente: error.AutenticacaoFalhou e error.PermissaoNegada nunca vão se resolver com mais tentativas. Filtre os erros retentáveis explicitamente com switch, como demonstrado no exemplo retrySeRetentavel.
Não logar as tentativas em produção: sem logs, você não tem visibilidade de quantas retentativas estão acontecendo. Isso pode mascarar problemas sérios — um sistema que retenta 4 vezes por requisição está efetivamente recebendo 5x a carga esperada.
Compartilhar contador de falhas entre requests: var contador_falhas: u32 = 0 como variável global faz com que o estado de uma operação afete outras. Mantenha o estado de retry local a cada invocação.
Perguntas Frequentes
Qual é a fórmula correta para backoff exponencial com jitter?
delay = min(cap, base * 2^tentativa) + random(0, base * 2^tentativa / 2). A parte fixa garante espaçamento mínimo entre tentativas; o jitter distribui as retentativas no tempo.
Devo usar Retry ou Circuit Breaker? Use ambos em camadas. O Retry lida com falhas esporádicas (1-2 tentativas antes de sucesso). O Circuit Breaker detecta quando o serviço está completamente fora do ar e para de retentar por um período, evitando sobrecarga.
Como testar o comportamento de retry sem esperar os sleeps?
Injete uma função de sleep como dependência: sleepFn: *const fn (u64) void. Em testes, passe uma função que apenas conta o número de sleeps sem realmente esperar. Isso torna os testes de retry rápidos e determinísticos.
Quando Evitar
- Erros que não são temporários (autenticação, permissão, dados inválidos)
- Operações não idempotentes (pagamentos, envio de email)
- Quando o serviço está claramente offline (use Circuit Breaker)
- Loops infinitos de retry sem backoff
Veja Também
- Circuit Breaker — Parar de retentar quando há muitas falhas
- Error Handling — Tratamento de erros em Zig
- Strategy — Diferentes estratégias de backoff
- Concorrência — Retry com timeout
- FAQ Produção — Resiliência em sistemas reais