Cheatsheet: Retry Pattern em Zig

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.