Lab 37 — Consistência eventual do ponto de vista do usuário
O problema, e a empresa que o tem
A Rota Livre é um marketplace de entregas com 1.200 restaurantes parceiros. Cada um atualiza o status do próprio pedido — pendente, confirmado, cancelado — pelo tablet na cozinha, e o mesmo aplicativo mostra esse status na tela seguinte. A escrita já passa por uma fila e um cache antes de chegar ao banco: decisão tomada para o pico de meio-dia não derrubar a tabela, e legítima.
O ticket que chega ao suporte é sempre a mesma frase: "confirmei o pedido, saí da tela e voltei, e continua pendente." A pessoa de suporte reabre como bug, porque reproduz — literalmente, o mesmo restaurante consegue reproduzir na hora, na frente de quem atende. O que ninguém tinha medido ainda é que a reprodução é intermitente: às vezes acontece, às vezes não, e essa intermitência é a pista que aponta para dois relógios assíncronos, não para uma falha determinística.
Este laboratório assume que a decisão de usar fila e cache já foi tomada — é o assunto do L32 — e parte do sintoma que ela produz quando ninguém trata quem é o AUTOR da escrita como um caso diferente de qualquer outro leitor.
O que este laboratório NÃO é
Não é sobre eliminar a fila ou o cache — isso reintroduziria o problema de capacidade que os dois resolvem, e é outro laboratório de trade-off. Não é sobre transação distribuída nem compensação (saga, L35): aqui existe um único item, uma única escrita. E não é sobre a decisão síncrono/assíncrono em si (L32): este módulo começa DEPOIS que essa decisão já foi tomada, e trata do sintoma que ela produz quando ninguém distingue o autor da escrita de qualquer outro leitor.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que a mesma arquitetura parece "com bug" para o autor e normal para qualquer outro leitor.
- Nomear os dois relógios assíncronos deste desenho e medir, com número, a janela de cada um.
- Derivar, a partir da documentação oficial, por que ConsistentRead sozinho não resolve o atraso de uma fila.
- Implementar um marcador de leitura-própria-escrita com chave composta por autor e recurso, e TTL derivado de medição.
- Justificar, com o comportamento do SQS padrão, por que o invalidador de cache deve apagar a chave em vez de escrever o novo valor.
- Medir a taxa de divergência do autor antes e depois da correção, com os dois números registrados.
- Provar que outros usuários continuam eventualmente consistentes, por decisão, e não por falha da correção.
- Diagnosticar, a partir do sintoma relatado pelo suporte, qual dos dois relógios — fila ou cache — está em jogo.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Consistência eventual vs forte no DynamoDB | SAP-C02, DVA-C02 | `ConsistentRead` de um lado, leitura padrão do outro | quando a leitura forte resolve, e quando não resolve nada (atraso de fila) |
| Read-your-writes como padrão de arquitetura | SAP-C02 | marcador com TTL gravado no request de escrita | é solução de UX cirúrgica, não muda o modelo de consistência do sistema todo |
| Garantias de entrega e ordem do SQS | SAP-C02, DVA-C02 | fila padrão: pelo menos uma vez, sem ordem garantida | por que isso exige idempotência em quem consome, não em quem produz |
| DynamoDB Streams como fonte de eventos | DVA-C02, SAP-C02 | event source mapping do invalidador para o Lambda | NewImage/OldImage, StreamViewType, e que a leitura do stream também é eventualmente consistente |
| Padrão cache-aside e invalidação | SAP-C02 | ElastiCache como camada de leitura, populada sob demanda | quando invalidar (DELETE) versus atualizar (SET), e por que a ordem de chegada decide isso |
| Acoplamento temporal e disponibilidade composta | SAP-C02 | fila entre a API e a persistência — ligado à decisão do L32 | o trade-off entre latência percebida pelo usuário e resiliência a pico de escrita |
| Idempotência em processamento distribuído | SAP-C02, DVA-C02 | `PutItem` sobrescrevendo pela mesma chave de partição | por que entrega "pelo menos uma vez" exige que reprocessar tenha o mesmo efeito que processar uma vez |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um usuário que não vê a própria alteração e pede a causa mais provável, oferecendo "o DynamoDB perdeu o dado" como distrator. A resposta certa nomeia o mecanismo: nada foi perdido, a leitura ocorreu numa janela em que o dado ainda não tinha sido processado ou o cache ainda não tinha sido avisado. "Perda" e "atraso" são conceitos diferentes, e a prova cobra a distinção.
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 |
|---|---|---|
| Autor vê a própria escrita | sempre, sem exceção perceptível | marcador síncrono gravado no mesmo request que aceita a escrita, com TTL derivado de medição |
| Escrita não pode sobrecarregar a tabela em pico | até 15 escritas/s no almoço | fila SQS absorvendo o pico antes do `PutItem`, sem mudar com esta correção |
| Leitura repetida não pode bater direto no banco | toda leitura do painel | cache-aside com ElastiCache para a chave compartilhada, sem mudar com esta correção |
| Outros usuários podem continuar eventualmente consistentes | aceito pela hipótese declarada | o marcador é EXCLUSIVO do autor — chave composta por autor e recurso, nunca só o recurso |
| Mensagens podem chegar fora de ordem ou duplicadas | fila SQS padrão, não FIFO | `PutItem` idempotente por chave; invalidador usa DELETE, nunca SET |
| Rejeição de regra não pode continuar "confirmada" na tela do autor | obrigatório | o worker apaga o marcador do autor ao rejeitar a escrita |
| Sistema sobrevive a falha de nó do Redis | sem indisponibilidade de leitura | réplica automática do ElastiCache, e a função de leitura cai para o DynamoDB se o Redis não responder |
| Auditabilidade de quem alterou o quê e quando | obrigatório para disputas de pedido | `autorId` e `atualizadoEm` gravados no item; Streams carrega `OldImage`/`NewImage` completos |
Arquitetura mínima: o defeito visível
Este é o desenho que a Rota Livre já tinha construído para resolver capacidade — fila absorvendo pico de escrita, cache absorvendo leitura repetida — e ele está certo nisso. O laboratório começa por medir o efeito colateral dele, porque um número torna o defeito discutível, e "às vezes não atualiza" não.
- → salva a alteração (POST)
- → comando de escrita enfileirado, resposta 200 imediata
- → consumido sujeito a visibility timeout e à ordem que a fila entregar
- → só aqui o item passa a existir de fato
- → evento de INSERT ou MODIFY capturado
- → aciona a função ao detectar a mudança
- → apaga a chave compartilhada do item
- → atualiza a página em seguida (GET)
- → busca o valor mais recente
- → cache-aside: tenta a chave compartilhada antes do banco
- Fora da AWS
- Rede e entrega
- Integração de apps
- Compute
- Banco de dados
- Conceito de arquitetura
Nada aqui está mal configurado — a fila absorve pico de escrita e o cache tira carga de leitura repetida, os dois por bons motivos. O que ninguém decidiu de propósito é por quanto tempo a PRÓPRIA pessoa que acabou de salvar fica sem ver o resultado. Percorra os passos: a leitura do autor não sabe que é dele.
- A resposta 200 não é prova de que o item existe. A API valida só a forma do pedido e devolve sucesso assim que a mensagem está na fila — não quando o item foi gravado. Para o cliente HTTP, a diferença entre "aceito" e "aplicado" já desapareceu neste ponto.
- A fila entrega quando entrega. Fila padrão do SQS garante entrega pelo menos uma vez, não ordem nem latência fixa. Duas mensagens da mesma sessão podem ser processadas fora da ordem de envio, e o tempo até o worker pegar a mensagem varia com o backlog.
- O item só existe a partir deste instante. É dentro do worker que a validação de negócio de verdade acontece e o item é gravado. Antes deste ponto, uma leitura direta ao DynamoDB para aquele ID não encontraria a mudança — não é problema de cache, é ausência do dado.
- A mudança ainda precisa avisar o cache, por um caminho separado. O Streams e a função invalidadora formam um SEGUNDO relógio assíncrono, independente do primeiro. Mesmo depois de o item existir no DynamoDB, a chave velha continua servível no Redis até este caminho terminar.
- O autor lê pelo caminho comum, igual a qualquer outro usuário. Nada nesta leitura sabe que foi o mesmo usuário que acabou de escrever. Ela pode cair em qualquer um dos dois atrasos: se o worker ainda não processou, o item nem existe; se já existe mas a invalidação não chegou, o Redis devolve o valor antigo.
- Por que alguém desenha exatamente assim. Fila e cache resolvem problemas reais: a fila absorve pico de escrita sem sobrecarregar a tabela, o cache tira carga de leitura repetida. O que ninguém decidiu de propósito foi por quanto tempo o AUTOR da escrita fica sem ver o próprio resultado.
Dois atrasos diferentes, com a mesma aparência para o usuário
Se a leitura do autor cai ANTES de o worker processar a fila, o item simplesmente não existe ainda no DynamoDB — nenhuma leitura, forte ou eventual, encontra dado que não foi gravado. Se cai DEPOIS disso mas antes de a invalidação chegar, o item existe e o Redis está mentindo sobre ele. São causas diferentes, com correções diferentes, e tratá-las como "o mesmo bug de sempre" é o que faz alguém aplicar `ConsistentRead` e continuar vendo o problema — porque ele só ataca a segunda causa.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → salva a alteração (POST)
- → comando de escrita durável, para o worker validar de verdade
- → grava o marcador do autor (TTL curto), no mesmo request
- → consumido sujeito a visibility timeout; a durabilidade continua assíncrona
- → grava o item validado, fonte da verdade permanece aqui
- → evento de INSERT ou MODIFY capturado
- → aciona a invalidação, sem mudança em relação ao desenho anterior
- → apaga a chave COMPARTILHADA — nunca a chave de marcador de ninguém
- → atualiza a página em seguida (GET)
- → busca o valor, identificado como o mesmo usuário da escrita
- → primeiro pergunta pelo marcador do autor; só na ausência cai na chave comum
- → na ausência de marcador, cai no caminho comum de sempre (cache-aside)
- → publica se serviu o marcador (fresco) ou a chave comum (pode estar velha)
- Fora da AWS
- Rede e entrega
- Integração de apps
- Compute
- Banco de dados
- Conceito de arquitetura
- Gestão e governança
A escrita ganha um segundo destino, gravado no MESMO request que aceita o pedido — não mais um relógio a menos, e sim um caminho que não depende de relógio nenhum. Percorra os passos: cada peça nova resolve um requisito da seção anterior, e a durabilidade continua exatamente assíncrona como antes.
- O marcador nasce no mesmo request que a resposta. A função de escrita grava o marcador numa chave PRÓPRIA do Redis, de forma síncrona, antes de responder — não depende do worker nem da fila terem processado nada. Ainda depende do MESMO Redis do cache compartilhado estar saudável — diferença real do desenho, tratada na seção de segurança.
- A durabilidade continua assíncrona, e isso é intencional. A fila e o worker não mudaram: continuam absorvendo pico e validando de verdade antes de gravar no DynamoDB. Visibilidade do autor e durabilidade do dado são requisitos diferentes, e resolvê-los por caminhos diferentes é a decisão central deste laboratório.
- Para todo mundo, exceto o autor, nada mudou. A invalidação por Streams segue exatamente como no desenho anterior. Outros usuários continuam eventualmente consistentes — de propósito: dar a todo mundo o mesmo atalho do autor devolveria à arquitetura o custo que a fila e o cache existem para evitar.
- A leitura pergunta ao marcador antes de perguntar ao cache de todo mundo. A função de leitura busca primeiro o item do marcador daquele usuário. Se existir, devolve o valor dali — que É a escrita recém-feita, porque foi gravado no passo 1. Só na ausência do marcador ela cai na chave compartilhada, exatamente o caminho cache-aside do desenho mínimo.
- A prova de que funcionou está no painel, não na sensação de ter testado. Cada leitura publica se veio do marcador ou da chave comum. É essa métrica, não uma inspeção manual, que mostra a divergência do autor caindo a zero enquanto a de terceiros continua existindo por alguns segundos.
- O marcador expira de propósito. Passado o TTL do item, o autor volta a ler pelo caminho comum, igual a qualquer usuário. Isso só é seguro porque o TTL foi calibrado para ser MAIOR que a janela de invalidação medida — a saída do atalho é silenciosa e não deveria ser notada.
Por que um mecanismo só resolve os dois atrasos
O marcador não pergunta "a fila já processou?" nem "o cache já foi invalidado?" — ele simplesmente CONTÉM o valor novo, gravado no instante em que a escrita foi aceita. Isso o torna imune aos dois relógios ao mesmo tempo: não importa se o worker ainda não rodou ou se a invalidação ainda não chegou, a leitura do autor encontra o marcador antes de perguntar a qualquer um dos dois.
Como funciona, ponta a ponta: os dois relógios na mesma linha do tempo
Os nomes dos campos do evento de Streams não são jargão: são a prova de que a mudança realmente ocorreu e de qual era o valor anterior. eventName diz o tipo de mudança; NewImage e OldImage existem porque a tabela está configurada com NEW_AND_OLD_IMAGES.
evento-streams-pedido.json — o que o Lambda invalidador recebe NewImage/OldImage só existem porque a tabela usa StreamViewType NEW_AND_OLD_IMAGES; com KEYS_ONLY este evento não teria o campo "status".
{
"eventID": "3b2c1de9f7a84e6a9c3d1f6b2a7e5c41",
"eventName": "MODIFY",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": { "pedidoId": { "S": "PED-93214" } },
"NewImage": {
"pedidoId": { "S": "PED-93214" },
"status": { "S": "confirmado" },
"autorId": { "S": "usr-7781" },
"atualizadoEm": { "S": "2026-08-07T18:04:02.118Z" }
},
"OldImage": {
"pedidoId": { "S": "PED-93214" },
"status": { "S": "pendente" },
"autorId": { "S": "usr-7781" },
"atualizadoEm": { "S": "2026-08-07T18:03:58.004Z" }
},
"SequenceNumber": "820400000000015221234567",
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:111122223333:table/Pedidos/stream/2026-08-07T18:00:00.000"
}
Nem os Streams são garantidamente instantâneos
A documentação descreve o comportamento do DynamoDB Streams como próximo do tempo real, sem publicar um número de latência garantido — e a leitura do próprio stream é, por definição, eventualmente consistente, no mesmo sentido em que uma leitura padrão da tabela é. Tratar o Streams como "instantâneo" é o mesmo erro, uma camada acima, de tratar o cache como sempre certo.
As decisões, e o que se perde em cada uma
📋 Restaurantes parceiros de um marketplace de entregas atualizam o status do próprio pedido pelo tablet, e o mesmo aplicativo mostra o status logo depois. A escrita já passa por fila e cache por decisão anterior (capacidade); o time não pode voltar a escrever direto na tabela sem perder essa proteção.
O marcador resolve os dois relógios assíncronos com um mecanismo só, porque é escrito no mesmo request que aceita a alteração — não depende de a fila nem o worker terem processado nada. Ele custa uma escrita a mais no Redis por requisição de escrita, e nada muda para quem não é o autor: a fila continua absorvendo pico, o cache continua eventualmente consistente para todo mundo, e o time não perde nenhuma das duas proteções.
Alt: ConsistentRead=true no DynamoDB para o autor — resolve a leitura eventualmente consistente da TABELA, mas nada resolve a espera da fila — se o worker ainda não processou, mesmo uma leitura fortemente consistente não encontra o item. É a armadilha mais comum: tratar como problema de leitura do banco algo que também é atraso de fila.
Alt: Ler sempre do DynamoDB, sem cache, para todo mundo — elimina a inconsistência do CACHE mas não a da fila, e joga toda a carga de leitura repetida de volta na tabela — o problema que o cache existia para resolver volta inteiro, para resolver só metade do problema original.
Alt: Escrita síncrona, sem fila — resolve os dois atrasos de uma vez, mas devolve ao cliente a latência real de validar e gravar, e tira da arquitetura a proteção contra pico de escrita que a fila oferecia — troca um problema de UX por um de capacidade.
Alt: WebSocket empurrando a confirmação real para o cliente — resolve de forma mais elegante do ponto de vista do cliente, mas exige conexão persistente e estado de sessão no servidor — investimento de infraestrutura maior que o marcador para o mesmo resultado percebido; é o nível 3 da evolução deste laboratório, não o ponto de partida.
O erro de raciocínio mais caro deste laboratório
Achar que `ConsistentRead=true` no DynamoDB "resolve consistência eventual" e parar por aí. Ele resolve a leitura da TABELA — se a única fonte de atraso fosse a réplica de leitura interna do DynamoDB, bastaria. Nesta arquitetura, com uma fila entre a API e a persistência, o atraso pode estar inteiramente ANTES de o item existir, e nenhuma configuração de leitura encontra o que ainda não foi escrito.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde grava o marcador | ElastiCache, chave por autor + recurso, TTL curto | DynamoDB com atributo de expiração; sessão em memória do processo | leitura em microssegundos e expiração automática sem job de limpeza | é dado descartável por definição — se o Redis reiniciar, o autor só perde o atalho, nunca o dado real |
| O que o invalidador faz com a chave comum | DELETE da chave | SET com o novo valor vindo do evento do Streams | apagar é idempotente independente da ordem de chegada; SET não é — mensagem fora de ordem pode deixar a chave com um valor mais antigo que o atual | uma leitura imediatamente após o DELETE paga o custo de um cache-miss a mais |
| Como a leitura decide a fonte | marcador primeiro, chave comum depois, banco por último | sempre banco para o próprio autor durante N segundos, sem cache nenhum | caminho único e barato quando o marcador expira ou nunca existiu | uma consulta a mais na cadeia de decisão dentro da função |
| TTL do marcador | curto, derivado da janela de invalidação medida | longo, "para garantir" | o marcador não é fonte da verdade — mantê-lo vivo além do necessário arrisca servir um valor que o worker depois rejeitou | se a janela real variar com um pico de fila, um TTL curto demais expõe o autor de novo |
| O que fazer quando o worker rejeita a escrita | apagar o marcador imediatamente | deixar o marcador expirar sozinho | o autor não deveria continuar vendo, por mais alguns segundos, um valor que o sistema já sabe que é inválido | exige que o worker tenha permissão de escrita no Redis — mais uma responsabilidade nele |
Construir: a tabela, a fila e o cache
Os três recursos onde um dos dois relógios começa ou termina. Nenhum deles é novo neste laboratório — o que muda é a atenção dada a cada um: o Streams precisa estar ligado, a fila precisa ser padrão (não FIFO) de propósito, e o Redis precisa das duas chaves, não só de uma.
# dados.tf — a tabela, a fila e o cache: os tres pontos onde um relogio comeca ou termina
resource "aws_dynamodb_table" "pedidos" {
name = "${var.projeto}-pedidos"
billing_mode = "PAY_PER_REQUEST"
hash_key = "pedidoId"
attribute {
name = "pedidoId"
type = "S"
}
# Sem isto nao ha evento para o invalidador ouvir. NEW_AND_OLD_IMAGES porque
# o invalidador so precisa da CHAVE para apagar o cache, mas o OldImage fica
# disponivel para auditoria — fora do escopo deste laboratorio, mas caro de
# adicionar depois se voce escolher KEYS_ONLY agora.
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
point_in_time_recovery {
enabled = true
}
}
# Fila PADRAO, nao FIFO: aceitar mensagem fora de ordem e o pressuposto deste
# desenho, nao uma limitacao contornada. FIFO custaria taxa de mensagem maior
# e um grupo por recurso so para resolver um problema que o invalidador ja
# resolve de outro jeito — apagando a chave em vez de escrever nela.
resource "aws_sqs_queue" "comandos_escrita" {
name = "${var.projeto}-comandos-escrita"
# Tempo para o worker processar UM lote antes da mensagem reaparecer para
# outro consumidor. Maior que o pior caso medido de validacao + PutItem;
# menor que isso reprocessa mensagem que ainda esta em voo (duplicata).
visibility_timeout_seconds = 30
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.comandos_escrita_dlq.arn
maxReceiveCount = 5
})
}
# Mensagem que falha 5 vezes nao fica reprocessando para sempre: vai para a
# DLQ, e um alarme (secao de observabilidade) avisa alguem. Sem isto, uma
# mensagem invalida fica em loop de visibilidade indefinidamente.
resource "aws_sqs_queue" "comandos_escrita_dlq" {
name = "${var.projeto}-comandos-escrita-dlq"
message_retention_seconds = 1209600 # 14 dias, o teto do SQS
}
resource "aws_elasticache_replication_group" "cache" {
replication_group_id = "${var.projeto}-cache"
description = "Chave comum do item e marcador de leitura-propria-escrita"
engine = "redis"
node_type = "cache.t4g.micro"
num_cache_clusters = 2 # primario + replica; leitura nao para se o primario falhar
automatic_failover_enabled = true
subnet_group_name = aws_elasticache_subnet_group.cache.name
security_group_ids = [aws_security_group.cache.id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
}
resource "aws_elasticache_subnet_group" "cache" {
name = "${var.projeto}-cache"
subnet_ids = aws_subnet.privada[*].id
}
# O grupo do cache so aceita trafego do grupo das funcoes — nunca faixa de IP.
# O Redis nao tem politica de recurso como o DynamoDB; o security group E o
# controle de acesso aqui.
resource "aws_security_group" "cache" {
name = "${var.projeto}-cache"
vpc_id = aws_vpc.principal.id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.funcoes.id]
}
}
Fila FIFO pareceria a correção óbvia, e não é
Garantir ordem por chave de grupo resolveria o risco de o invalidador processar eventos fora de ordem — mas custa taxa de mensagem maior, throughput menor, e o invalidador já resolve o mesmo risco de outro jeito, apagando a chave em vez de escrevê-la. Pagar por ordem quando você não precisa de ordem é comprar uma garantia que o desenho não usa.
Construir: quatro funções, dois caminhos assíncronos e um marcador síncrono
Escrita e leitura mudam nesta seção; worker e invalidador continuam com a mesma responsabilidade do desenho mínimo — e essa estabilidade é o ponto: a correção não mexeu na durabilidade, só na visibilidade.
# funcoes.tf — quatro funcoes: duas no caminho sincrono, duas nos dois relogios assincronos
resource "aws_security_group" "funcoes" {
name = "${var.projeto}-funcoes"
vpc_id = aws_vpc.principal.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # so saida; a funcao nao recebe conexao direta
}
}
data "aws_iam_policy_document" "assume_lambda" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
# ── Escrita: enfileira o comando E grava o marcador, na mesma invocacao ─────
resource "aws_iam_role" "escrita" {
name = "${var.projeto}-escrita"
assume_role_policy = data.aws_iam_policy_document.assume_lambda.json
}
data "aws_iam_policy_document" "escrita" {
statement {
effect = "Allow"
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.comandos_escrita.arn]
}
statement {
effect = "Allow"
actions = ["ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface"]
# Lambda em VPC precisa gerenciar a propria interface de rede; a acao nao
# aceita recurso especifico — e por isso que aparece com "*", nao preguica.
resources = ["*"]
}
}
resource "aws_iam_role_policy" "escrita" {
role = aws_iam_role.escrita.id
policy = data.aws_iam_policy_document.escrita.json
}
resource "aws_lambda_function" "escrita" {
function_name = "${var.projeto}-escrita"
role = aws_iam_role.escrita.arn
runtime = "dotnet8"
handler = "Funcoes::Consistencia.EscritaFunction::ManipularAsync"
filename = data.archive_file.funcoes.output_path
timeout = 10
memory_size = 256
vpc_config {
subnet_ids = aws_subnet.privada[*].id
security_group_ids = [aws_security_group.funcoes.id]
}
environment {
variables = {
FILA_URL = aws_sqs_queue.comandos_escrita.url
REDIS_ENDPOINT = aws_elasticache_replication_group.cache.primary_endpoint_address
# Deve ser MAIOR que a janela de invalidacao medida na prova 4. Comeca
# conservador; recalibre com o numero real do seu ambiente.
TTL_MARCADOR_S = "10"
}
}
}
# ── Worker: consome a fila, valida de verdade, grava a fonte da verdade ─────
resource "aws_lambda_event_source_mapping" "worker" {
event_source_arn = aws_sqs_queue.comandos_escrita.arn
function_name = aws_lambda_function.worker.arn
batch_size = 10
}
resource "aws_lambda_function" "worker" {
function_name = "${var.projeto}-worker"
role = aws_iam_role.worker.arn
runtime = "dotnet8"
handler = "Funcoes::Consistencia.WorkerFunction::ProcessarAsync"
filename = data.archive_file.funcoes.output_path
timeout = 30 # cobre o visibility_timeout da fila com folga
memory_size = 256
vpc_config {
subnet_ids = aws_subnet.privada[*].id
security_group_ids = [aws_security_group.funcoes.id]
}
}
# ── Invalidador: ouve o Streams, apaga a chave comum ────────────────────────
resource "aws_lambda_event_source_mapping" "invalidador" {
event_source_arn = aws_dynamodb_table.pedidos.stream_arn
function_name = aws_lambda_function.invalidador.arn
starting_position = "LATEST"
}
resource "aws_lambda_function" "invalidador" {
function_name = "${var.projeto}-invalidador"
role = aws_iam_role.invalidador.arn
runtime = "dotnet8"
handler = "Funcoes::Consistencia.InvalidadorFunction::InvalidarAsync"
filename = data.archive_file.funcoes.output_path
vpc_config {
subnet_ids = aws_subnet.privada[*].id
security_group_ids = [aws_security_group.funcoes.id]
}
}
# ── Leitura: marcador primeiro, chave comum depois, banco por ultimo ────────
resource "aws_lambda_function" "leitura" {
function_name = "${var.projeto}-leitura"
role = aws_iam_role.leitura.arn
runtime = "dotnet8"
handler = "Funcoes::Consistencia.LeituraFunction::ManipularAsync"
filename = data.archive_file.funcoes.output_path
vpc_config {
subnet_ids = aws_subnet.privada[*].id
security_group_ids = [aws_security_group.funcoes.id]
}
}
# Os papeis de worker, invalidador e leitura seguem o mesmo padrao do de
# escrita — dynamodb:PutItem/GetItem restrito ao ARN da tabela, mais as tres
# acoes de ENI acima — e foram omitidos aqui por repeticao, nao por estarem
# ausentes: cada funcao tem o SEU papel, nunca um papel compartilhado entre as
# quatro, porque o menor privilegio de cada uma e diferente.
resource "aws_apigatewayv2_api" "http" {
name = "${var.projeto}-http"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_route" "escrita" {
api_id = aws_apigatewayv2_api.http.id
route_key = "POST /pedidos/{id}"
target = "integrations/${aws_apigatewayv2_integration.escrita.id}"
}
resource "aws_apigatewayv2_route" "leitura" {
api_id = aws_apigatewayv2_api.http.id
route_key = "GET /pedidos/{id}"
target = "integrations/${aws_apigatewayv2_integration.leitura.id}"
}
O `*` que aparece na política, e por que ele se justifica
`ec2:CreateNetworkInterface`, `DescribeNetworkInterfaces` e `DeleteNetworkInterface` são as três ações que uma Lambda em VPC precisa para gerenciar a própria interface de rede, e nenhuma delas aceita recurso específico — são operações de conta, não de objeto. A regra não é "nunca use `*`"; é "todo `*` tem de ter uma frase explicando por que não pode ser mais estreito". Sem a frase, é preguiça; com ela, é decisão.
Construir: o marcador que resolve os dois atrasos
O handler de escrita grava dois destinos antes de responder: a fila, para a durabilidade que já existia, e o marcador, que é a peça nova. A ordem dentro do código não importa para o resultado — o que importa é que os dois acontecem no MESMO request, antes do `return`.
// Funcoes.cs — os dois handlers que resolvem o sintoma do usuario, sem mudar
// a arquitetura duravel por baixo
using System.Text.Json;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.SQS;
using Amazon.SQS.Model;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.CloudWatch;
using Amazon.CloudWatch.Model;
using StackExchange.Redis;
namespace Consistencia;
public class EscritaFunction
{
private static readonly IAmazonSQS Sqs = new AmazonSQSClient();
private static readonly Lazy<ConnectionMultiplexer> Redis = new(() =>
ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_ENDPOINT")!));
private static readonly int TtlMarcadorSegundos =
int.Parse(Environment.GetEnvironmentVariable("TTL_MARCADOR_S") ?? "10");
public async Task<APIGatewayHttpApiV2ProxyResponse> ManipularAsync(
APIGatewayHttpApiV2ProxyRequest req)
{
var pedidoId = req.PathParameters["id"];
// A identidade do autor vem do contexto de autorizacao ja validado
// pelo API Gateway (JWT authorizer) — fora do escopo deste
// laboratorio, que assume que a identidade ja chega resolvida.
var autorId = req.RequestContext.Authorizer.Jwt.Claims["sub"];
var corpo = JsonSerializer.Deserialize<PedidoEntrada>(req.Body)!;
// Validacao de FORMA apenas. A validacao de REGRA de negocio — que
// pode rejeitar o pedido — acontece no worker, depois da fila. Essa
// divisao e o que permite responder rapido sem mentir sobre o que
// ja foi checado.
if (string.IsNullOrWhiteSpace(corpo.Status))
return Resposta(400, new { erro = "status obrigatorio" });
var comando = new
{
pedidoId,
autorId,
corpo.Status,
enviadoEm = DateTimeOffset.UtcNow,
};
var comandoJson = JsonSerializer.Serialize(comando);
await Sqs.SendMessageAsync(new SendMessageRequest
{
QueueUrl = Environment.GetEnvironmentVariable("FILA_URL"),
MessageBody = comandoJson,
});
// O passo que resolve os DOIS atrasos do desenho minimo: grava o
// resultado que o autor vai querer ler, no MESMO request que aceitou
// a escrita — sem esperar fila nem worker. TTL curto: o marcador nao
// e fonte da verdade, e um atalho com prazo de validade.
//
// Fail-open, igual a Leitura: se o Redis estiver fora do ar aqui, a
// ESCRITA (que ja foi enfileirada acima) nao pode falhar por causa de
// um atalho de visibilidade. O autor so perde o marcador, nao a escrita.
try
{
var db = Redis.Value.GetDatabase();
await db.StringSetAsync(
$"rw:{autorId}:{pedidoId}",
comandoJson,
TimeSpan.FromSeconds(TtlMarcadorSegundos));
}
catch (RedisConnectionException)
{
// Sem marcador, o autor cai no caminho comum ate a fila processar —
// pior UX, nunca escrita perdida: a mensagem ja esta no SQS.
}
return Resposta(200, comando);
}
private static APIGatewayHttpApiV2ProxyResponse Resposta(int codigo, object corpo) => new()
{
StatusCode = codigo,
Body = JsonSerializer.Serialize(corpo),
Headers = new Dictionary<string, string> { ["Content-Type"] = "application/json" },
};
}
public record PedidoEntrada(string Status);
public class LeituraFunction
{
private static readonly Lazy<ConnectionMultiplexer> Redis = new(() =>
ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_ENDPOINT")!));
private static readonly IAmazonDynamoDB Dynamo = new AmazonDynamoDBClient();
private static readonly IAmazonCloudWatch Metricas = new AmazonCloudWatchClient();
public async Task<APIGatewayHttpApiV2ProxyResponse> ManipularAsync(
APIGatewayHttpApiV2ProxyRequest req)
{
var pedidoId = req.PathParameters["id"];
var autorId = req.RequestContext.Authorizer.Jwt.Claims["sub"];
IDatabase db;
try { db = Redis.Value.GetDatabase(); }
catch (RedisConnectionException)
{
// Falha do Redis nao vira 500 para o leitor: cai direto para o
// banco. Fail-open para consistencia, nao fail-closed para erro.
return await LerDoBancoAsync(pedidoId, "RedisIndisponivel");
}
// 1) O marcador do PROPRIO autor tem prioridade. Se existir, E a
// escrita que ele acabou de fazer — nao ha condicao de corrida
// possivel aqui, porque foi ele mesmo quem gravou.
var marcador = await db.StringGetAsync($"rw:{autorId}:{pedidoId}");
if (marcador.HasValue)
{
await PublicarMetricaAsync("MarcadorAutor");
return Resposta(200, marcador.ToString());
}
// 2) Chave compartilhada, igual para qualquer usuario. Pode estar
// desatualizada se a invalidacao ainda nao chegou — aceitavel para
// quem NAO e o autor da ultima escrita.
var chaveComum = $"item:{pedidoId}";
var comum = await db.StringGetAsync(chaveComum);
if (comum.HasValue)
{
await PublicarMetricaAsync("ChaveComum");
return Resposta(200, comum.ToString());
}
// 3) Cache-miss: busca no DynamoDB e repovoa a chave comum.
return await LerDoBancoAsync(pedidoId, "BancoAposMiss", db, chaveComum);
}
private async Task<APIGatewayHttpApiV2ProxyResponse> LerDoBancoAsync(
string pedidoId, string origemMetrica, IDatabase? db = null, string? chaveParaRepovoar = null)
{
// Leitura padrao (eventualmente consistente): a mesma tabela nao tem
// por que custar o dobro so porque o cache falhou ou estava vazio.
var item = await Dynamo.GetItemAsync(new GetItemRequest
{
TableName = "Pedidos",
Key = new() { ["pedidoId"] = new AttributeValue(pedidoId) },
});
if (item.Item.Count == 0) return Resposta(404, new { erro = "nao encontrado" });
var valor = JsonSerializer.Serialize(item.Item);
if (db is not null && chaveParaRepovoar is not null)
await db.StringSetAsync(chaveParaRepovoar, valor, TimeSpan.FromMinutes(5));
await PublicarMetricaAsync(origemMetrica);
return Resposta(200, valor);
}
// A prova da secao de implantacao le esta metrica, nao o log — e a
// diferenca entre testar e medir.
private static Task PublicarMetricaAsync(string origem) =>
Metricas.PutMetricDataAsync(new PutMetricDataRequest
{
Namespace = "Consistencia/Leitura",
MetricData = new List<MetricDatum> { new()
{
MetricName = "OrigemDaResposta",
Dimensions = new List<Dimension> { new() { Name = "Origem", Value = origem } },
Value = 1,
Unit = StandardUnit.Count,
}},
});
private static APIGatewayHttpApiV2ProxyResponse Resposta(int codigo, object corpo) => new()
{
StatusCode = codigo,
Body = corpo is string s ? s : JsonSerializer.Serialize(corpo),
Headers = new Dictionary<string, string> { ["Content-Type"] = "application/json" },
};
}
O marcador não é fonte da verdade, é atalho de visibilidade
Se o worker rejeitar a escrita depois, o marcador — que já foi lido por um GET entre o momento da resposta 200 e a rejeição — mostrou ao autor um valor que o sistema decidiu invalidar. É por isso que o worker apaga o marcador ao rejeitar, e por isso o TTL é curto: quanto menor a janela em que o marcador pode estar "errado", menor o dano de uma UI otimista que confiou nele.
Construir: o worker e o invalidador, sem mudança de propósito
A validação de regra e a invalidação de cache continuam fazendo exatamente o que faziam no desenho mínimo. A única linha nova é o `KeyDeleteAsync` do marcador quando o worker rejeita — o resto é o mesmo código, e isso é deliberado: menos mudança é menos risco.
// Funcoes.cs (continuacao) — a validacao de verdade e a invalidacao; nada
// aqui muda em relacao ao desenho minimo, e essa estabilidade e proposital
using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.SQSEvents;
using Amazon.Lambda.DynamoDBEvents;
using StackExchange.Redis;
namespace Consistencia;
public class WorkerFunction
{
private static readonly IAmazonDynamoDB Dynamo = new AmazonDynamoDBClient();
private static readonly Lazy<ConnectionMultiplexer> Redis = new(() =>
ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_ENDPOINT")!));
public async Task ProcessarAsync(SQSEvent evento)
{
foreach (var msg in evento.Records)
{
var comando = JsonSerializer.Deserialize<ComandoEscrita>(msg.Body)!;
// A validacao de REGRA acontece aqui, nao na API. Se falhar, o
// item nunca chega ao DynamoDB — e o marcador que a Escrita ja
// gravou continua de pe ate o TTL expirar, mostrando ao autor um
// valor que o sistema decidiu recusar. Por isso apagamos.
if (!RegraDeStatusValida(comando.Status))
{
var db = Redis.Value.GetDatabase();
await db.KeyDeleteAsync($"rw:{comando.AutorId}:{comando.PedidoId}");
continue; // mensagem processada; a rejeicao e o resultado
}
// Escrita idempotente por natureza: PutItem com o mesmo
// pedidoId sobrescreve. Se o SQS entregar a mesma mensagem duas
// vezes (at-least-once), o efeito final e o mesmo.
await Dynamo.PutItemAsync(new PutItemRequest
{
TableName = "Pedidos",
Item = new()
{
["pedidoId"] = new AttributeValue(comando.PedidoId),
["autorId"] = new AttributeValue(comando.AutorId),
["status"] = new AttributeValue(comando.Status),
["atualizadoEm"] = new AttributeValue(DateTimeOffset.UtcNow.ToString("O")),
},
});
}
}
private static bool RegraDeStatusValida(string status) =>
status is "pendente" or "confirmado" or "cancelado";
}
public record ComandoEscrita(string PedidoId, string AutorId, string Status, DateTimeOffset EnviadoEm);
public class InvalidadorFunction
{
private static readonly Lazy<ConnectionMultiplexer> Redis = new(() =>
ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_ENDPOINT")!));
public async Task InvalidarAsync(DynamoDBEvent evento)
{
var db = Redis.Value.GetDatabase();
foreach (var registro in evento.Records)
{
if (registro.EventName is not ("INSERT" or "MODIFY")) continue;
var pedidoId = registro.Dynamodb.Keys["pedidoId"].S;
// DELETE, nunca SET. A fila que carrega este evento e a mesma
// maquina de Streams->Lambda, sujeita a nova tentativa e a
// reprocessamento: se dois eventos do mesmo item forem
// processados fora de ordem, escrever o "novo valor" poderia
// deixar no cache um dado MAIS VELHO que o atual. Apagar e
// idempotente — nao importa a ordem, o resultado final e sempre
// "sem chave", e a proxima leitura busca a verdade no DynamoDB.
await db.KeyDeleteAsync($"item:{pedidoId}");
}
}
}
Por que o comentário sobre ordem está no código, não só no módulo
É o tipo de decisão que alguém "otimiza" seis meses depois, trocando `KeyDeleteAsync` por um `StringSetAsync` com o valor do `NewImage` — parece evitar um cache-miss e passa no teste local, onde eventos raramente colidem. O comentário existe para essa pessoa, não para quem está lendo o módulo hoje.
Implantar, e provar com número — não com "parece que funcionou"
Cinco provas. A primeira mede o defeito no desenho mínimo, para que ele deixe de ser opinião. As quatro seguintes rodam contra o desenho de produção.
#!/usr/bin/env bash
# medir-divergencia-autor.sh — a prova do defeito, ANTES da correção
# Roda contra o desenho MÍNIMO (sem marcador). Escreve, e imediatamente tenta
# ler como o MESMO autor, repetidas vezes, contando quantas leituras não
# batem com o que acabou de ser escrito.
set -euo pipefail
BASE_URL="$(terraform output -raw url_api)"
TOKEN="$(obter-token-teste.sh usr-7781)" # helper local, fora do escopo
PEDIDO="ped-$(date +%s)"
curl -s -X POST "$BASE_URL/pedidos/$PEDIDO" \
-H "Authorization: Bearer $TOKEN" -d '{"status":"confirmado"}' > /dev/null
divergentes=0; total=50
for i in $(seq 1 $total); do
resp=$(curl -s "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN")
echo "$resp" | grep -q '"status":"confirmado"' || divergentes=$((divergentes+1))
done
echo "$divergentes de $total leituras do PRÓPRIO autor não viram a escrita"
# Medido no ambiente de exemplo, com a fila em backlog normal: 21 de 50
# (42%). O número varia com o backlog da fila no instante do teste — meça
# o seu, não copie este.
#!/usr/bin/env bash
# provas.sh — quatro medições contra o desenho de PRODUÇÃO (com marcador)
BASE_URL="$(terraform output -raw url_api)"
TOKEN_A="$(obter-token-teste.sh usr-7781)"
TOKEN_B="$(obter-token-teste.sh usr-4402)" # um SEGUNDO usuário, que não escreveu nada
# ── Prova 1: divergência do autor, com o marcador ativo ──────────────────────
PEDIDO="ped-$(date +%s)"
curl -s -X POST "$BASE_URL/pedidos/$PEDIDO" \
-H "Authorization: Bearer $TOKEN_A" -d '{"status":"confirmado"}' > /dev/null
divergentes=0
for i in $(seq 1 50); do
resp=$(curl -s "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN_A")
echo "$resp" | grep -q '"Status":"confirmado"' || divergentes=$((divergentes+1))
done
echo "Prova 1 — divergência do autor: $divergentes de 50 (esperado: 0)"
# ── Prova 2: outros usuários continuam eventualmente consistentes ───────────
# Isto NÃO é um bug remanescente: é a decisão central do módulo funcionando.
resp_b=$(curl -s "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN_B")
echo "Prova 2 — leitura de outro usuário logo após a escrita: $resp_b"
echo " (esperado: pode mostrar 'pendente' por alguns segundos — é o design, não a falha)"
# ── Prova 3: a métrica publicada confirma a origem de cada resposta ─────────
aws cloudwatch get-metric-statistics --namespace Consistencia/Leitura \
--metric-name OrigemDaResposta --dimensions Name=Origem,Value=MarcadorAutor \
--start-time "$(date -u -d '5 min ago' +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--period 300 --statistics Sum --output text
echo "Prova 3 — esperado: Sum próximo de 50 (as leituras do autor na prova 1)"
# ── Prova 4: a janela real de invalidação, para calibrar o TTL do marcador ──
# Compara o instante do PutItem (atualizadoEm) com o instante do DELETE nos
# logs do invalidador. O TTL do marcador tem de ser MAIOR que este p99.
aws logs filter-log-events --log-group-name /aws/lambda/ffv-lab-invalidador \
--filter-pattern "apagou" --query 'events[].message' --output text
echo "Prova 4 — o maior intervalo observado é o piso do TTL do marcador"
# ── Prova 5: DELETE é idempotente diante de eventos fora de ordem ───────────
# Duas escritas rápidas no mesmo item; a leitura final (após o marcador
# expirar) tem de bater com o ÚLTIMO estado gravado no DynamoDB, não importa
# a ordem em que os dois eventos do Streams chegaram ao invalidador.
curl -s -X POST "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN_A" \
-d '{"status":"confirmado"}' > /dev/null &
curl -s -X POST "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN_A" \
-d '{"status":"cancelado"}' > /dev/null &
wait; sleep 12 # aguarda o marcador expirar (TTL=10s) e a invalidação assentar
final=$(curl -s "$BASE_URL/pedidos/$PEDIDO" -H "Authorization: Bearer $TOKEN_B")
verdade=$(aws dynamodb get-item --table-name ffv-lab-pedidos \
--key "{\"pedidoId\":{\"S\":\"$PEDIDO\"}}" --query 'Item.status.S' --output text)
echo "Prova 5 — cache: $final | DynamoDB: $verdade (têm de bater)"
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Divergência do autor, sem marcador | laço de GET logo após o POST, 50 tentativas | um número maior que zero — o defeito é real e mensurável | zero aqui não prova que o desenho mínimo está certo; prova que o teste rodou fora do horário de pico |
| 2 · Divergência do autor, com marcador | mesmo laço, contra o desenho de produção | 0 de 50 | qualquer valor > 0 indica TTL menor que a janela real, ou marcador com chave errada |
| 3 · Outros usuários continuam eventuais | GET de um segundo usuário logo após a escrita | pode devolver o valor antigo por alguns segundos | se SEMPRE devolver o valor novo, o marcador vazou para quem não é o autor — falha de segurança, não só de design |
| 4 · A janela real de invalidação | diff entre `atualizadoEm` e o log do DELETE | um número — é o piso do TTL do marcador | se o maior valor observado for maior que o TTL configurado, o marcador expira cedo demais |
| 5 · DELETE é idempotente sob desordem | duas escritas rápidas no mesmo item, leitura após TTL | cache e DynamoDB concordam no valor final | divergência aqui indica que o invalidador foi trocado para SET em algum ponto |
Quebrar de propósito: três falhas e o diagnóstico
As três produzem o mesmo sintoma superficial — "o autor não vê a própria mudança" — depois de o marcador já existir. O que as separa é onde o marcador falhou.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| TTL menor que a janela real | configure `TTL_MARCADOR_S = 2` com a fila em backlog de 5 s | divergência volta a aparecer, mas só perto do fim do TTL — parece intermitente de novo | compare o TTL configurado com o maior valor da prova 4 | recalibrar o TTL com o p99 MEDIDO, não com um número redondo |
| Invalidador trocado de DELETE para SET | altere `InvalidarAsync` para escrever o `NewImage` do evento em vez de apagar a chave | divergência esporádica para leitores QUAISQUER, que "conserta sozinha" na próxima escrita | compare o `SequenceNumber` do evento processado por último com o valor salvo no cache | reverter para `KeyDeleteAsync` |
| Marcador sem `autorId` na chave | grave o marcador como `rw:{pedidoId}` em vez de `rw:{autorId}:{pedidoId}` | um SEGUNDO usuário lendo o mesmo recurso vê o valor que o primeiro autor acabou de escrever | teste com dois usuários simultâneos no mesmo recurso | sempre compor a chave com autor e recurso — nunca só o recurso |
A terceira falha não é só um bug de UX
Se o corpo do marcador contiver qualquer dado que o segundo usuário não deveria ver — um campo interno, um preço negociado, uma nota do autor — a chave sem `autorId` transforma um defeito de consistência em vazamento de dado entre usuários. É a diferença entre "a tela está desatualizada" e "a tela mostrou algo que não era seu".
Um usuário salva um pedido e, meio segundo depois, atualiza a página e ainda vê o status antigo. A equipe de suporte registra como bug. Dado que a escrita passa por uma fila SQS antes de chegar ao DynamoDB, qual é a explicação mais provável?
Segurança: o marcador é um caminho novo, e caminho novo é superfície nova
Nada aqui expõe o Redis à internet nem muda a fronteira de rede — o risco introduzido é lógico: uma chave mal composta pode servir o dado de um autor para outro leitor.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Marcador vaza entre usuários por chave sem `autorId` | baixa | alto | chave sempre composta por `autorId` + `pedidoId`, nunca só o recurso | teste automatizado com dois usuários simultâneos; auditoria de padrão de chaves no Redis | corrigir a chave e invalidar (`SCAN` + `DEL`) todos os marcadores existentes do padrão errado |
| Identidade do autor confiada sem verificação | baixa | alto | JWT authorizer nativo do API Gateway, nunca decodificação manual do token na função | CloudWatch de tentativas rejeitadas pelo authorizer | revisar a configuração do authorizer, não a lógica da função |
| Mensagem processada mais de uma vez (at-least-once) | média | médio | `PutItem` idempotente por natureza — mesmo `pedidoId` sobrescreve com o mesmo resultado | `SequenceNumber` ou `MessageId` duplicado nos logs | nenhuma: é o comportamento esperado e já coberto pelo design |
| Marcador gravado sem TTL por falha de configuração | baixa | médio | TTL sempre explícito no `StringSetAsync`, nunca chave sem expiração | script de auditoria rodando `TTL rw:*` no Redis — qualquer `-1` é falha | `EXPIRE` manual na chave e correção do código |
| DLQ acumulando comandos rejeitados sem ninguém olhar | média | baixo a médio | alarme de profundidade da DLQ acima de zero | `ApproximateNumberOfMessagesVisible` da fila de DLQ | processar manualmente ou descartar com justificativa registrada |
| Redis alcançável além do grupo de funções | baixa | alto | security group do cache só libera o SG das funções — nunca faixa de IP | IAM Access Analyzer / Config rule sobre o security group | corrigir a regra; girar a validação se havia caminho sem TLS |
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Quantas leituras do autor vieram do marcador, da chave comum ou do banco? | custom: `OrigemDaResposta` por dimensão `Origem` | é a métrica que prova a correção — deve dominar `MarcadorAutor` logo após cada escrita | nenhum limiar de alarme; é painel de leitura, não de disparo |
| A fila de comandos está atrasando? | `ApproximateNumberOfMessages` | crescente sem baixar alonga a janela sem marcador em caso de falha do Redis | crescimento sustentado por mais de 5 min |
| Quantas mensagens foram para a DLQ? | `ApproximateNumberOfMessagesVisible` da DLQ | comando que a regra de negócio recusa repetidamente, ou payload malformado | qualquer valor > 0 |
| Qual o atraso entre escrita e invalidação? | diff entre `atualizadoEm` e o log do DELETE | é o número que calibra o TTL do marcador | p99 acima do TTL configurado |
| A taxa de acerto da chave comum caiu? | razão entre `ChaveComum` e `BancoAposMiss` | TTL da chave comum mal calibrado, ou invalidação disparando cedo demais | queda abrupta em relação à média da semana |
| O worker está rejeitando mais comandos que o normal? | contagem de `RegraDeStatusValida = false` | pico correlacionado com um deploy do cliente indica contrato quebrado | acima de 1% dos comandos processados |
A métrica que engana se você olhar sozinha
`MarcadorAutor` alto é bom logo após uma escrita, mas alto o TEMPO TODO indica TTL longo demais — o marcador virou cache permanente do autor, disfarçado de correção pontual. Compare sempre com o instante da última escrita daquele usuário, não com o total isolado.
Escala: 10, 10 mil, 1 milhão
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 escritas/dia | worker processa quase instantaneamente; o marcador quase nunca é necessário | nada — é o cenário de desenvolvimento | nada |
| 10 mil escritas/dia, pico de 15/s | a fila absorve a rajada; sem marcador, a janela cresce no pico | é justamente quando o suporte recebe mais tickets — pico de pedido é pico de reclamação | o marcador já cobre isso; confirmar que o TTL foi medido NO pico, não fora dele |
| 1 milhão de escritas/dia | muitas chaves de marcador de curta duração no Redis simultaneamente | pressão de memória no nó, e esgotamento de conexão se cada invocação abrir uma nova | TTL curto já ajuda a memória; `Lazy<ConnectionMultiplexer>` reutiliza conexão entre invocações do mesmo runtime |
| Falha de AZ durante o pico | o primário do ElastiCache falha e a réplica assume | leituras de marcador podem falhar por alguns segundos durante o failover | a função de leitura cai para o DynamoDB nesse intervalo — fail-open, não erro 500 |
O gargalo que só aparece em frota grande
Lambda em VPC paga o custo de anexar uma interface de rede por execução concorrente. Em pico alto, o número de execuções simultâneas das funções de escrita e leitura pode esbarrar em cota de ENI da sub-rede antes de esbarrar em qualquer limite do Redis — e o sintoma parece lentidão do cache quando é falta de rede para a própria função.
Custo: o que este laboratório acrescenta à fatura
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | centenas de escritas/dia | um nó `cache.t4g.micro` ligado 24/7; Lambda e SQS praticamente de graça no free tier | o cache domina o custo total, mesmo pequeno | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | 40 mil escritas/dia, pico de 15/s | réplica do cache (2 nós) + invocações de 4 funções + métricas customizadas | previsível; métrica customizada cresce com o número de leituras, não de escritas | agrupar a métrica por período maior se o volume de leitura for muito acima do de escrita |
| Alta escala | milhões de escritas/dia | nó de cache maior, mais réplicas, e concorrência de Lambda em VPC dimensionada | o Redis passa a ser a linha mais visível, seguido de perto pelas métricas customizadas | reduzir a granularidade da métrica de `OrigemDaResposta` (amostragem) se o volume de leitura for muito alto |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| ElastiCache | hora do nó ligado, independente de tráfego | é o único recurso "sempre ligado" desta arquitetura inteiramente orientada a evento |
| Lambda em VPC | duração + GB-segundo, com custo adicional de latência de rede na primeira invocação fria | quatro funções em VPC multiplicam o efeito de partida fria em relação a uma única função |
| SQS | por milhão de requisições | o polling do event source mapping do worker conta como requisição |
| DynamoDB sob demanda | por leitura e escrita | cache-miss em pico gera onda de leitura direta na tabela |
| CloudWatch, métricas customizadas | por métrica-mês, não por chamada | a dimensão `Origem` com valores distintos (`MarcadorAutor`, `ChaveComum`, `BancoAposMiss`, `RedisIndisponivel`) conta como métricas separadas — não adicione uma dimensão nova sem necessidade |
O custo oculto que este desenho evita
A alternativa "ler sempre do DynamoDB para todo mundo" pareceria mais simples e mais barata em Terraform — sem ElastiCache, sem marcador. Na fatura ela é o oposto: perde a proteção de leitura repetida que o cache oferece, e toda leitura do painel, de todo restaurante, o tempo todo, vira uma leitura na tabela. O marcador custa um nó de cache 24/7; a alternativa custaria a tabela inteira sob a carga de leitura de 1.200 restaurantes.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | divergência do autor medida e provada em zero; painel distingue origem da resposta | o TTL do marcador é um valor manual, revisado por pessoa | recalibração automática do TTL a partir da métrica de janela real (nível 6 da evolução) | média |
| Segurança | marcador isolado por autor; JWT authorizer nativo; segredo do Redis por TLS/security group | nenhuma rotina audita periodicamente o padrão de chaves do marcador | job agendado que confere `SCAN rw:*` contra o padrão esperado | média |
| Confiabilidade | leitura cai para o DynamoDB se o Redis falhar; worker idempotente contra reentrega da fila | nenhum alarme automático se a divergência do autor voltar a subir | alarme sobre a métrica `OrigemDaResposta` quando `MarcadorAutor` cai abaixo do esperado logo após uma escrita | alta |
| Eficiência de performance | marcador resolve em leitura de Redis, mais barata que leitura forte no DynamoDB | quatro funções em VPC pagam custo de ENI que uma arquitetura sem VPC não pagaria | avaliar VPC Lattice ou endpoint de interface se o número de funções crescer (fora do escopo aqui) | baixa |
| Otimização de custos | cache dimensionado pequeno; TTL curto evita acúmulo de chaves de marcador | métricas customizadas por dimensão podem crescer sem controle se novos valores de `Origem` forem adicionados | revisar a lista de valores de `Origem` antes de qualquer mudança de código | baixa |
| Sustentabilidade | TTL curto libera memória do Redis automaticamente, sem job de limpeza | nó de cache sempre ligado mesmo fora do horário de pico | avaliar escala automática do número de réplicas por horário, se o tráfego permitir | 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 estratégia de leitura. Cada nível resolve um risco e compra outro.
Leitura sempre direto do DynamoDB, sem fila nem cache: escrita e leitura síncronas e simples.Fila absorvendo pico de escrita, cache absorvendo leitura repetida, e marcador de leitura-própria-escrita para o autor.WebSocket ou eventos empurram a confirmação real para o cliente, em vez de o cliente confiar num marcador local com prazo de validade.Réplica global de tabela e cache replicado por região, para servir leitura de baixa latência perto do usuário.Quando a escrita deixa de ser um item isolado e passa a envolver múltiplos serviços — estoque, pagamento, pedido — a janela de inconsistência de um item vira compensação entre vários (L35).Os dados já existem: a métrica `OrigemDaResposta`, por tipo de fluxo, é uma série histórica de quanto tempo cada fluxo passa servindo do marcador antes de cair no caminho comum. Isso vira o conjunto de treino de um modelo pequeno — regressão simples, não geração de texto — que prevê o TTL de marcador por fluxo em vez de um valor único fixo para todos. A pergunta que separa isto de IA decorativa: sem o modelo, a alternativa é um humano olhando o mesmo painel de tempos em tempos e ajustando o TTL à mão — o modelo faz a mesma coisa, com mais fluxos e mais frequência do que uma pessoa consegue acompanhar.A ordem não é negociável, e o motivo é concreto
Notificação ativa no nível 3 só faz sentido depois de o nível 2 garantir que existe um valor correto e recente para notificar — sem o marcador, o WebSocket empurraria exatamente a mesma incerteza, só que mais rápido. É a decoração que o nível anterior evita.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. "Como garantir que o autor veja a própria escrita" tem resposta determinística: um marcador com TTL derivado de medição. Um modelo não melhora nenhuma das duas peças — são configuração e aritmética.
Há um lugar onde IA acrescentaria valor real, e ele é modesto: calibrar o TTL POR TIPO de fluxo, em vez de um valor único para todo o sistema. Hoje todo comando passa pelo mesmo TTL; um pedido de restaurante e uma atualização de perfil de usuário podem ter distribuições de latência de fila muito diferentes, e um TTL pensado para o pico do almoço pode ser generoso demais para tráfego fora de hora.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | ajustar o TTL do marcador por tipo de fluxo, em vez de um valor único |
| Por que uma regra não bastaria? | uma regra bastaria para começar: "TTL = p99 medido por tipo de fluxo, mais margem fixa" cobre a maior parte. IA só se justifica depois de a regra simples mostrar seu limite |
| De onde viriam os dados? | a própria métrica `OrigemDaResposta` e o log de instante de invalidação, segmentados por tipo de comando — nada disso é novo, já existe no CloudWatch |
| Qual o risco? | TTL calculado errado para baixo expõe o autor ao próprio bug de novo, de forma mais difícil de depurar porque parece ocasional; exige teto manual e alarme de divergência |
| Por que não agora? | a Rota Livre tem poucos tipos de fluxo distintos hoje. Um modelo sobre poucas categorias é complexidade sem ganho mensurável sobre a regra simples |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "decidir se este valor está desatualizado" olhando o conteúdo do item troca um sinal determinístico — a presença ou ausência do marcador, que é exata — por um probabilístico. A pergunta "isto é uma escrita recente deste autor?" já tem resposta binária no Redis; um modelo aqui só acrescentaria latência e uma nova forma de errar.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Marcador sem TTL, "para garantir que o usuário sempre veja" | parece mais seguro deixar o valor lá indefinidamente | o marcador vira fonte da verdade paralela, e diverge do DynamoDB se o worker rejeitar depois | usuário vê, por minutos ou horas, um estado que o sistema já invalidou | TTL curto, derivado da janela medida | nunca em produção |
| Chave do marcador sem `autorId`, só o recurso | é uma linha a menos de código — "o pedido é o pedido" | vaza a escrita de um usuário para outro que lê o mesmo recurso na janela | usuário B vê o estado transitório que o usuário A acabou de gravar | sempre compor a chave com autor e recurso | recurso genuinamente sem noção de autor — mas aí não é mais read-your-writes, é outra coisa |
| Invalidador escrevendo o novo valor (SET) em vez de apagar (DELETE) | parece uma otimização — evita um cache-miss na próxima leitura | SQS padrão não garante ordem; um SET fora de ordem deixa a chave com um valor mais VELHO que o real | divergência esporádica que "conserta sozinha" na próxima escrita, difícil de reproduzir | DELETE, sempre | só com fila FIFO por chave — que custa mais e não resolve o resto do problema |
| Tratar `ConsistentRead=true` como a correção completa | é a resposta que aparece primeiro numa busca sobre consistência eventual no DynamoDB | resolve só a leitura da TABELA; nada resolve o atraso da fila antes de o worker processar | autor ainda não vê a própria escrita quando ela está na fila, mesmo com leitura fortemente consistente | marcador cobre os dois atrasos, não só um | quando não existe fila no caminho — escrita síncrona — aí `ConsistentRead` sozinho já resolve |
| UI otimista sem plano para quando o worker rejeita | a maioria das escritas passa mesmo, e o caminho feliz é o que se testa primeiro | usuário vê confirmação de algo que o sistema, segundos depois, decide que é inválido — silenciosamente | reclamação de "sumiu" ao contrário: viu, depois não viu mais | worker apaga o marcador ao rejeitar; a UI trata a ausência subsequente como reversão, não como bug | fluxos onde a rejeição é praticamente impossível, porque a validação de forma já cobre tudo |
| Aplicar o marcador a todo leitor recente, não só ao autor | parece mais justo e mais uniforme para todo mundo | a função de leitura vira sempre-consistente de fato, e o motivo de ter cache desaparece | toda leitura passa a pagar o custo total, e o cache perde sua função | marcador é exclusivo do autor da escrita | nunca — é a decisão central do módulo sendo revertida |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Autor não vê a própria escrita mesmo com o marcador implementado | TTL menor que a janela real de invalidação/fila | compare o TTL configurado com o p99 medido na prova 4 | métrica `OrigemDaResposta` caindo para `ChaveComum`/`BancoAposMiss` antes do esperado | aumentar o TTL com base no dado medido, não estimado |
| Usuário B vê o rascunho do usuário A | chave do marcador sem `autorId` | inspecionar o padrão de chaves no Redis (`SCAN rw:*`) | logs da função de leitura, comparando o autor da sessão com o dono do marcador retornado | recompor a chave com `autorId` + `pedidoId` |
| Divergência que "some sozinha" e reaparece | invalidador fazendo SET em vez de DELETE, com mensagens fora de ordem | comparar o `SequenceNumber` do evento processado por último com o valor salvo | logs do invalidador e métricas do Streams | trocar para `DELETE` |
| Fila de comandos crescendo sem parar | worker mais lento que a taxa de chegada, ou travado num erro não tratado até o timeout | `ApproximateNumberOfMessages` e duração média das invocações do worker | métricas de `Duration` e `Errors` do Lambda | aumentar concorrência/memória, ou corrigir o erro que trava o worker |
| DLQ acumulando mensagens | regra de negócio mudou e comandos antigos não passam mais, ou payload malformado | inspecionar o corpo das mensagens retidas na DLQ | console ou CLI da fila de DLQ | reprocessar manualmente após corrigir, ou descartar com justificativa registrada |
| Erro 500 na leitura durante um evento de failover do Redis | função de leitura sem caminho de fallback quando o Redis está indisponível | correlacionar eventos de failover do ElastiCache com erros da função de leitura | logs da Lambda de leitura | cair para o DynamoDB quando o Redis não responde, nunca 500 |
| Marcador nunca expira, Redis cheio de chaves antigas | `StringSetAsync` chamado sem `TimeSpan` — TTL esquecido | `TTL rw:*` no Redis; qualquer `-1` é falha | código do handler de escrita | sempre setar o TTL explicitamente |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer parâmetro, pergunte: o problema está ANTES de o item existir no DynamoDB, ou DEPOIS disso mas antes de o cache saber? A primeira aponta para a fila e o worker; a segunda aponta para o Streams e o invalidador. As duas produzem o mesmo sintoma visível e pedem investigação em lugares opostos.
Limpeza: o que o destroy não leva
Este laboratório acrescenta poucos recursos, mas o cache é o que mais cobra parado — é o único recurso "sempre ligado" desta arquitetura orientada a evento.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. FILA DLQ: mensagens retidas por ate 14 dias sobrevivem se a fila
# principal foi apagada antes da DLQ, ou se o destroy foi interrompido.
aws sqs list-queues --queue-name-prefix ffv-lab-comandos --output table
# 3. GRUPOS DE LOGS das quatro funcoes: tem retencao propria e nao
# pertencem ao ciclo de vida do Lambda que os escreveu.
for fn in escrita leitura worker invalidador; do
aws logs delete-log-group --log-group-name "/aws/lambda/ffv-lab-${fn}" 2>/dev/null || true
done
# 4. METRICAS CUSTOMIZADAS nao sao apagaveis por comando, mas continuam
# aparecendo no CloudWatch (sem cobrar) ate expirar por retencao padrao.
# Confirme que nenhum alarme criado a mao ficou orfao.
aws cloudwatch describe-alarms --alarm-name-prefix ffv-lab --output table
# 5. Prova final: nada com nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output tableO ElastiCache é o item que cobra por estar ligado, não por uso
Diferente de Lambda, SQS e DynamoDB sob demanda, o `aws_elasticache_replication_group` cobra por hora do nó independentemente de tráfego. Se você pausar o laboratório sem rodar o `destroy`, é este recurso que segue cobrando enquanto os outros ficam ociosos de graça.
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Tabela DynamoDB e Streams | sim | não | nenhum item permanece; o stream some com a tabela |
| Fila SQS principal | sim | não | sem custo por estar vazia |
| Fila DLQ | sim, se referenciada no Terraform | não | mensagens retidas somem junto; risco é a fila ter sido apagada antes de você olhar o conteúdo |
| ElastiCache Redis | sim | sim, por hora do nó | é o único recurso desta arquitetura que cobra 24/7 |
| Grupos de logs das 4 funções | depende de `skip_destroy` | sim, por retenção | ciclo próprio; sobrevivem à função que os alimentava |
| Métricas customizadas no CloudWatch | não se aplica (não são recurso) | não | não há como "apagar" uma métrica; ela só para de receber pontos novos |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Autor não vê a própria escrita | marcador de leitura-própria-escrita, TTL curto | é gravado no mesmo request da escrita — não depende de fila nem de invalidação terem terminado |
| Item ainda não existe quando o autor lê | o marcador CONTÉM o valor, não aponta para ele | resolve mesmo quando o `PutItem` de verdade ainda não aconteceu |
| Cache mentindo sobre um item que já existe | invalidador com DELETE | apagar é idempotente independente da ordem de chegada do evento |
| Outros usuários não deveriam pagar o custo da correção do autor | chave composta por `autorId` | restringe o atalho a quem escreveu, preservando a proteção de cache para todos os outros |
| Worker rejeita a escrita depois de o autor já ter visto "sucesso" | apagar o marcador na rejeição | evita que o autor continue vendo, por segundos, um valor que o sistema já invalidou |
| Redis fica indisponível durante um failover | fallback de leitura para o DynamoDB | fail-open para consistência em vez de fail-closed com erro 500 |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Autor não vê a própria escrita | marcador síncrono com TTL | se o TTL for menor que a janela real, o autor volta a ser exposto perto do fim do prazo |
| Cache desatualizado para outros usuários | invalidação por Streams | nada — é o comportamento aceito por design |
| Vazamento de estado entre usuários | chave composta por `autorId` | se o `autorId` vier de fonte não autenticada, a composição da chave não ajuda |
| Reprocessamento duplicado de mensagem | `PutItem` idempotente | efeitos colaterais fora do `PutItem`, se existissem |
| Ordem de eventos do Streams fora de sequência | DELETE em vez de SET no invalidador | não resolve latência da invalidação, só a correção do valor final |
- O restaurante confirma o pedido; a API valida a forma e responde antes de qualquer processamento pesado.
- No mesmo request, a API grava o marcador de leitura-própria-escrita no Redis, com TTL curto.
- Também no mesmo request, a API enfileira o comando de escrita durável no SQS.
- O worker consome a mensagem, valida a regra de negócio de verdade e grava o item no DynamoDB.
- O DynamoDB Streams captura a mudança e aciona a função invalidadora.
- O invalidador apaga a chave compartilhada do item — nunca a chave de marcador de ninguém.
- O restaurante atualiza a própria tela: a leitura encontra o marcador e devolve o valor certo, sem tocar na fila nem no worker.
- Um segundo usuário lendo o mesmo pedido pode ainda ver o valor antigo por alguns segundos — por decisão, não por falha.
- Passado o TTL, o marcador expira e o autor volta a ler pelo caminho comum, já invalidado.
- A métrica `OrigemDaResposta` registrou cada uma dessas decisões, e é ela — não a sensação de ter testado — que prova o resultado.
Perguntas frequentes
❓ Por que meu app mostra o dado antigo logo depois de eu salvar?
❓ Read-your-writes resolve consistência eventual de verdade?
❓ ConsistentRead no DynamoDB resolve o problema de não ver a própria escrita?
❓ Por que o invalidador de cache deve apagar a chave, e não escrever o novo valor nela?
❓ Quanto tempo devo deixar o marcador de leitura-própria-escrita vivo?
❓ É seguro mostrar a mudança na tela antes do servidor confirmar (UI otimista)?
❓ Outros usuários também precisam do marcador de leitura-própria-escrita?
❓ Como eu meço se meu sistema tem esse problema, sem esperar um usuário reclamar?
Fixando
Depois de implementar o marcador de leitura-própria-escrita no ElastiCache, um teste automatizado mostra 0% de divergência para o autor, mas um SEGUNDO usuário lendo o mesmo item ainda vê o valor antigo por alguns segundos após a escrita. O que isso indica?
A função invalidadora deste laboratório apaga a chave do cache (DELETE) em vez de gravar o novo valor a partir do evento do DynamoDB Streams (SET). Por que essa escolha importa especificamente numa fila padrão como a que alimenta o invalidador?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | API HTTP básica, fila assíncrona (ideal ter feito L32 — síncrono ou assíncrono entre serviços), Terraform e .NET 8 básicos |
| Conhecimentos adquiridos | os dois relógios assíncronos de uma escrita desacoplada e por que produzem o mesmo sintoma; por que ConsistentRead resolve só metade do problema; o marcador de leitura-própria-escrita como correção de UX que não muda o modelo de consistência; por que invalidação de cache deve apagar, não sobrescrever |
| Limitação que fica | o TTL do marcador é um valor único, calibrado manualmente; fluxos com perfis de latência de fila muito diferentes compartilham o mesmo prazo |
| Também depende de | L32 (síncrono ou assíncrono entre serviços) — a decisão que introduz o primeiro relógio deste laboratório; e conceitualmente de L35 (saga), como contraste: aqui existe um item só, lá existem vários serviços compensando uns aos outros. Nenhum dos dois tinha seed publicado no momento desta escrita, e este módulo os referencia pelo papel no catálogo, sem repetir conteúdo que ainda não existe |
| Próximo módulo recomendado | L67 — lakehouse com Iceberg, upsert e time travel. O mesmo raciocínio de "quem lê o quê, e quando" generaliza de um item no DynamoDB para uma tabela analítica: lá, a pergunta vira "que SNAPSHOT esta consulta deveria ver", em vez de "este usuário vê a própria escrita" |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: DynamoDB read consistency — a distinção entre leitura eventualmente consistente (padrão) e fortemente consistente, e a nota de que GSI e Streams são sempre eventualmente consistentes; Amazon SQS at-least-once delivery — entrega pelo menos uma vez, ordem não garantida em fila padrão, e o `visibility timeout` padrão de 30 s; e Change data capture for DynamoDB Streams — o formato do registro (`eventName`, `NewImage`, `OldImage`, `StreamViewType`). Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
A taxa de divergência de 42% citada na prova 1 é a medida no ambiente de exemplo, sob um backlog de fila específico — serve como ordem de grandeza de que o defeito é real e mensurável, não como número de referência. A AWS não publica um SLA de latência para o DynamoDB Streams nem para a propagação de eventos até o event source mapping do Lambda; o TTL de 10 s usado nos exemplos deste módulo é ponto de partida conservador, e o valor certo para o seu sistema é o que a prova 4 medir no seu ambiente.
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…