Cheatsheet: Circuit Breaker em Zig

Circuit Breaker em Zig

O padrão Circuit Breaker protege um sistema contra falhas em cascata, funcionando como um disjuntor elétrico. Quando um serviço externo apresenta falhas repetidas, o circuit breaker “abre”, impedindo novas tentativas e falhando rapidamente. Após um período de espera, permite tentativas limitadas para verificar se o serviço se recuperou.

Quando Usar

  • Chamadas a serviços externos (APIs, bancos de dados)
  • Operações de rede que podem falhar
  • Proteção contra sobrecarga de serviços em recuperação
  • Microservices e sistemas distribuídos

Implementação

const std = @import("std");

fn CircuitBreaker(comptime T: type, comptime E: type) type {
    return struct {
        const Self = @This();

        const Estado = enum {
            fechado,    // operação normal
            aberto,     // falhas demais, bloqueando chamadas
            semi_aberto, // testando se serviço se recuperou
        };

        estado: Estado = .fechado,
        falhas_consecutivas: u32 = 0,
        limite_falhas: u32,
        tempo_timeout_ns: u64,
        ultimo_falha_ns: i128 = 0,
        sucessos_semi_aberto: u32 = 0,
        sucessos_necessarios: u32 = 2,

        pub fn init(limite_falhas: u32, timeout_segundos: u32) Self {
            return .{
                .limite_falhas = limite_falhas,
                .tempo_timeout_ns = @as(u64, timeout_segundos) * std.time.ns_per_s,
            };
        }

        pub fn executar(self: *Self, operacao: *const fn () E!T) E!T {
            switch (self.estado) {
                .aberto => {
                    const agora = std.time.nanoTimestamp();
                    if (agora - self.ultimo_falha_ns > self.tempo_timeout_ns) {
                        // Timeout expirou — tentar semi-aberto
                        self.estado = .semi_aberto;
                        self.sucessos_semi_aberto = 0;
                    } else {
                        return error.CircuitoBloqueado;
                    }
                },
                .fechado, .semi_aberto => {},
            }

            const resultado = operacao() catch |err| {
                self.registrarFalha();
                return err;
            };

            self.registrarSucesso();
            return resultado;
        }

        fn registrarFalha(self: *Self) void {
            self.falhas_consecutivas += 1;
            self.ultimo_falha_ns = std.time.nanoTimestamp();

            if (self.estado == .semi_aberto or
                self.falhas_consecutivas >= self.limite_falhas)
            {
                self.estado = .aberto;
                std.debug.print("[CIRCUIT BREAKER] Circuito ABERTO após {d} falhas\n", .{
                    self.falhas_consecutivas,
                });
            }
        }

        fn registrarSucesso(self: *Self) void {
            switch (self.estado) {
                .semi_aberto => {
                    self.sucessos_semi_aberto += 1;
                    if (self.sucessos_semi_aberto >= self.sucessos_necessarios) {
                        self.estado = .fechado;
                        self.falhas_consecutivas = 0;
                        std.debug.print("[CIRCUIT BREAKER] Circuito FECHADO — serviço recuperado\n", .{});
                    }
                },
                .fechado => {
                    self.falhas_consecutivas = 0;
                },
                .aberto => {},
            }
        }

        pub fn estadoAtual(self: *const Self) []const u8 {
            return switch (self.estado) {
                .fechado => "FECHADO (operando normalmente)",
                .aberto => "ABERTO (bloqueando chamadas)",
                .semi_aberto => "SEMI-ABERTO (testando recuperação)",
            };
        }
    };
}

Uso Prático

const CB = CircuitBreaker([]const u8, anyerror);

var tentativas: u32 = 0;

fn chamarServico() ![]const u8 {
    tentativas += 1;
    if (tentativas < 8) return error.ServicoIndisponivel;
    return "resposta ok";
}

pub fn main() void {
    var cb = CB.init(3, 5); // 3 falhas para abrir, 5s timeout

    for (0..10) |i| {
        if (cb.executar(chamarServico)) |resp| {
            std.debug.print("Tentativa {d}: {s}\n", .{ i, resp });
        } else |err| {
            std.debug.print("Tentativa {d}: erro {}\n", .{ i, err });
        }
        std.debug.print("  Estado: {s}\n", .{cb.estadoAtual()});
    }
}

Integrando com Métricas e Observabilidade

Em produção, o circuit breaker deve expor métricas para monitoramento. Uma extensão prática é adicionar callbacks de estado:

const CircuitoBreakerComMetricas = struct {
    cb: CircuitBreaker([]const u8, anyerror),
    total_chamadas: u64 = 0,
    total_bloqueadas: u64 = 0,
    total_falhas: u64 = 0,

    pub fn executar(self: *@This(), operacao: anytype) ![]const u8 {
        self.total_chamadas += 1;
        return self.cb.executar(operacao) catch |err| {
            if (err == error.CircuitoBloqueado) {
                self.total_bloqueadas += 1;
            } else {
                self.total_falhas += 1;
            }
            return err;
        };
    }

    pub fn taxaBloqueio(self: *const @This()) f64 {
        if (self.total_chamadas == 0) return 0;
        return @as(f64, @floatFromInt(self.total_bloqueadas)) /
               @as(f64, @floatFromInt(self.total_chamadas));
    }
};

Considerações de Performance

  • Overhead mínimo no caminho feliz: quando o circuito está fechado, a única operação extra é incrementar o contador de falhas após cada sucesso — custo desprezível.
  • Estado compartilhado entre threads: se múltiplas threads chamam o mesmo circuit breaker, use std.Thread.Mutex para proteger os campos falhas_consecutivas e estado. Sem isso, leituras e escritas concorrentes geram comportamento indefinido.
  • std.time.nanoTimestamp() é uma syscall: em ambientes de altíssima performance, considere checar o timestamp apenas uma vez por batch de chamadas, não a cada invocação.
  • Evite alocar no caminho de erro: o circuit breaker deve ser livre de alocações — seu valor está justamente em falhar rápido sem overhead adicional.

Erros Comuns

Não diferenciar erros retentáveis de erros permanentes: o circuit breaker deve abrir apenas para falhas que indicam indisponibilidade do serviço (error.Timeout, error.ConexaoRecusada), não para erros de negócio (error.UsuarioNaoEncontrado). Mapeie cuidadosamente quais erros contam como “falha”.

Timeout muito curto no estado semi-aberto: se o timeout for menor que o tempo de recuperação real do serviço, o circuito vai abrir e fechar repetidamente, sem deixar o serviço se estabilizar.

Usar um único circuit breaker global para múltiplos serviços: cada serviço externo deve ter seu próprio circuit breaker. Se o banco de dados falhar, isso não deve bloquear chamadas para a API de pagamentos.

Perguntas Frequentes

Quantas falhas devo usar como limite antes de abrir o circuito? Depende do perfil de tráfego. Para serviços com baixo volume (< 10 req/s), 3 a 5 falhas consecutivas é razoável. Para alto volume, use uma taxa de falha percentual (ex: >50% em uma janela de 10 segundos) em vez de contagem absoluta.

Qual é a diferença entre Circuit Breaker e Retry Pattern? O Retry retenta imediatamente ou com backoff — é adequado para falhas ocasionais. O Circuit Breaker para de tentar completamente por um período — é adequado quando o serviço está claramente indisponível. Use os dois juntos: Retry para falhas esporádicas, Circuit Breaker para indisponibilidade prolongada.

Como testar o circuit breaker sem um serviço real falhando? Injete a operação como ponteiro de função (como no exemplo acima) e passe uma função de teste que falha nas N primeiras chamadas. Use testing.allocator para verificar que não há leaks durante os ciclos de abertura/fechamento.

Quando Evitar

  • Operações locais que não falham por indisponibilidade de serviço
  • Quando falhas são esperadas e tratadas individualmente
  • Sistemas simples sem dependências externas

Veja Também

Continue aprendendo Zig

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