💻 Exemplos: Padrões Enterprise: Resiliência e Microsserviços
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')]