Módulo 10: Paradigmas & Padrões • Bloco 5: Arquitetura & Enterprise • Demonstrações Práticas

🔌 Resiliência Distribuída na Prática: No Exemplo 1, implementamos a máquina de estados completa do Circuit Breaker (CLOSED → OPEN → HALF-OPEN) com um relógio simulado, protegendo a aplicação de um gateway de frete instável. No Exemplo 2, orquestramos uma Saga de checkout com transações compensatórias e publicamos os eventos com segurança via Transactional Outbox em SQLite.


1. Circuit Breaker com Fallback e Relógio Simulado

Depois de 3 falhas seguidas, o disjuntor abre e passa a responder com fast-fail + fallback (frete padrão), sem gastar threads esperando timeout. Após o cooldown de 30s, uma única chamada de teste decide o destino: falhou (pedido #7), reabre; funcionou (pedido #8), fecha. O relógio simulado deixa o exemplo determinístico — em produção, use time.monotonic() ou bibliotecas como Resilience4j e Polly.

# ent_circuit_breaker.py
from enum import Enum
from typing import Callable, List


class Estado(Enum):
    FECHADO = "CLOSED"
    ABERTO = "OPEN"
    SEMI_ABERTO = "HALF-OPEN"


class CircuitoAbertoErro(Exception):
    pass


class RelogioSimulado:
    """Relógio controlável: torna o exemplo determinístico (em produção, time.monotonic)."""
    def __init__(self):
        self.agora = 0.0

    def avancar(self, segundos: float):
        self.agora += segundos


class CircuitBreaker:
    def __init__(self, limite_falhas: int, espera_s: float, relogio: RelogioSimulado):
        self.estado = Estado.FECHADO
        self._limite, self._espera, self._relogio = limite_falhas, espera_s, relogio
        self._falhas_seguidas = 0
        self._aberto_em = 0.0

    def chamar(self, operacao: Callable[[], str]) -> str:
        if self.estado is Estado.ABERTO:
            if self._relogio.agora - self._aberto_em < self._espera:
                raise CircuitoAbertoErro("fast-fail: circuito aberto, downstream não foi chamado")
            self._mudar(Estado.SEMI_ABERTO)  # cooldown expirou: deixa passar 1 chamada de teste
        try:
            resultado = operacao()
        except ConnectionError:
            self._registrar_falha()
            raise
        self._registrar_sucesso()
        return resultado

    def _registrar_falha(self):
        self._falhas_seguidas += 1
        if self.estado is Estado.SEMI_ABERTO or self._falhas_seguidas >= self._limite:
            self._aberto_em = self._relogio.agora
            self._mudar(Estado.ABERTO)

    def _registrar_sucesso(self):
        self._falhas_seguidas = 0
        if self.estado is not Estado.FECHADO:
            self._mudar(Estado.FECHADO)

    def _mudar(self, novo: Estado):
        print(f"      ⚡ transição {self.estado.value} -> {novo.value}")
        self.estado = novo


class GatewayFrete:
    """Serviço externo instável: responde conforme um roteiro de disponibilidade."""
    def __init__(self, roteiro: List[bool]):
        self._roteiro, self.chamadas_recebidas = roteiro, 0

    def cotar(self) -> str:
        self.chamadas_recebidas += 1
        if not self._roteiro.pop(0):
            raise ConnectionError("timeout após 3s")
        return "frete R$ 19,90"


if __name__ == "__main__":
    print("=" * 65)
    print("🔌 CIRCUIT BREAKER: CLOSED -> OPEN -> HALF-OPEN -> CLOSED")
    print("=" * 65)
    relogio = RelogioSimulado()
    gateway = GatewayFrete([True, False, False, False, False, True])
    disjuntor = CircuitBreaker(limite_falhas=3, espera_s=30, relogio=relogio)

    def cotar_com_fallback(rotulo: str):
        print(f"  t={relogio.agora:>3.0f}s {rotulo}")
        try:
            resposta = disjuntor.chamar(gateway.cotar)
        except CircuitoAbertoErro as erro:
            resposta = f"fallback: frete padrão R$ 25,00 ({erro})"
        except ConnectionError as erro:
            resposta = f"falha: {erro}"
        print(f"      [{disjuntor.estado.value:<9}] {resposta}")

    cotar_com_fallback("pedido #1")
    for n in range(2, 5):
        cotar_com_fallback(f"pedido #{n}")
    relogio.avancar(5)
    cotar_com_fallback("pedido #5")
    cotar_com_fallback("pedido #6")
    relogio.avancar(30)
    cotar_com_fallback("pedido #7")  # teste no HALF-OPEN falha: reabre
    relogio.avancar(31)
    cotar_com_fallback("pedido #8")  # novo teste funciona: fecha

    print(f"\nChamadas que chegaram ao gateway: {gateway.chamadas_recebidas} de 8 pedidos"
          f" (as demais foram protegidas pelo fast-fail)")

2. Saga Orquestrada com Compensações + Transactional Outbox

Cada Passo da Saga carrega sua ação e sua compensação; ao falhar a reserva de estoque do PED-101, o orquestrador desfaz os passos concluídos em ordem inversa. Já o ServicoPedidos grava o status do pedido e o evento na mesma transação SQL — o relay publica depois, e uma segunda execução não duplica nada. É o que garante que banco e broker nunca fiquem inconsistentes.

# ent_saga_orquestrada_outbox.py
import json
import sqlite3
from dataclasses import dataclass
from typing import Callable, List


# 1. Transactional Outbox: o evento é gravado na MESMA transação do dado de negócio
class ServicoPedidos:
    def __init__(self):
        self.db = sqlite3.connect(":memory:")
        self.db.execute("CREATE TABLE pedidos (id TEXT PRIMARY KEY, status TEXT)")
        self.db.execute("CREATE TABLE outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, evento TEXT, publicado INTEGER DEFAULT 0)")

    def mudar_status(self, pedido_id: str, status: str):
        with self.db:  # commit atômico: pedido + evento, ou nenhum dos dois
            self.db.execute("INSERT OR REPLACE INTO pedidos VALUES (?, ?)", (pedido_id, status))
            evento = json.dumps({"tipo": f"Pedido{status.title()}", "pedido": pedido_id})
            self.db.execute("INSERT INTO outbox (evento) VALUES (?)", (evento,))

    def relay_publicar(self, broker: List[str]):
        """Processo separado que lê a outbox e publica no broker (Kafka/RabbitMQ)."""
        pendentes = self.db.execute("SELECT seq, evento FROM outbox WHERE publicado = 0 ORDER BY seq").fetchall()
        with self.db:
            for seq, evento in pendentes:
                broker.append(evento)
                self.db.execute("UPDATE outbox SET publicado = 1 WHERE seq = ?", (seq,))
        return len(pendentes)


# 2. Saga orquestrada: cada passo tem uma ação e uma compensação
@dataclass
class Passo:
    nome: str
    acao: Callable[[], None]
    compensacao: Callable[[], None]


class OrquestradorSaga:
    def __init__(self, passos: List[Passo]):
        self._passos = passos

    def executar(self) -> bool:
        concluidos: List[Passo] = []
        for passo in self._passos:
            try:
                passo.acao()
                print(f"    ✔ {passo.nome}")
                concluidos.append(passo)
            except RuntimeError as erro:
                print(f"    ✘ {passo.nome} falhou: {erro}")
                for feito in reversed(concluidos):  # compensa na ordem INVERSA
                    feito.compensacao()
                    print(f"    ↩ compensado: {feito.nome}")
                return False
        return True


def rodar_checkout(pedido_id: str, estoque_disponivel: int, pedidos: ServicoPedidos):
    estado = {"cartao_reservado": 0.0, "estoque": estoque_disponivel}

    def reservar_estoque():
        if estado["estoque"] < 2:
            raise RuntimeError("estoque esgotado")
        estado["estoque"] -= 2

    passos = [
        Passo("Criar pedido (PENDENTE)",
              lambda: pedidos.mudar_status(pedido_id, "pendente"),
              lambda: pedidos.mudar_status(pedido_id, "cancelado")),
        Passo("Pré-autorizar R$ 240,00 no cartão",
              lambda: estado.update(cartao_reservado=240.0),
              lambda: estado.update(cartao_reservado=0.0)),
        Passo("Reservar 2 unidades no estoque", reservar_estoque, lambda: estado.update(estoque=estado["estoque"] + 2)),
        Passo("Confirmar pedido",
              lambda: pedidos.mudar_status(pedido_id, "confirmado"),
              lambda: None),
    ]
    print(f"\n[Saga {pedido_id}] estoque disponível: {estoque_disponivel}")
    sucesso = OrquestradorSaga(passos).executar()
    print(f"  Resultado: {'CONCLUÍDA' if sucesso else 'REVERTIDA'} | cartão pré-autorizado: "
          f"R$ {estado['cartao_reservado']:.2f} | estoque final: {estado['estoque']}")


if __name__ == "__main__":
    print("=" * 65)
    print("📜 SAGA ORQUESTRADA COM COMPENSAÇÕES + TRANSACTIONAL OUTBOX")
    print("=" * 65)
    servico = ServicoPedidos()
    rodar_checkout("PED-100", estoque_disponivel=5, pedidos=servico)
    rodar_checkout("PED-101", estoque_disponivel=1, pedidos=servico)

    broker: List[str] = []
    print(f"\n[Relay da Outbox] {servico.relay_publicar(broker)} eventos publicados no broker:")
    for evento in broker:
        print(f"  📨 {evento}")
    print(f"[Relay da Outbox] nova execução publica {servico.relay_publicar(broker)} eventos (nada duplicado)")
    print(f"Status finais: {servico.db.execute('SELECT id, status FROM pedidos ORDER BY id').fetchall()}")

Saída Esperada dos Scripts:

=================================================================
🔌 CIRCUIT BREAKER: CLOSED -> OPEN -> HALF-OPEN -> CLOSED
=================================================================
  t=  0s pedido #1
      [CLOSED   ] frete R$ 19,90
  t=  0s pedido #2
      [CLOSED   ] falha: timeout após 3s
  t=  0s pedido #3
      [CLOSED   ] falha: timeout após 3s
  t=  0s pedido #4
      ⚡ transição CLOSED -> OPEN
      [OPEN     ] falha: timeout após 3s
  t=  5s pedido #5
      [OPEN     ] fallback: frete padrão R$ 25,00 (fast-fail: circuito aberto, downstream não foi chamado)
  t=  5s pedido #6
      [OPEN     ] fallback: frete padrão R$ 25,00 (fast-fail: circuito aberto, downstream não foi chamado)
  t= 35s pedido #7
      ⚡ transição OPEN -> HALF-OPEN
      ⚡ transição HALF-OPEN -> OPEN
      [OPEN     ] falha: timeout após 3s
  t= 66s pedido #8
      ⚡ transição OPEN -> HALF-OPEN
      ⚡ transição HALF-OPEN -> CLOSED
      [CLOSED   ] frete R$ 19,90

Chamadas que chegaram ao gateway: 6 de 8 pedidos (as demais foram protegidas pelo fast-fail)

=================================================================
📜 SAGA ORQUESTRADA COM COMPENSAÇÕES + TRANSACTIONAL OUTBOX
=================================================================

[Saga PED-100] estoque disponível: 5
    ✔ Criar pedido (PENDENTE)
    ✔ Pré-autorizar R$ 240,00 no cartão
    ✔ Reservar 2 unidades no estoque
    ✔ Confirmar pedido
  Resultado: CONCLUÍDA | cartão pré-autorizado: R$ 240.00 | estoque final: 3

[Saga PED-101] estoque disponível: 1
    ✔ Criar pedido (PENDENTE)
    ✔ Pré-autorizar R$ 240,00 no cartão
    ✘ Reservar 2 unidades no estoque falhou: estoque esgotado
    ↩ compensado: Pré-autorizar R$ 240,00 no cartão
    ↩ compensado: Criar pedido (PENDENTE)
  Resultado: REVERTIDA | cartão pré-autorizado: R$ 0.00 | estoque final: 1

[Relay da Outbox] 4 eventos publicados no broker:
  📨 {"tipo": "PedidoPendente", "pedido": "PED-100"}
  📨 {"tipo": "PedidoConfirmado", "pedido": "PED-100"}
  📨 {"tipo": "PedidoPendente", "pedido": "PED-101"}
  📨 {"tipo": "PedidoCancelado", "pedido": "PED-101"}
[Relay da Outbox] nova execução publica 0 eventos (nada duplicado)
Status finais: [('PED-100', 'confirmado'), ('PED-101', 'cancelado')]