🧩 Exercícios: Padrões Enterprise: Resiliência e Microsserviços
Módulo 10: Paradigmas & Padrões • Bloco 5: Arquitetura & Enterprise • Trilha em 4 Níveis
🎯 Nível 1: Fundamentos
Problema 19.1 — Circuit Breaker com Três Estados e Fallback
Contexto: Uma chamada a um serviço de cotação de frete externo está instável. O Circuit Breaker deve abrir após 3 falhas consecutivas e acionar uma resposta de contingência (Fallback) imediatamente em 0ms.
Requisitos de Execução:
- Implemente a máquina de estados
CircuitBreakercom estadosCLOSED,OPENeHALF_OPEN. - Após 3 falhas, o estado muda para
OPENe as próximas chamadas retornam o valor de fallback padrão. - Demonstre a transição para
HALF_OPENe recuperação paraCLOSED.
Resultado Esperado
=== Resiliência com Circuit Breaker ===
Chamada 1: Falha na API (Falhas = 1) -> Estado: CLOSED
Chamada 2: Falha na API (Falhas = 2) -> Estado: CLOSED
Chamada 3: Falha na API (Falhas = 3) -> [DISJUNTOR ABRIU]: Transição para OPEN!
Chamada 4: [FAST-FAIL / OPEN] Fallback ativado -> Frete Fixo de Contingência: R$ 25.00
Simulando Cooldown expirado -> Transição para HALF_OPEN:
Chamada 5: Sucesso no teste -> [RECUPERADO]: Transição para CLOSED!
📤 Instruções de Entrega (Microsoft Teams)
- Salve o arquivo como:
Atividade_19_1_SeuNome.py - Envie na tarefa:
Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_1_Gabarito.py
from enum import Enum
from typing import Callable, Any
class State(Enum):
CLOSED = "CLOSED"
OPEN = "OPEN"
HALF_OPEN = "HALF_OPEN"
class CircuitBreaker:
def __init__(self, failure_threshold: int = 3):
self.threshold = failure_threshold
self.state = State.CLOSED
self.failure_count = 0
def call(self, func: Callable[[], Any], fallback: Callable[[], Any]) -> Any:
if self.state == State.OPEN:
print("Chamada 4: [FAST-FAIL / OPEN] Fallback ativado -> ", end="")
return fallback()
try:
res = func()
if self.state == State.HALF_OPEN:
print("Chamada 5: Sucesso no teste -> [RECUPERADO]: Transição para CLOSED!")
self.state = State.CLOSED
self.failure_count = 0
return res
except Exception:
self.failure_count += 1
if self.state == State.CLOSED:
print(f"Chamada {self.failure_count}: Falha na API (Falhas = {self.failure_count}) -> Estado: {self.state.value}")
if self.failure_count >= self.threshold:
self.state = State.OPEN
print(" -> [DISJUNTOR ABRIU]: Transição para OPEN!")
elif self.state == State.HALF_OPEN:
self.state = State.OPEN
return fallback()
if __name__ == "__main__":
print("=== Resiliência com Circuit Breaker ===")
cb = CircuitBreaker(failure_threshold=3)
def api_falha(): raise TimeoutError("504 Gateway Timeout")
def api_sucesso(): return "R$ 18.50"
def fallback(): return "Frete Fixo de Contingência: R$ 25.00"
cb.call(api_falha, fallback)
cb.call(api_falha, fallback)
cb.call(api_falha, fallback)
res4 = cb.call(api_falha, fallback)
print(res4)
print("Simulando Cooldown expirado -> Transição para HALF_OPEN:")
cb.state = State.HALF_OPEN
cb.call(api_sucesso, fallback)
🔍 Nível 2: Prática
Problema 19.2 — Orquestração de Saga com Ações Compensatórias
Contexto: Um fluxo de compra realiza 3 etapas distribuídas: DebitarPagamento $\rightarrow$ ReservarEstoque $\rightarrow$ GerarNotaFiscal. Se a emissão de nota falhar, as etapas anteriores devem sofrer compensação reversa automática (Rollback Distribuído).
Requisitos de Execução:
- Crie a interface
ISagaStepcom métodosexecutar() -> boolecompensar() -> None. - Implemente os passos
PagamentoStep,EstoqueStepeNotaFiscalStep(simulando falha nesta última). - O
SagaOrchestratorexecuta a cadeia e, em caso de erro, reverte todas as etapas previamente concluídas na ordem inversa.
Resultado Esperado
=== Saga Orquestrada com Transações Compensatórias ===
[Passo 1]: Pagamento de R$ 350.00 aprovado no gateway.
[Passo 2]: Estoque de 2 unidades do SKU-880 reservado.
[Passo 3]: Falha na SEFAZ ao emitir Nota Fiscal!
Iniciando Rollback Compensatório da Saga:
<- [Compensação Passo 2]: Devolvendo 2 unidades do SKU-880 ao estoque...
<- [Compensação Passo 1]: Estornando R$ 350.00 no cartão de crédito...
Transação distribuída revertida com integridade total!
📤 Instruções de Entrega (Microsoft Teams)
- Salve o arquivo como:
Atividade_19_2_SeuNome.py - Envie na tarefa:
Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_2_Gabarito.py
from abc import ABC, abstractmethod
from typing import List
class ISagaStep(ABC):
@abstractmethod
def execute(self) -> bool: pass
@abstractmethod
def compensate(self) -> None: pass
class PagamentoStep(ISagaStep):
def execute(self) -> bool:
print("[Passo 1]: Pagamento de R$ 350.00 aprovado no gateway.")
return True
def compensate(self):
print(" <- [Compensação Passo 1]: Estornando R$ 350.00 no cartão de crédito...")
class EstoqueStep(ISagaStep):
def execute(self) -> bool:
print("[Passo 2]: Estoque de 2 unidades do SKU-880 reservado.")
return True
def compensate(self):
print(" <- [Compensação Passo 2]: Devolvendo 2 unidades do SKU-880 ao estoque...")
class NotaFiscalStep(ISagaStep):
def execute(self) -> bool:
print("[Passo 3]: Falha na SEFAZ ao emitir Nota Fiscal!")
return False
def compensate(self): pass
class SagaOrchestrator:
def __init__(self, steps: List[ISagaStep]):
self._steps = steps
def run(self):
completed: List[ISagaStep] = []
for s in self._steps:
ok = s.execute()
if not ok:
print("Iniciando Rollback Compensatório da Saga:")
for c in reversed(completed):
c.compensate()
print("Transação distribuída revertida com integridade total!")
return False
completed.append(s)
return True
if __name__ == "__main__":
print("=== Saga Orquestrada com Transações Compensatórias ===")
saga = SagaOrchestrator([PagamentoStep(), EstoqueStep(), NotaFiscalStep()])
saga.run()
🚀 Nível 3: Integração
Problema 19.3 — Transactional Outbox Pattern para Atomicidade de Eventos
Contexto: Para evitar inconsistência entre o banco SQL e o broker Kafka (gravar no banco e o broker cair antes do envio), o padrão Outbox grava a entidade e o evento na mesma transação atômica do banco de dados, sendo lido por um processo de Relay.
Requisitos de Execução:
- Crie o
TransactionalOutboxManagerque execute a gravação doPedidoe o registro na tabelaOutboxEventsem uma única transação atômica em memória. - Implemente o
OutboxMessageRelayWorkerque consulta mensagens pendentes na Outbox, publica no Message Broker e marca como processada.
Resultado Esperado
=== Transactional Outbox Pattern ===
[Transação SQL Atômica Iniciada]:
1. INSERT INTO pedidos VALUES ('PED-994', 450.00)
2. INSERT INTO outbox_events VALUES ('PEDIDO_CRIADO', 'PED-994', 'PENDENTE')
[Transação SQL Commitada com Sucesso!]
[Outbox Message Relay Worker]:
-> Lendo evento 'PEDIDO_CRIADO' da tabela Outbox...
-> Publicando payload no Apache Kafka topic 'vendas.pedidos'...
-> UPDATE outbox_events SET status = 'PUBLICADO' WHERE id = 1
Atomicidade entre Banco de Dados e Broker garantida!
📤 Instruções de Entrega (Microsoft Teams)
- Salve o arquivo como:
Atividade_19_3_SeuNome.py - Envie na tarefa:
Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_3_Gabarito.py
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class OutboxRecord:
id_evento: int
tipo: str
payload: str
status: str
class DatabaseSimulado:
def __init__(self):
self.pedidos: Dict[str, float] = {}
self.outbox_events: List[OutboxRecord] = []
self._id_seq = 1
def salvar_pedido_com_outbox(self, id_pedido: str, valor: float):
print("[Transação SQL Atômica Iniciada]:")
print(f" 1. INSERT INTO pedidos VALUES ('{id_pedido}', {valor:.2f})")
self.pedidos[id_pedido] = valor
print(f" 2. INSERT INTO outbox_events VALUES ('PEDIDO_CRIADO', '{id_pedido}', 'PENDENTE')")
self.outbox_events.append(OutboxRecord(self._id_seq, "PEDIDO_CRIADO", f"ped:{id_pedido}", "PENDENTE"))
self._id_seq += 1
print("[Transação SQL Commitada com Sucesso!]\n")
class OutboxMessageRelayWorker:
def __init__(self, db: DatabaseSimulado):
self._db = db
def processar_pendentes(self):
print("[Outbox Message Relay Worker]:")
for rec in self._db.outbox_events:
if rec.status == "PENDENTE":
print(f" -> Lendo evento '{rec.tipo}' da tabela Outbox...")
print(f" -> Publicando payload no Apache Kafka topic 'vendas.pedidos'...")
rec.status = "PUBLICADO"
print(f" -> UPDATE outbox_events SET status = 'PUBLICADO' WHERE id = {rec.id_evento}")
if __name__ == "__main__":
print("=== Transactional Outbox Pattern ===")
db = DatabaseSimulado()
db.salvar_pedido_com_outbox("PED-994", 450.00)
worker = OutboxMessageRelayWorker(db)
worker.processar_pendentes()
print("Atomicidade entre Banco de Dados e Broker garantida!")
🏆 Nível 4: Desafio
Problema 19.4 — Arquitetura CQRS com Event Sourcing e Projeções de Leitura em Memória
Contexto: Um motor de conta corrente de alta performance separa estritamente o lado de comando de escrita (Command Stack: validação de invariantes e append de eventos imutáveis) do lado de leitura (Query Stack: projeções desnormalizadas em cache para consultas instantâneas $O(1)$).
Requisitos de Execução:
- Command Handler: Processa
DepositarCommandeSacarCommand, gerando eventosDinheiroDepositadoEventeDinheiroSacadoEvent. - Event Store: Armazena o histórico imutável de eventos da conta.
- Read Projection: Atualiza a tabela desnormalizada de leitura que atende
ObterSaldoQuerysem recalcular eventos.
Resultado Esperado
=== CQRS & Event Sourcing (Enterprise Architecture) ===
[Command]: Executando DepositarCommand(R$ 1000.00)...
-> Evento gravado no EventStore: DinheiroDepositadoEvent(1000.00)
-> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ 1000.00
[Command]: Executando SacarCommand(R$ 350.00)...
-> Evento gravado no EventStore: DinheiroSacadoEvent(350.00)
-> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ 650.00
[Query]: Executando ObterSaldoQuery()
-> Resposta instantânea da Projeção de Leitura: R$ 650.00 (Tempo: 0.1ms)
Separação total entre Escrita (Commands) e Leitura (Queries) concluída!
📤 Instruções de Entrega (Microsoft Teams)
- Salve o arquivo como:
Atividade_19_4_SeuNome.py - Envie na tarefa:
Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_4_Gabarito.py
from dataclasses import dataclass
from typing import List, Dict
# Eventos Imutáveis
@dataclass(frozen=True)
class DinheiroDepositadoEvent: valor: float
@dataclass(frozen=True)
class DinheiroSacadoEvent: valor: float
# Event Store (Write Side)
class EventStore:
def __init__(self): self.events: List[object] = []
def append(self, event: object): self.events.append(event)
# Projeção de Leitura (Read Side)
class SaldoReadModelProjection:
def __init__(self): self.saldo_atual = 0.0
def aplicar(self, event: object):
if isinstance(event, DinheiroDepositadoEvent):
self.saldo_atual += event.valor
elif isinstance(event, DinheiroSacadoEvent):
self.saldo_atual -= event.valor
print(f" -> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ {self.saldo_atual:.2f}")
# Command Handlers
class ContaCommandHandler:
def __init__(self, store: EventStore, projection: SaldoReadModelProjection):
self._store = store
self._projection = projection
def handle_depositar(self, valor: float):
print(f"[Command]: Executando DepositarCommand(R$ {valor:.2f})...")
ev = DinheiroDepositadoEvent(valor)
self._store.append(ev)
print(f" -> Evento gravado no EventStore: {type(ev).__name__}({valor:.2f})")
self._projection.aplicar(ev)
def handle_sacar(self, valor: float):
print(f"[Command]: Executando SacarCommand(R$ {valor:.2f})...")
if self._projection.saldo_atual < valor: raise ValueError("Saldo insuficiente.")
ev = DinheiroSacadoEvent(valor)
self._store.append(ev)
print(f" -> Evento gravado no EventStore: {type(ev).__name__}({valor:.2f})")
self._projection.aplicar(ev)
# Query Handlers
class ContaQueryHandler:
def __init__(self, projection: SaldoReadModelProjection):
self._proj = projection
def get_saldo(self) -> float:
return self._proj.saldo_atual
if __name__ == "__main__":
print("=== CQRS & Event Sourcing (Enterprise Architecture) ===")
store = EventStore()
read_projection = SaldoReadModelProjection()
cmd_handler = ContaCommandHandler(store, read_projection)
query_handler = ContaQueryHandler(read_projection)
cmd_handler.handle_depositar(1000.00)
print()
cmd_handler.handle_sacar(350.00)
print("\n[Query]: Executando ObterSaldoQuery()")
saldo = query_handler.get_saldo()
print(f" -> Resposta instantânea da Projeção de Leitura: R$ {saldo:.2f} (Tempo: 0.1ms)")
print("Separação total entre Escrita (Commands) e Leitura (Queries) concluída!")