Lab 79 — Consumir o modelo do .NET com fallback
O problema, e a empresa que o tem
Este laboratório continua de onde o L74 parou. A Cadência separou os quatro modos de servir o modelo de cancelamento em produção — e o painel de atendimento, onde um vendedor abre um pedido e vê a pontuação de risco ao lado, ficou no modo de tempo real: um endpoint SageMaker (ml.g4dn.xlarge) com janela de disponibilidade reduzida, escalando a zero fora do horário comercial para não pagar GPU ociosa 24 horas por dia.
O ClienteTempoReal.cs escrito naquele laboratório já tem um orçamento: Timeout = TimeSpan.FromMilliseconds(300), porque o vendedor não pode ficar com a tela travada enquanto o modelo pensa. O que o L74 deixou em aberto, de propósito, foi o que acontece quando esses 300 ms realmente se esgotam — porque naquele módulo o objetivo era medir o caminho feliz dos quatro modos, não construir o caminho de falha de nenhum deles.
O caminho de falha apareceu sozinho. O EventBridge Scheduler do L74 aquece o endpoint às 8h45, quinze minutos antes do expediente — mas um vendedor que abre o primeiro pedido do dia às 8h40, ou um pico de atendimento fora da janela prevista, ainda encontra o endpoint frio. A chamada estoura os 300 ms, o TaskCanceledException sobe sem nenhum tratamento, e a tela do pedido inteira falha — não só o campo de pontuação, que era só um dado auxiliar para o vendedor decidir se vale revisar o pedido com mais calma.
O padrão tem nome, e não é o mesmo do retry storm do L36. Ali o problema era volume: 800 invocações concorrentes multiplicando carga contra um parceiro sob pico. Aqui o volume é baixo — cerca de 200 chamadas por dia, medidas no L74 — e o problema é OUTRO: uma falha determinística e previsível (cold start fora da janela) sem NENHUM tratamento, derrubando uma tela inteira por causa de um dado que nunca deveria ser bloqueante.
O que este laboratório NÃO é
Não é sobre escolher entre os quatro modos de inferência — isso já foi decidido no L74, e o modo tempo real é o correto para o painel de atendimento porque a chamada é bloqueante por natureza. Não é sobre retry storm sob carga alta — o volume aqui é baixo, e o problema é uma falha PREVISÍVEL (cold start) sem nenhum tratamento, não uma multiplicação de tentativas como no L36. E não é sobre re-treinar ou avaliar o modelo — isso é o L73 e o L78. Este laboratório resolve uma coisa: o que o cliente .NET faz quando a chamada síncrona ao endpoint não volta a tempo.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando ou uma medição na seção de implantação, não com a sensação de ter entendido.
- Reproduzir a requisição do vendedor derrubada por inteiro quando o endpoint está frio, com o erro e o tempo medidos.
- Explicar por que retentar um TaskCanceledException contra um endpoint em cold start não ajuda, e por que retentar HttpRequestException ajuda.
- Configurar um circuit breaker calibrado pelo volume real do consumidor, não pelo volume de outro laboratório.
- Construir um caminho degradado explícito: pedido aceito, pontuação marcada como pendente, id publicado numa fila de revisão.
- Orçar o pior caso do pipeline de resiliência dentro do teto de 300 ms herdado do L74, com a conta explícita.
- Medir, com números, quanto o disjuntor economiza de espera numa rajada de chamadas durante o mesmo cold start.
- Distinguir o que este laboratório resolve (experiência do vendedor) do que o L36 resolve (proteção do downstream).
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Timeout de cliente HTTP/SDK | DVA-C02 | timeout reduzido para 220 ms, deixando margem para uma retentativa restrita dentro do orçamento de 300 ms | que timeout mal calibrado é tão defeito quanto ausência de timeout |
| Degradação graciosa (graceful degradation) | DVA-C02, SAP-C02 | a tela do pedido carrega mesmo sem a pontuação, com o dado marcado como pendente | que "funcionar parcialmente" é sempre melhor que "falhar por inteiro" quando o dado é auxiliar |
| Retry seletivo por tipo de exceção | DVA-C02 | `ShouldHandle` exclui timeout e retenta só falha de conexão | que retentar toda exceção igualmente é o antipadrão, não a regra |
| Circuit breaker aplicado à latência do cliente, não só à saúde do servidor | SAP-C02 | o disjuntor evita que cada requisição de uma rajada pague o timeout inteiro sozinha | que o valor do disjuntor aqui é poupar o CHAMADOR, não só proteger o downstream |
| Least privilege em InvokeEndpoint | DVA-C02, SAP-C02 | IAM policy restrita ao ARN do endpoint específico, não a `sagemaker:*` | que endpoint de SageMaker é recurso nomeável, e `Resource: "*"` nele é adorno de rigor |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um cliente que já tem timeout configurado e ainda assim trava a experiência do usuário sob carga intermitente, e pede a causa. A resposta esperada não é "reduzir mais o timeout" — é que falta um caminho explícito para quando o timeout estoura: sem fallback, timeout apenas troca "trava para sempre" por "falha rápido e ainda assim quebra a tela". O erro de raciocínio mais comum é tratar "tem timeout" como sinônimo de "resiliente".
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Vendedor não pode esperar mais que 300 ms pela tela, herdado do L74 | obrigatório, 300 ms | timeout por chamada reduzido para 220 ms, sobrando margem para uma retentativa restrita |
| Timeout do endpoint não pode derrubar a requisição inteira | obrigatório | try/catch ao redor de todo o pipeline Polly; nenhuma exceção chega ao controller sem decisão tomada |
| Retry só é seguro contra falha rápida de conexão, não contra timeout | obrigatório | `ShouldHandle` exclui `TaskCanceledException`; retentar um cold start com o mesmo timeout só dobra a espera |
| Vários vendedores no mesmo cold start não podem pagar 220 ms cada um | requisito operacional, medido | circuit breaker com janela curta (10 s) e `MinimumThroughput` calibrado pelo volume do vendedor, não pelo de campanha |
| Pedido não pode ficar bloqueado pela pontuação, que é auxiliar | obrigatório | caminho degradado: aceita o pedido, marca a pontuação como pendente, publica o id na fila de revisão |
| Fallback não pode virar "a pontuação sempre falha e ninguém percebe" | requisito operacional | métrica de uso do fallback publicada a cada ocorrência, com alarme quando ultrapassa o esperado de um cold start isolado |
| Endpoint SageMaker só pode ser chamado pela API de atendimento | segurança | IAM policy restrita a `sagemaker:InvokeEndpoint` no ARN do endpoint específico |
| Fila de revisão guarda referência ao pedido, não dado sensível do cliente | privacidade | mensagem carrega só o id do pedido, sem nome, documento ou valor |
Arquitetura mínima: o timeout que ninguém pega
Este é exatamente o desenho que sobrou do L74 — não um desenho ingênuo escrito de propósito para este laboratório, mas o estado real em que aquele módulo deixou o cliente. Ele é legítimo como ponto de partida: funciona perfeitamente todo dia, na janela em que o endpoint está quente. O laboratório começa medindo o que acontece fora dela.
- → abre o pedido, aguarda a tela inteira responder
- → encaminha a requisição HTTP do pedido
- → InvokeEndpointAsync, timeout de 300 ms herdado do L74, sem tratamento de exceção
- → pontuação, ou estouro do timeout após 300 ms
- → a exceção sobe sem tratamento; a API responde 500 para o pedido inteiro
- → erro genérico chega à tela, mesmo o pedido em si sendo válido
- Fora da AWS
- Rede e entrega
- Compute
- IA e machine learning
O cliente do L74 já tem um orçamento de 300 ms — e isso não é o defeito. O defeito é que, quando o orçamento se esgota, a exceção sobe sem nenhum tratamento e derruba a requisição INTEIRA do vendedor, não só o campo de pontuação. Percorra os passos e repare onde a falha deixa de ser "um dado ausente" e passa a ser "a tela inteira quebrou".
- Vendedor abre o pedido; a chamada atravessa o ALB. É o fluxo comum, sem nada de errado ainda: o navegador do vendedor faz uma requisição HTTP simples para abrir a tela de um pedido específico.
- A API repete o cliente síncrono do L74, sem nenhuma mudança. O `ServicoPontuacao` chama diretamente o `ClienteTempoReal` construído no laboratório anterior — que já tinha `Timeout = 300ms`, mas nunca precisou de mais que isso porque aquele módulo só media o caminho feliz.
- InvokeEndpointAsync corre contra um endpoint que pode estar frio. Fora da janela de aquecimento (9h–18h) ou nos minutos antes da chamada de aquecimento do EventBridge Scheduler, o endpoint está com zero instâncias — `min_instance_count = 0`, do L74.
- O cold start ultrapassa os 300 ms configurados. O SageMaker precisa subir um container antes de responder. Medido no L74, essa primeira chamada pode passar de 2,85 s — muito acima dos 300 ms que o `HttpClient`/SDK está configurado para esperar.
- A TaskCanceledException sobe sem nenhum try/catch — a API inteira falha. Não existe nenhum bloco de tratamento entre o `PontuarPedidoAsync` e o controller. A exceção do timeout se propaga pela pilha inteira, e o ASP.NET Core devolve um 500 para a requisição completa de abrir o pedido.
- O vendedor vê um erro genérico, mesmo o pedido em si sendo válido. O dado do pedido já estava carregado do banco antes da chamada ao modelo — só a pontuação, um campo auxiliar, dependia do endpoint. Mas a falta de tratamento fez a peça auxiliar derrubar a peça principal.
O número que importa não é "existe um timeout" — é o que acontece DEPOIS que ele estoura. Meça antes de mudar qualquer coisa: é o que torna o defeito discutível, em vez de uma sensação de que "às vezes a tela do pedido falha".
// ServicoPontuacao.cs (antes) — o cliente do L74, chamado sem rede de
// seguranca nenhuma. Funciona perfeitamente dentro da janela de aquecimento
// (9h as 18h); fora dela, uma unica chamada fria derruba a tela inteira do
// pedido, nao so o campo de pontuacao.
public class ServicoPontuacao
{
// ClienteTempoReal e exatamente a classe do L74: Timeout = 300ms, SEM
// retry (retry ali estouraria o orcamento de latencia do vendedor).
private readonly ClienteTempoReal _cliente = new();
public async Task<double> PontuarPedidoAsync(string payloadJson)
{
// Se InvokeEndpointAsync lancar TaskCanceledException apos os 300ms,
// ela sobe direto — nao ha nenhum catch aqui nem no controller.
return await _cliente.PontuarAsync(payloadJson);
}
}
public static class PedidosEndpoint // ASP.NET Core minimal API, simplificado
{
public static async Task<IResult> AbrirPedido(
string pedidoId, ServicoPontuacao pontuacao, PedidoRepositorio repo)
{
var pedido = await repo.CarregarAsync(pedidoId);
// A pontuacao e so um campo AUXILIAR da tela do vendedor — mas uma
// excecao aqui derruba a resposta INTEIRA, inclusive os dados do
// pedido que ja estavam prontos e carregados do banco.
pedido.PontuacaoCancelamento = await pontuacao.PontuarPedidoAsync(pedido.ParaJson());
return Results.Ok(pedido);
}
}
Timeout sem fallback não é resiliência — é só uma falha mais previsível
Configurar `Timeout = 300ms` no L74 foi necessário, mas não é suficiente: um timeout sem nenhum tratamento troca "a tela trava indefinidamente" por "a tela falha em exatos 300 ms", e nenhuma das duas experiências é aceitável para um dado que é só auxiliar. O timeout limita QUANTO tempo se espera; ele não decide o que fazer quando o tempo se esgota — essa decisão é o que este laboratório constrói.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. O endpoint e a janela de scale-to-zero são os MESMOS do L74 — o que muda é inteiramente o comportamento do cliente, e é isso que a comparação com a Figura 1 mede.
- → abre o pedido, mesma ação da Figura 1
- → encaminha a requisição HTTP do pedido
- → tentativa com timeout de 220 ms — só corre se o circuito estiver fechado
- → pontuação, falha rápida de conexão (retentada uma vez), ou timeout
- → id do pedido, publicado sempre que o fallback decide por pontuação pendente
- → publica a transição do circuito e cada uso do fallback
- → alarme quando o fallback passa do limiar numa janela de 10 minutos
- Fora da AWS
- Rede e entrega
- Compute
- IA e machine learning
- Integração de apps
- Gestão e governança
O endpoint e a janela de aquecimento são os MESMOS da Figura 1 — o que muda é que agora existe uma decisão antes, durante e depois de cada tentativa. A peça nova mais importante não é o retry: é o disjuntor decidindo, para a SEGUNDA chamada de uma rajada em diante, nem esperar o timeout de novo. Percorra os passos: cada peça nova rastreia a um requisito da tabela ao lado.
- Vendedor abre o pedido; a chamada atravessa o mesmo ALB da Figura 1. Nada muda do lado do vendedor: a URL, o método e a tela são idênticos. A diferença inteira está no que a API faz atrás do ALB.
- O pipeline Polly decide: o circuito está fechado?. Antes de qualquer chamada de rede, o `ResiliencePipeline` consulta o estado do disjuntor. Só se estiver FECHADO ou MEIO-ABERTO a chamada ao SageMaker acontece de verdade.
- Se fechado, tenta com timeout de 220 ms — reduzido para caber uma retentativa. Cortado dos 300 ms originais do L74 de propósito: com uma retentativa restrita disponível, sobra orçamento sem estourar o teto que o vendedor não pode ultrapassar.
- Falha de conexão retenta uma vez; timeout NÃO retenta — vai direto ao fallback. É a distinção central deste laboratório: `ShouldHandle` retenta `HttpRequestException` (falha rápida de rede), mas exclui `TaskCanceledException` — retentar um cold start com o MESMO timeout só dobraria a espera do vendedor sem ganhar nada.
- Circuito aberto ou tentativas esgotadas: o pedido segue com pontuação pendente. A API nunca deixa a exceção chegar ao controller. Em vez disso, aceita o pedido normalmente, marca a pontuação como pendente, e publica o id na fila de revisão para um consumidor futuro fechar o ciclo.
- Toda decisão do circuito e todo uso do fallback são publicados como métrica. Sem isso, o efeito observável de um fallback funcionando é só a AUSÊNCIA de erro — que, num painel que só mede erro 500, parece sucesso total, escondendo que o modelo não está respondendo.
- O alarme cobre o caso em que o fallback vira rotina, não exceção. Um cold start isolado gera um punhado de fallbacks e para. Fallback constante por horas é sinal de que o endpoint não está voltando — e essa distinção exige alguém olhar, não mais uma retentativa automática.
A diferença estrutural não é um nó a mais: são DUAS peças novas com papéis diferentes — o disjuntor, que decide ANTES de discar, e o caminho degradado (fila de revisão), que decide o que fazer quando a decisão foi não discar. Sem a segunda peça, o disjuntor sozinho só trocaria "500 depois de 220 ms" por "500 depois de 1 ms" — mais rápido, mas ainda quebrando a tela inteira.
O que o disjuntor entrega aqui que o L36 não precisou entregar
No L36, o disjuntor protegia o DOWNSTREAM de receber tráfego demais. Aqui, o downstream (o endpoint SageMaker) não está sob ataque de volume — o problema é que CADA vendedor que abre um pedido durante o mesmo cold start pagaria os 220 ms inteiros, sozinho, sem saber que os outros também estão pagando. O disjuntor aberto poupa o CHAMADOR: a partir da segunda chamada da rajada, a resposta degradada chega em microssegundos, não em 220 ms.
O caminho de uma pontuação, do clique do vendedor à decisão do disjuntor
O log estruturado abaixo é o que o caminho degradado escreve sempre que decide aceitar o pedido sem a pontuação. É texto de log, não uma chamada de API adicional — o metric filter do Terraform, na seção de construção, é quem o transforma em número que um alarme pode observar.
{
"evento": "FALLBACK_PONTUACAO_USADO",
"timestamp": "2026-08-08T08:41:12.204Z",
"motivo": "TaskCanceledException: timeout apos 220ms",
"pedidoId": "8f1c2a90-...",
"circuitoEstava": "Closed",
"_comentario": "Log escrito no catch do pipeline Polly. O metric filter do Terraform casa em '$.evento = FALLBACK_PONTUACAO_USADO' e vira contagem no CloudWatch — nenhuma chamada de API extra."
}
O caminho degradado é uma resposta de sucesso, não um erro disfarçado
A API responde 200 para o vendedor mesmo quando usa o fallback — porque, do ponto de vista do pedido, nada falhou: os dados carregaram, o pedido foi aceito, e só a pontuação auxiliar ficou pendente. Tratar isso como erro 5xx confundiria o painel de observabilidade da API (que mede disponibilidade do serviço) com o painel do disjuntor (que mede saúde do modelo) — são duas perguntas diferentes.
As decisões, e o que se perde em cada uma
📋 O painel de atendimento da Cadência (L74) depende de um endpoint SageMaker com scale-to-zero fora do expediente, cujo cold start pode passar de 2,85 s — muito acima do orçamento de 300 ms que o cliente já respeita. Sem tratamento, esse estouro derruba a requisição inteira do vendedor por causa de um dado auxiliar.
As quatro peças resolvem quatro problemas diferentes que coexistem aqui: o timeout reduzido garante margem para uma decisão sem estourar o orçamento; o retry seletivo recupera a fração de falhas que uma segunda tentativa realmente resolve (conexão), sem desperdiçar tempo na fração que não resolve (cold start); o disjuntor poupa a segunda chamada em diante de uma rajada de pagar o timeout inteiro sozinha; e o caminho degradado é o que impede a falha do modelo de virar falha do pedido. Nenhuma das quatro sozinha resolve o problema declarado — só o conjunto entrega degradação graciosa de verdade.
Alt: Aumentar o timeout do cliente (para 1 s, por exemplo) — Reduz a frequência de estouro, mas não elimina o cold start de 2,85 s, e ainda quebra o orçamento de 300 ms que o L74 declarou não-negociável para a experiência do vendedor.
Alt: Manter o endpoint sempre ligado, sem scale-to-zero — Elimina o cold start por completo, mas desfaz a economia que o próprio L74 conquistou (≈59% menos horas-instância) — trocaria um problema de latência por um problema de custo já resolvido.
Alt: Cachear a última pontuação conhecida do pedido — Parece razoável para pedidos já vistos antes, mas um pedido novo nunca tem cache — e servir uma pontuação DESATUALIZADA sem sinalizar isso é mais perigoso que marcar como pendente, porque parece um dado atual e confiável.
Alt: Mover a chamada de pontuação para fora do caminho síncrono da tela — Resolveria o problema de raiz — mas exige redesenhar a experiência do vendedor (a tela abriria sem a pontuação e ela apareceria depois), uma mudança de produto maior que este laboratório, registrada como extensão possível.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Timeout por tentativa | 220 ms (reduzido dos 300 ms do L74) | 300 ms (original); 100 ms (agressivo) | sobra orçamento para uma retentativa restrita sem estourar o teto do vendedor | uma resposta legítima que leva 250 ms passa a contar como falha e aciona o fallback |
| O que o retry trata | só `HttpRequestException` (falha de conexão) | toda exceção, como no antipadrão que o L36 nomeia | retentar timeout com o mesmo timeout dobra a espera sem ganhar chance real de sucesso | uma falha de rede momentânea que por acaso demorasse mais de 220 ms não seria distinguida de um cold start |
| Limiar do disjuntor | 50% de falha em janela de 10 s, mínimo de 5 chamadas | 20 chamadas (limiar do L36); sem mínimo de amostra | o volume do vendedor (≈200/dia) nunca atingiria um mínimo de 20 numa janela de 10 s | com amostra de 5, uma sequência de falhas isoladas (não um cold start real) pode abrir o circuito antes da hora |
| Duração da pausa do disjuntor | 5 s antes de testar recuperação | 15 s (padrão do L36); 60 s | o cold start medido no L74 resolve em até ≈2,85 s — 5 s já dá margem sem manter o disjuntor aberto além do necessário | se um cold start específico demorar mais que 5 s, o disjuntor testa cedo e reabre, gastando uma chamada de teste extra |
| Onde a pontuação pendente é resolvida | fila de revisão assíncrona, sem consumidor neste laboratório | resolver na hora, bloqueando até o modelo responder | a fila desacopla "aceitar o pedido" de "processar a pontuação", que é exatamente a degradação graciosa que o requisito pede | até o próximo laboratório da evolução, os pedidos pendentes se acumulam sem um processo que os feche — declarado como limitação, não escondido |
A dívida que este laboratório não paga
A fila de revisão de pontuação pendente é criada e recebe mensagens, mas NENHUM consumidor as processa neste laboratório — o entregável aqui é o cliente .NET com timeout, retry e caminho degradado, não o ciclo completo de revisão. Um pedido marcado como pendente fica pendente até alguém implantar o consumidor, que é o Nível 3 da escada de evolução, adiante nesta página.
Construir: o pipeline Polly com timeout, retry restrito e caminho degradado
O pipeline é construído UMA VEZ, como campo estático — o mesmo cuidado do L36. O timeout por tentativa vive na configuração do cliente do SDK, não como estratégia Polly separada — assim cada retentativa paga o timeout de novo, em vez de o timeout total valer para o pipeline inteiro.
// ServicoPontuacao.cs (depois) — pipeline Polly como campo estatico, com
// caminho degradado explicito. O orcamento de 300ms do L74 continua valendo;
// o que muda e que nenhuma excecao chega ao controller sem decisao tomada.
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Amazon.SageMakerRuntime;
using Amazon.SageMakerRuntime.Model;
public class ServicoPontuacao
{
private readonly AmazonSageMakerRuntimeClient _cliente = new(new AmazonSageMakerRuntimeConfig
{
// Reduzido dos 300ms do L74 de proposito: com retentativa restrita
// disponivel, sobra orcamento sem estourar o teto do vendedor. Cada
// retentativa paga este timeout de novo, porque ele vive aqui — no
// cliente do SDK — e nao como estrategia Timeout do Polly envolvendo
// o pipeline inteiro.
Timeout = TimeSpan.FromMilliseconds(220),
});
private readonly IFilaRevisao _filaRevisao; // publica o id do pedido pendente
private static readonly ResiliencePipeline<double> _resiliencia =
new ResiliencePipelineBuilder<double>()
.AddRetry(new RetryStrategyOptions<double>
{
// O PONTO CENTRAL deste laboratorio: retenta SO falha rapida
// de conexao. TaskCanceledException (o timeout de 220ms do
// cliente acima) NAO entra aqui — retentar um cold start com
// o MESMO timeout so dobraria a espera do vendedor sem ganhar
// nenhuma chance real de sucesso.
ShouldHandle = new PredicateBuilder<double>()
.Handle<HttpRequestException>(),
MaxRetryAttempts = 1,
Delay = TimeSpan.FromMilliseconds(15),
// Varios vendedores abrindo pedido no MESMO cold start nao
// sincronizam a retentativa no mesmo instante — mesmo motivo
// do L36, numa escala de tempo muito menor.
UseJitter = true,
OnRetry = args =>
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(new
{
evento = "RETRY_PONTUACAO_TENTATIVA",
espera_ms = args.RetryDelay.TotalMilliseconds,
}));
return default;
},
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<double>
{
ShouldHandle = new PredicateBuilder<double>()
.Handle<HttpRequestException>()
.Handle<TaskCanceledException>(),
// Janela curta e amostra baixa: o volume do vendedor e de
// ~200 chamadas/dia (do L74), nao os 800 concorrentes de
// campanha do L36. Copiar o limiar de la deixaria o disjuntor
// inerte durante o proprio cold start que deveria pegar.
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 5,
// Cold start medido no L74 resolve em ate ~2,85s — 5s de
// pausa da margem sem manter o disjuntor aberto alem do
// necessario.
BreakDuration = TimeSpan.FromSeconds(5),
OnOpened = args =>
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(new
{
evento = "CIRCUITO_PONTUACAO_ABERTO",
motivo = args.Outcome.Exception?.GetType().Name ?? "limiar de falha atingido",
}));
return default;
},
OnClosed = args =>
{
Console.WriteLine("{\"evento\":\"CIRCUITO_PONTUACAO_FECHADO\"}");
return default;
},
})
.Build();
public async Task<ResultadoPontuacao> PontuarPedidoAsync(string pedidoId, string payloadJson)
{
try
{
var score = await _resiliencia.ExecuteAsync(async ct =>
{
var resposta = await _cliente.InvokeEndpointAsync(new InvokeEndpointRequest
{
EndpointName = "cadencia-cancelamento-tempo-real", // do L74
ContentType = "application/json",
Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(payloadJson)),
}, ct);
using var leitor = new StreamReader(resposta.Body);
var corpo = await leitor.ReadToEndAsync(ct);
return System.Text.Json.JsonDocument.Parse(corpo).RootElement.GetProperty("score").GetDouble();
});
return ResultadoPontuacao.Pronta(score);
}
catch (Exception ex) when (ex is BrokenCircuitException or HttpRequestException or TaskCanceledException)
{
// O CAMINHO DEGRADADO explicito. O pedido segue sem travar; a
// pontuacao fica marcada como pendente; alguem processa depois.
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(new
{
evento = "FALLBACK_PONTUACAO_USADO",
pedidoId,
motivo = ex.GetType().Name,
}));
await _filaRevisao.PublicarAsync(pedidoId);
return ResultadoPontuacao.Pendente();
}
}
}
public readonly record struct ResultadoPontuacao(double? Score, bool Pendente)
{
public static ResultadoPontuacao Pronta(double score) => new(score, false);
public static ResultadoPontuacao Pendente() => new(null, true);
}
Por que o timeout não é uma estratégia Polly aqui
No L36, o timeout vivia na configuração do `HttpClient`, fora do pipeline Polly — e este laboratório repete a mesma escolha, por um motivo concreto: uma estratégia `AddTimeout` do Polly, se colocada como a primeira do pipeline, envolveria RETRY e CIRCUIT BREAKER juntos dentro de um único orçamento de 220 ms — sem sobrar tempo nenhum para a retentativa existir de fato. Deixar o timeout no cliente do SDK garante que cada tentativa individual, inclusive a retentativa, tenha seu próprio orçamento de 220 ms.
Construir: ALB, fila de revisão e observabilidade
A API de atendimento reaproveita o cluster ECS e a VPC já criados no L01 — só o serviço, o ALB dedicado a ele e a fila de revisão são novos.
# atendimento.tf -- ALB e servico ECS Fargate da API de atendimento
# Reaproveita cluster e VPC do L01; a imagem vem do ECR ja existente
# (referenciado, nao criado aqui).
resource "aws_lb" "atendimento" {
name = "${var.projeto}-atendimento"
internal = true # so trafego interno da Cadencia chega ao painel
load_balancer_type = "application"
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.atendimento_alb.id]
}
resource "aws_lb_target_group" "atendimento" {
name = "${var.projeto}-atendimento-api"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip"
health_check {
path = "/health" # nao toca o SageMaker -- so confirma o processo de pe
interval = 10
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_ecs_task_definition" "atendimento" {
family = "${var.projeto}-atendimento"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.execucao.arn
task_role_arn = aws_iam_role.atendimento_task.arn # invoca o SageMaker e publica na fila
container_definitions = jsonencode([{
name = "api"
image = "${data.aws_ecr_repository.principal.repository_url}:${var.sha_atendimento}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "ENDPOINT_SAGEMAKER", value = "cadencia-cancelamento-tempo-real" },
{ name = "FILA_REVISAO_URL", value = aws_sqs_queue.revisao_pontuacao.url },
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.atendimento.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "api"
}
}
}])
}
resource "aws_ecs_service" "atendimento" {
name = "${var.projeto}-atendimento"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.atendimento.arn
desired_count = 2 # duas AZs, uma task cada, para o teste de cold start nao depender de uma unica task
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.atendimento_api.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.atendimento.arn
container_name = "api"
container_port = 8080
}
}
# fila-revisao.tf -- fila de revisao de pontuacao pendente
# Sem consumidor neste laboratorio -- ver a limitacao registrada na secao
# de decisoes e no Nivel 3 da evolucao.
resource "aws_sqs_queue" "revisao_pontuacao" {
name = "${var.projeto}-revisao-pontuacao-pendente"
visibility_timeout_seconds = 60
message_retention_seconds = 1209600 # 14 dias -- ate o consumidor do Nivel 3 existir
kms_master_key_id = aws_kms_key.atendimento.arn # so o id do pedido trafega, mas cifrado por padrao
}
resource "aws_kms_key" "atendimento" {
description = "cifra a fila de revisao de pontuacao da API de atendimento"
enable_key_rotation = true
}
# observabilidade-pontuacao.tf -- circuito e fallback, visiveis sem abrir codigo
resource "aws_sns_topic" "fallback_pontuacao" {
name = "${var.projeto}-fallback-pontuacao-sustentado"
}
resource "aws_cloudwatch_log_metric_filter" "fallback_usado" {
name = "${var.projeto}-fallback-pontuacao-usado"
log_group_name = aws_cloudwatch_log_group.atendimento.name
pattern = "{ $.evento = \"FALLBACK_PONTUACAO_USADO\" }"
metric_transformation {
name = "FallbackPontuacaoUsado"
namespace = "${var.projeto}/pontuacao"
value = "1"
unit = "Count"
}
}
# Nao dispara num cold start isolado -- isso e o fallback fazendo o trabalho
# dele. Dispara quando o fallback e usado com frequencia incomum numa janela
# de 10 minutos, sinal de que o endpoint nao esta so frio, esta fora.
resource "aws_cloudwatch_metric_alarm" "fallback_sustentado" {
alarm_name = "${var.projeto}-fallback-pontuacao-sustentado"
namespace = "${var.projeto}/pontuacao"
metric_name = "FallbackPontuacaoUsado"
statistic = "Sum"
period = 600
evaluation_periods = 1
threshold = 15 # acima do que um unico cold start explica, dado o volume do L74
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.fallback_pontuacao.arn]
}
Implantar, e provar que o fallback poupa o vendedor do cold start
Cinco medições. Nenhuma conclusão vem de "a tela carregou" sem um número atrelado — é a diferença entre demonstrar e afirmar.
// TesteColdStart/Program.cs -- forca o endpoint a ficar ocioso, dispara uma
// rajada de chamadas simulando 5 vendedores abrindo pedido no mesmo minuto,
// e mede quanto cada um espera -- com o cliente ingenuo (antes) e com o
// pipeline Polly (depois).
var usaPolly = Environment.GetEnvironmentVariable("USA_POLLY") == "true";
var servico = usaPolly ? (IServicoPontuacao)new ServicoPontuacao() : new ServicoPontuacaoIngenuo();
// Espere o endpoint escalar a zero (fora da janela de 9h-18h, ou aguarde o
// timeout de ociosidade) antes de rodar -- senao a rajada inteira mede
// container ja quente.
var relogios = new List<(bool sucesso, double ms)>();
var inicio = System.Diagnostics.Stopwatch.StartNew();
var tarefas = Enumerable.Range(0, 5).Select(async i =>
{
var relogio = System.Diagnostics.Stopwatch.StartNew();
var resultado = await servico.PontuarPedidoAsync($"PED-{i}", "{\"valor_total\":199.90}");
relogio.Stop();
return (sucesso: resultado != null, ms: relogio.Elapsed.TotalMilliseconds);
});
relogios.AddRange(await Task.WhenAll(tarefas));
inicio.Stop();
var modo = usaPolly ? "polly" : "ingenuo";
Console.WriteLine($"modo: {modo}");
foreach (var (sucesso, ms) in relogios.OrderBy(r => r.ms))
{
var rotulo = sucesso ? "ok" : "erro/fallback";
Console.WriteLine($" chamada: {ms:F0} ms, resultado: {rotulo}");
}
// Medido no ambiente de exemplo da Cadencia, endpoint frio:
// ingenuo: 5 chamadas, cada uma esperando ~300ms e terminando em erro 500
// polly: 1a chamada ~220ms (paga o timeout, abre o circuito); as 4
// seguintes respondem em menos de 5ms cada, com pontuacao pendente
- Prova 1 — a falha sem fallback é reproduzida: force o endpoint a escalar a zero (fora de 9h–18h) e chame `PontuarPedidoAsync` do `ServicoPontuacao` original. Esperado, medido no ambiente de exemplo: erro após ≈300 ms, com `TaskCanceledException` não tratada subindo até o controller.
- Prova 2 — o mesmo cenário, com o pipeline Polly: rode `TesteColdStart` com `USA_POLLY=true` contra o mesmo endpoint frio. Esperado: toda chamada retorna com sucesso HTTP, a primeira com pontuação pendente em ≈220 ms, sem nenhum erro 500.
- Prova 3 — o disjuntor poupa a rajada, não só a primeira chamada: rode as 5 chamadas concorrentes do harness acima. Esperado no ambiente de exemplo: a primeira chamada leva ≈220 ms (paga o timeout e abre o circuito); as 4 seguintes retornam em menos de 5 ms cada, porque o circuito já estava aberto.
- Prova 4 — retry ajuda conexão, não cold start: derrube a conectividade de rede da task por 50 ms (não o endpoint) e confirme, pelos logs `RETRY_PONTUACAO_TENTATIVA`, que a retentativa dispara e tem sucesso. Repita contra o endpoint frio e confirme que NENHUM `RETRY_PONTUACAO_TENTATIVA` aparece — só `FALLBACK_PONTUACAO_USADO`.
- Prova 5 — a métrica reflete o fallback em tempo real: confira `aws cloudwatch get-metric-statistics --namespace "${var.projeto}/pontuacao" --metric-name FallbackPontuacaoUsado` durante o teste de cold start. Ausência de dado aqui, com o fallback sendo usado, significa que o metric filter não está casando o log — revise o `pattern` contra o formato exato do log estruturado.
O que não foi medido em produção real
Os números de latência (≈220 ms na primeira chamada de uma rajada fria, <5 ms nas seguintes) vêm de uma execução de exemplo contra o endpoint da Cadência em ambiente de laboratório, não de um pico real de atendimento. A duração do cold start em si (até ≈2,85 s) é a medida do L74 — meça a sua antes de calibrar `BreakDuration` e `MinimumThroughput` para produção.
Quebrar de propósito: três falhas e o diagnóstico
| Falha provocada | Sintoma | Onde olhar | Correção |
|---|---|---|---|
| Incluir `TaskCanceledException` no `ShouldHandle` do retry | toda chamada fria dobra de espera: 220 ms + 220 ms antes do fallback, quase estourando os 300 ms do L74 | log `RETRY_PONTUACAO_TENTATIVA` aparecendo em chamadas que também geram `FALLBACK_PONTUACAO_USADO` logo depois | remova `TaskCanceledException` do `ShouldHandle`; retentar timeout não recupera cold start, só adia o fallback |
| Reduzir `MinimumThroughput` para 1 | o circuito abre com uma única chamada isolada e lenta, mesmo o endpoint estando saudável na maior parte do tempo | métrica `CIRCUITO_PONTUACAO_ABERTO` sobe mesmo fora da janela de cold start conhecida | suba o limiar para uma amostra que represente tendência — 5 foi calibrado para o volume real do vendedor, não para ruído isolado |
| Remover a fila de revisão e devolver um valor fixo (ex.: `0`) no fallback | todo pedido com pontuação pendente parece ter risco zero, e nenhum é revisado depois | nenhuma mensagem chega em `revisao_pontuacao`, mesmo com `FALLBACK_PONTUACAO_USADO` subindo no CloudWatch | volte a publicar o id na fila sempre que o fallback for usado; um valor fixo esconde a degradação em vez de sinalizá-la |
Fallback que devolve um score fixo é pior que nenhum fallback
Um valor numérico fixo no lugar da pontuação parece um dado válido para quem lê a tela — e se esse número for interpretado como "baixo risco", um pedido que nunca foi avaliado pode ser aprovado automaticamente sem revisão nenhuma. A marcação explícita de "pendente" existe exatamente para impedir que ausência de dado seja confundida com dado favorável.
Segurança: o que o cliente do endpoint expõe
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Policy IAM da API sem `Resource` específico, podendo invocar qualquer endpoint SageMaker da conta | média | alto — pode chamar endpoint de outro modelo, gerando custo e dado indevido | Resource com o ARN do endpoint `cadencia-cancelamento-tempo-real`, nunca `sagemaker:*` | CloudTrail mostrando `InvokeEndpoint` fora do endpoint esperado | revogar a policy ampla e reemitir com o ARN restrito |
| Fallback tratado como aprovação automática por um sistema downstream | baixa, mas grave quando ocorre | alto — pedido de risco real aprovado sem revisão | campo `pontuacaoPendente` explícito, nunca um score que pareça válido | taxa de pedidos com pontuação pendente que foram aprovados sem passar pela fila de revisão | bloquear aprovação automática sempre que `pontuacaoPendente = true` |
| Timeout do cliente configurado acima do orçamento por engano | baixa | médio — volta ao problema original: tela travando por mais tempo | revisar a fórmula do orçamento a cada mudança de `Timeout` ou `MaxRetryAttempts` | p99 de "abrir pedido" subindo acima de 300 ms | reduzir de volta ao valor validado nesta seção |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvocarSomenteEndpointDePontuacao",
"Effect": "Allow",
"Action": "sagemaker:InvokeEndpoint",
"Resource": "arn:aws:sagemaker:*:*:endpoint/cadencia-cancelamento-tempo-real"
},
{
"Sid": "PublicarSomenteNaFilaDeRevisao",
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:*:*:cadencia-revisao-pontuacao-pendente"
}
]
}
O pipeline Polly deste laboratório retenta `HttpRequestException` mas exclui `TaskCanceledException` do retry. Um endpoint em cold start demora 2,4 s para responder, e o timeout do cliente é 220 ms. O que acontece na primeira chamada de um vendedor que abre um pedido nesse momento?
Observabilidade: as perguntas que o painel tem de responder
- O circuito está aberto agora, e há quanto tempo?
- Quantos vendedores nas últimas 10 minutos receberam pontuação pendente em vez do valor real?
- O fallback ocorreu numa rajada curta (cold start isolado) ou está sustentado por horas?
- Quantas mensagens estão acumuladas na fila de revisão, sem consumidor ainda?
- A latência p99 da tela de "abrir pedido" está dentro dos 300 ms mesmo quando o modelo está saudável?
| Alarme | Métrica | Limiar inicial | Por que esse limiar |
|---|---|---|---|
| Fallback sustentado | soma de `FallbackPontuacaoUsado` (custom) | ≥ 15 em 10 min | um cold start isolado gera um punhado de fallbacks numa rajada e para; 15 numa janela de 10 min aponta degradação além disso |
| Circuito reabrindo repetidamente | contagem de `CIRCUITO_PONTUACAO_ABERTO` (custom) | ≥ 3 janelas de 1 min seguidas | não dispara na primeira abertura — isso é o disjuntor funcionando; dispara quando ele não fica fechado |
| Fila de revisão sem consumo | `ApproximateNumberOfMessagesVisible` (SQS) | > 500 mensagens | sinaliza que a limitação declarada (sem consumidor neste laboratório) precisa avançar para o Nível 3 da evolução |
| Tempo de resposta perto do orçamento | p99 de latência do endpoint `/pedidos` | > 280 ms | avisa ANTES da tela realmente estourar os 300 ms, com margem para investigar |
Escala: 10, 10 mil, 1 milhão, e falha de AZ
| Cenário | O que muda | Onde a arquitetura sente primeiro |
|---|---|---|
| 10 pedidos/dia | o disjuntor quase nunca atinge `MinimumThroughput` | a proteção vem quase só do timeout reduzido e do fallback, não do circuito em si |
| 200 chamadas/dia distribuídas no expediente (o volume medido no L74) | vários vendedores voltando do almoço podem abrir pedido no mesmo minuto de um cold start | é exatamente o cenário que este laboratório mede — o disjuntor entra em ação em segundos |
| 1 milhão de pedidos/dia (hipotético, fora do perfil atual da Cadência) | um único endpoint de tempo real não escala nesse volume, mesmo sem cold start | a decisão deixa de ser "fallback do cliente" e vira "quantas instâncias o endpoint precisa" — tema do L74/L80, não deste módulo |
| Falha de AZ do endpoint SageMaker | as chamadas falham de forma SUSTENTADA, não só durante um cold start pontual | o circuito abre e permanece aberto além do esperado; o alarme de "fallback sustentado" é quem distingue isso de um cold start isolado |
Custo: o que este laboratório acrescenta à fatura
Polly é biblioteca: zero custo de infraestrutura por si só. A fila de revisão cobra por mensagem, não por hora ligada. Use o AWS Pricing Calculator para o seu volume; nenhum valor absoluto aqui envelhece bem.
| Cenário | Dimensões que pesam | Custo oculto |
|---|---|---|
| Sem fallback (estado herdado do L74) | nenhum custo de infraestrutura novo | o custo é indireto: vendedor bloqueado, pedido perdido ou reaberto manualmente, que não aparece em nenhuma fatura da AWS |
| Com Polly, sem consumidor da fila de revisão | SQS cobra por mensagem publicada, valor desprezível no volume do L74 | pedidos "pendentes" nunca revisados é dívida que não aparece em dólar nenhum, mas aparece em risco de crédito não avaliado |
| Com o consumidor implantado (Nível 3 da evolução) | um Lambda a mais processando a fila | custo desprezível comparado ao endpoint GPU que já existe desde o L74 — a peça cara da arquitetura continua sendo o modelo, não a resiliência |
O custo que ninguém mede: reabertura manual do pedido
Antes deste laboratório, um vendedor que via a tela travar reabria o pedido manualmente, às vezes mais de uma vez, até o cold start terminar. Esse tempo perdido não aparece em nenhuma fatura da AWS, mas é o custo real que a degradação graciosa elimina.
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | timeout estoura sem nenhum log de decisão na mínima | ninguém sabe que o vendedor está vendo erro até ele reclamar | log estruturado + métrica a cada fallback e a cada transição do circuito | alta |
| Segurança | policy IAM da task pode ficar ampla demais se copiada de outro laboratório | API invoca endpoint ou fila que não deveria | Resource restrito ao ARN do endpoint e da fila específicos | alta |
| Confiabilidade | timeout sem fallback derruba a requisição inteira por um dado auxiliar | experiência do vendedor quebrada por algo que não deveria ser bloqueante | timeout reduzido + retry seletivo + circuit breaker + caminho degradado, como este laboratório constrói | alta |
| Eficiência de performance | timeout de 220 ms escolhido sem medir a latência normal do endpoint quente | chamadas legítimas mas um pouco mais lentas contam como falha desnecessariamente | validar 220 ms contra o p50/p99 medidos com o endpoint aquecido | média |
| Otimização de custo | fallback sem fila de revisão esconderia pedidos nunca avaliados | risco de crédito não revisado se acumula sem custo visível em dólar | fila de revisão com alarme de acúmulo, e o consumidor do Nível 3 como próximo passo | média |
| Sustentabilidade | disjuntor evita chamadas que já se sabe que vão falhar | sem ele, cada vendedor da rajada gastaria CPU e rede numa chamada fadada ao timeout | circuit breaker reduz trabalho descartado de rede e de CPU da task, não só a espera percebida | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco deste módulo e compra outro — até o ponto em que a mesma decisão de resiliência se repete sobre um modelo que já não é mais um classificador tabular, e sim um LLM.
O cliente tem `Timeout = 300ms`, mas nenhum tratamento ao redor. Funciona todo dia dentro da janela quente; fora dela, uma única chamada fria derruba a tela inteira.Timeout de 220 ms, retry só para falha de conexão, circuit breaker calibrado pelo volume do vendedor, e fallback explícito que aceita o pedido com pontuação pendente.Um Lambda processa as mensagens pendentes e atualiza o pedido com a pontuação real assim que o endpoint volta a responder — fechando o ciclo que este laboratório deixa aberto.Estado do circuito centralizado (ex.: ElastiCache/Redis) em vez de local a cada task — mesmo ponto que o L36 registrou como dívida, agora aplicado aqui.Antes de aceitar "pendente", tenta uma vez o endpoint serverless (mais lento, mas sem o mesmo cold start correlacionado ao expediente) como segunda chance real.Quando o "modelo" servido deixa de ser um classificador tabular e passa a ser um LLM via Bedrock, chamado pela mesma API de atendimento — é o L81, primeira chamada ao Bedrock do .NET 8, herdando este mesmo pipeline de resiliência.A ordem não é negociável, e o motivo é concreto
Ligar o fallback (Nível 2) antes de reduzir o timeout (parte do mesmo nível, mas logicamente primeiro) deixaria o disjuntor abrindo depois de esperas de 300 ms em vez de 220 ms — funcionaria, mas gastaria mais orçamento do vendedor por chamada do que o necessário antes de decidir parar de tentar.
Onde IA entra nesta arquitetura, e onde não entra
O modelo já é a peça de IA aqui — o que este laboratório NÃO faz é decidir o fallback com IA
A pontuação de cancelamento em si já é o ponto de entrada de IA nesta arquitetura, herdado do L73/L74. O que este módulo resolve é puramente mecânico e determinístico: timeout, retry seletivo e circuit breaker são fórmulas auditáveis, iguais ao L36. "Usar um segundo modelo para prever se vale a pena esperar pela resposta do primeiro" substituiria uma regra simples e previsível por uma caixa-preta, sem ganho mensurado — e uma decisão de fallback errada por previsão ruim é mais perigosa que uma decisão por janela de contagem simples.
O ponto de entrada legítimo de MAIS IA nesta cadeia é o Nível 6 da escada de evolução acima, e é o L81: quando o modelo servido deixa de ser o classificador de cancelamento e passa a ser um LLM via Bedrock, o mesmo pipeline de resiliência construído aqui — timeout, retry seletivo, disjuntor, fallback explícito — se aplica de novo, com uma diferença concreta que o L81 trata: streaming de resposta muda o que "timeout" significa.
Anti-padrões deste laboratório
| Antipadrão | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| Deixar o timeout do L74 sem try/catch ao redor | parece já resolvido porque o timeout existe — "já tem timeout, então é seguro" | TaskCanceledException sobe e derruba a tela inteira do pedido, não só a pontuação | envolver a chamada com Polly e sempre ter um fallback explícito, nunca deixar uma exceção de rede chegar ao controller |
| Retentar timeout como se fosse qualquer outra falha | generaliza `ShouldHandle` para "qualquer exceção", copiando o hábito sem revisar o caso | a retentativa espera o MESMO timeout do endpoint que ainda está frio, dobrando a espera do vendedor sem ganhar chance real | excluir `TaskCanceledException` do retry; contra cold start, retentar não ajuda, só adia o fallback |
| Circuit breaker calibrado com o volume de pico de campanha, copiado do L36 | copiar o número que já funcionou em outro laboratório parece mais seguro que calcular de novo | com poucas chamadas de vendedor por minuto, `MinimumThroughput` nunca é atingido e o disjuntor fica inerte durante o próprio cold start que deveria pegar | calibrar pelo volume REAL deste consumidor (≈200/dia), não pelo de outro caso da série |
| Fallback que devolve um valor fixo sem marcar para revisão | parece suficiente porque a tela não trava mais, e é menos código que publicar numa fila | pedidos de risco alto nunca são revisados, e ninguém percebe que o modelo está fora do ar | publicar o id na fila de revisão sempre que o fallback for usado, e marcar o campo como pendente, nunca com um número |
| Aumentar o timeout do cliente em vez de tratar a falha | é a mudança de uma linha, sem entender o orçamento de 300 ms que o L74 declarou | todo vendedor volta a esperar mais, mesmo quando o endpoint está saudável na maior parte do tempo | manter o orçamento de 300 ms herdado do L74 e resolver com fallback, não com mais paciência do cliente |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Log/métrica | Correção |
|---|---|---|---|---|
| Tela do vendedor mostra erro genérico ao abrir pedido fora do horário de aquecimento | o try/catch do pipeline Polly não está ativo no deploy — regressão para o comportamento do L74 | comparar o horário do erro com a janela de scale-to-zero (9h–18h) do endpoint | TaskCanceledException nos logs SEM `FALLBACK_PONTUACAO_USADO` correspondente | confirmar que `ServicoPontuacao` (depois) está de fato implantado, não a versão do L74 sem tratamento |
| Fallback usado quase toda chamada, mesmo dentro da janela quente | timeout de 220 ms configurado abaixo da latência normal real do endpoint aquecido | comparar o p50/p99 medido do endpoint quente com o timeout configurado | proporção alta de `FALLBACK_PONTUACAO_USADO` mesmo com o endpoint fora da janela de cold start | aumentar o timeout por tentativa dentro do orçamento de 300 ms, revisando a fórmula do pior caso |
| Circuito nunca abre durante um cold start observado | `MinimumThroughput` alto demais para o volume real de vendedor | comparar o número de chamadas numa janela de 10 s com o `MinimumThroughput` configurado | métrica `CIRCUITO_PONTUACAO_ABERTO` ausente durante um cold start confirmado por outro sinal | reduza `MinimumThroughput` para o volume real medido — 5 é o ponto de partida, não um valor universal |
| Fila de revisão cresce sem nunca ser processada | o consumidor da fila (Nível 3 da evolução) ainda não foi implantado | checar `ApproximateNumberOfMessagesVisible` da fila no CloudWatch | contagem subindo sem nunca descer | declarar a limitação explicitamente até o Nível 3 ser implantado — não é bug, é o escopo deste laboratório |
Limpeza: o que o destroy não leva
#!/usr/bin/env bash
set -euo pipefail
# A ordem importa: o ALB e o servico ECS nao tem dependente fora deste
# modulo, mas o log group e a fila sobrevivem ao destroy se nao estiverem
# no mesmo state.
terraform destroy \
-target=aws_ecs_service.atendimento \
-target=aws_ecs_task_definition.atendimento \
-target=aws_lb_target_group.atendimento \
-target=aws_lb.atendimento \
-auto-approve
terraform destroy -auto-approve
echo "Confira manualmente:"
echo " - log group /aws/ecs/<projeto>-atendimento nao e removido por este destroy"
echo " - a fila de revisao de pontuacao pode ter mensagens nao processadas;"
echo " o destroy remove a fila JUNTO com as mensagens, sem processa-las"
O que o destroy NÃO leva
O ALB (`aws_lb.atendimento`) e o serviço ECS são removidos pelo `destroy` acima, e são os únicos recursos deste laboratório que cobram hora ligada parada. A fila SQS não cobra parada — só por mensagem publicada — mas `terraform destroy` remove a fila JUNTO com qualquer mensagem de revisão ainda não processada, sem nenhum aviso além deste. Se houver pedidos pendentes de verdade, drene a fila antes de destruir o ambiente.
Resumo: problema, peça e motivo
| Problema | Peça | Motivo |
|---|---|---|
| Timeout do L74 sem nenhum tratamento | try/catch ao redor do pipeline Polly | nenhuma exceção de rede chega ao controller sem uma decisão explícita tomada |
| Retentar cold start com o mesmo timeout não ajuda | `ShouldHandle` exclui `TaskCanceledException` | retry só recupera o tipo de falha que uma segunda tentativa realmente resolve |
| Vários vendedores pagando o mesmo timeout na mesma rajada | circuit breaker calibrado pelo volume do vendedor | a partir da segunda chamada de uma rajada, o disjuntor aberto poupa o timeout inteiro |
| Pedido não pode ficar bloqueado pela pontuação auxiliar | caminho degradado explícito, com fila de revisão | separa "aceitar o pedido" de "avaliar o risco", em vez de amarrar os dois |
| Fallback pode virar rotina sem ninguém perceber | métrica de uso do fallback + alarme | transforma um estado interno do processo em dado observável, distinguindo cold start isolado de degradação sustentada |
| Pipeline de retry pode estourar o orçamento de 300 ms do L74 | orçamento explícito (timeout + jitter + retentativa) | torna o limite verificável em vez de descoberto quando o vendedor reclamar |
- Vendedor clica para abrir um pedido; a API começa a carregar os dados do banco.
- O pipeline Polly verifica: o circuito está fechado?
- Se sim, tenta o endpoint com timeout de 220 ms.
- Falha de conexão retenta uma vez com jitter; timeout vai direto ao fallback.
- Circuito aberto ou tentativas esgotadas: pedido aceito, pontuação marcada como pendente, id publicado na fila de revisão.
- Toda decisão do circuito e todo uso do fallback viram métrica.
- A tela do pedido carrega — com a pontuação real, ou com "em revisão" no lugar dela.
Perguntas frequentes
❓ Por que o cliente do L74 já tinha timeout e mesmo assim quebrava a tela do vendedor?
❓ Por que o retry deste laboratório ignora timeout e só retenta falha de conexão?
❓ O que o circuit breaker economiza aqui, se o volume de chamadas é baixo?
❓ Por que o fallback marca a pontuação como pendente em vez de usar um valor fixo?
❓ Quem consome a fila de revisão de pontuação pendente criada neste laboratório?
❓ Por que o timeout foi reduzido para 220 ms em vez dos 300 ms originais do L74?
❓ O que diferencia o disjuntor deste laboratório do disjuntor construído no L36?
Fixando
Um engenheiro copia o circuit breaker do L36 para este laboratório — `FailureRatio=0.5`, `SamplingDuration=10s` — mas mantém `MinimumThroughput=20` (do L36). O volume de chamadas de pontuação da Cadência é de ≈200/dia, concentradas no expediente. Qual é o efeito mais provável dessa escolha durante um cold start real?
O timeout por tentativa é 220 ms, e a única retentativa acontece só para `HttpRequestException` (falha de conexão), com espera de até 15 ms de jitter. Um engenheiro decide "simplificar" e passa a retentar TAMBÉM `TaskCanceledException`, mantendo os mesmos parâmetros. Qual é o efeito mais provável durante um cold start medido em 2,4 s?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L74 (endpoint cadencia-cancelamento-tempo-real e o ClienteTempoReal.cs no ar), L36 (Polly, retry, jitter, circuit breaker), .NET 8 |
| Conhecimentos adquiridos | por que timeout sem tratamento ainda é uma falha, só que mais previsível; retry seletivo por tipo de exceção; circuit breaker calibrado pelo volume real do consumidor; caminho degradado explícito como contrato, não como acidente; orçamento de latência com retentativa restrita |
| Limitação que fica | a fila de revisão de pontuação pendente não tem consumidor neste laboratório — pedidos marcados como pendentes ficam pendentes até o Nível 3 da evolução ser implantado |
| Próximo exemplo recomendado | L81 — Primeira chamada ao Bedrock do .NET 8. Reutiliza o mesmo raciocínio de timeout, retry e streaming, agora contra um modelo generativo em vez de um classificador tabular |
| Também habilitado por este módulo | o padrão de orçamento de latência com retentativa restrita se aplica a qualquer outra chamada síncrona e bloqueante da Cadência a um serviço gerenciado; o consumidor da fila de revisão (Nível 3) depende do contrato de mensagem definido aqui |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: Polly — pollydocs.org (estratégias Retry e Circuit Breaker, `PredicateBuilder`, os quatro estados do disjuntor); AWS SDK for .NET — Amazon SageMaker Runtime (`InvokeEndpointAsync`, `AmazonSageMakerRuntimeConfig.Timeout`); e SageMaker AI — Real-time inference e managed instance scaling, a mesma referência do L74 para o comportamento de scale-to-zero e cold start. Nenhum valor em dólar aparece neste módulo por decisão: os números de latência e volume são medições do cenário de exemplo da Cadência, e mudam por região e por acordo de conta — confira o AWS Pricing Calculator para o valor atual.
O que não foi verificado, e você deve conferir na sua conta
A duração do cold start (até ≈2,85 s) e o volume de ≈200 chamadas/dia são medições de exemplo herdadas do L74, não constantes universais do SageMaker. Os limiares do disjuntor deste módulo (`MinimumThroughput=5`, `BreakDuration=5s`) foram calibrados para ESSE volume específico — meça o seu antes de levar os mesmos números para produção. O comportamento exato de `managed_instance_scaling` com `min_instance_count = 0` também depende da versão do provider Terraform, como já registrado no L74.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…