Trabalho que demora — Arduino e IoT — semana 7 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 7 · Trabalho que demora — Material de Apoio Arduino

Semana 7 de 15· 3o trimestre · 05/09 a 10/12

Trabalho que demora

O pedido leva oito segundos e a resposta tem de sair agora.

Aula 1 — Fila: o trabalho sai da frente do usuario

Objetivos

  • Explicar por que um pedido de oito segundos não pode ficar esperando na frente de quem fez o pedido, e o que a resposta imediata devolve no lugar.
  • Escrever o enfileirar de uma tarefa demorada e o processar fila do lado do worker, com o mesmo identificador nos dois lados.
  • Devolver o id do trabalho na resposta, escrever o status do trabalho, e consultar o andamento pelo mesmo identificador.
  • Medir o tempo da resposta imediata e o tempo de cada trabalho, e mostrar que a fila é o que separa os dois números.
  • Reproduzir a perda da fila no reinício com a fila em memória, e mostrar o que muda com a fila no banco.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 computador com o projeto do 2o trimestre aberto, com o servidor Node rodando
  • Folha de papel por dupla, com o desenho do pedido, da resposta e da fila
  • 1 computador com o monitor serial aberto a 115200
  • Projetor, para o terminal com a fila crescendo e com a resposta saindo na hora

Conceitos

Tarefa demorada não espera na frente

Uma tarefa demorada é um trabalho que leva mais tempo do que o usuário aceita esperar. No projeto do curso são três: gerar o relatório do dia, recalcular o gráfico depois de uma importão grande, e reprocessar as leituras que ficaram na fila. Todos os três levam segundos, e todos os três são disparados por um clique.

O problema do pedido que espera é que ele não termina sozinho: ele ocupa a conexão, ocupa um espaço do servidor enquanto trabalha, e a tela do usuário fica esperando. Com um relatório na frente, a fila de conexões do servidor fica ocupada com um trabalho que não responde nada, e os pedidos da placa — que são rápidos — começam a esperar atrás dele.

A solução é a resposta imediata: o servidor recebe o pedido, devolve um id do trabalho e encerra a conexão em milissegundos. É o que se chama de aceitar e processar depois: o aceite vem no mesmo instante, o processamento não. O trabalho vai para a fila e é executado depois, fora da frente. Quem pediu ficou com um identificador e pode consultar o andamento quando quiser, em vez de ficar olhando a tela.

O que muda na experiência do usuário é grande e o professor mede: a resposta saiu em milissegundos em vez de oito segundos. O que muda na arquitetura é maior ainda, e é a parte que interessa aqui: o trabalho pode ser retomado, pode ser tentado de novo se falhar, e pode ser distribuído para outro processo sem que o usuário perceba nada disso.

O que a resposta imediata não faz é resolver o trabalho. Ela não faz o relatório; ela registra a promessa. E é por isso que o status do trabalho é parte do contrato e não um detalhe: quem recebeu o id precisa conseguir descobrir o que aconteceu com ele, inclusive o caso em que nunca mais vai acontecer.

O desenho da lousa tem três tempos, e o que importa é que o segundo é opcional do ponto de vista do servidor:

TempoO que aconteceQuem espera
resposta imediatao servidor devolve o id do trabalhoquem pediu, alguns milissegundos
consulta de andamentoo status responde pendente ou prontoquem pede, quando quiser
processamento em segundo planoo worker executa a tarefa demoradaninguém

Enfileirar, processar e o contrato do id do trabalho

Enfileirar é colocar a tarefa na fila; processar fila é tirar tarefas da fila e executá-las. Quem enfileira é o servidor que atendeu o pedido; quem processa é o worker, que é um processo separado do servidor web. Consumidor é o mesmo papel visto do outro lado da fila: o worker é o consumidor da fila.

A separação entre servidor web e worker é o que dá o nome de segundo plano, ou *background job*, que é o termo em inglês que aparece em todo código de fila. Um trabalho em segundo plano é um trabalho que roda sem depender de alguém estar esperando, e a consequência prática é que ele sobrevive ao fim da conexão: o usuário pode fechar o navegador que o relatório continua sendo gerado.

O id do trabalho é o contrato entre os dois lados, e ele precisa estar em três lugares:

  1. Na resposta ao usuário, que recebeu o id e vai consultá-lo.
  2. No registro do trabalho, onde o servidor guarda estado, resultado e erro.
  3. Na mensagem da fila, para que o worker saiba qual registro atualizar quando terminar.

O id tem que ser o mesmo nos três, e essa exigência é o que permite a segunda consulta: o usuário manda o id, o servidor acha o registro, e o worker tinha atualizado o mesmo registro. Se qualquer um dos três usasse um id diferente, a consulta mostraria um trabalho que não termina nunca.

O status do trabalho tem três campos que a turma precisa escrever, e o professor não aceita menos que três:

  • estado: pendente, em andamento ou concluído, com a opção de falhou.
  • progresso: quanto já foi feito, quando a tarefa tem etapas.
  • erro: a mensagem do último erro, quando houve.

Sem o campo de erro, um trabalho que falhou é indistinguível de um trabalho que nunca foi processado, e a equipe passa a reiniciar o servidor achando que a fila travou. O campo de erro é o que fecha o status do trabalho.

A consultar o andamento pelo id é o que transforma a resposta imediata em algo aceitável para o usuário. O padrão do projeto é uma rota que recebe o id e devolve o status, e a turma escreve a resposta do item 7 da atividade com os três campos preenchidos. O que a consulta não pode fazer é recalcular o trabalho: ela lê o registro, e quem escreve no registro é o worker.

Medir os dois tempos: resposta e trabalho

O ponto que o professor insiste é que são dois números, e a confusão entre eles é o que faz a equipe acreditar que a fila deixou tudo rápido.

O tempo de resposta é o que o usuário espera: o tempo entre o clique e a resposta com o id. Ele é medido no relógio do servidor, entre a entrada da rota e a saída da resposta. Na bancada ele fica na casa das dezenas de milissegundos, e é esse o número que o professor mede com o sketch.

O tempo do trabalho é o que o trabalho demora depois: é o relógio do worker, do começo ao fim da tarefa. Ele não tem limite de resposta, porque ninguém está esperando; tem limite de recurso, porque ocupa uma máquina.

A fila é o que separa os dois números, e a forma de ver isso é o que o item 2 da atividade mede: o tempo de resposta é da ordem de milissegundos, o tempo de trabalho é da ordem de segundos. Se os dois fossem iguais, não haveria fila.

O limite de tempo de resposta é a regra que a equipe escreve depois da medição, e ela é sempre mais alta que o número medido e sempre mais baixa que o tempo do trabalho. Um limite de tempo de resposta acima do tempo de trabalho é uma regra que nunca dispara; abaixo dos milissegundos medidos, ela dispara em toda requisição. O professor pede o número medido e o limite escrito, lado a lado, e pergunta o que acontece em cada um dos dois erros de escrita.

O que a fila não resolve, e que é a armadilha do dia: ela não resolve trabalho que demora porque é lento, e não dá controle sobre a ordem em que os trabalhos são executados. Uma fila com dez tarefas demoradas e uma máquina com um worker é uma fila que demora dez vezes o tempo de uma tarefa. Por isso a medição do tempo de trabalho importa tanto quanto a da resposta: é ela que diz quantos workers a fila precisa.

A fila em memória e a fila no banco

Uma fila de mensagens, a queue em inglês, é o armazenamento intermediário entre quem produz e quem consome. Uma fila em memória é uma estrutura do processo: um vetor, uma lista, uma variável. A tarefa é enfileirada escrevendo nela, e é consumida retirando dela. É a implementação mais simples que existe, e é a que todo mundo escreve primeiro.

O problema dela tem nome, e o nome é perder fila no reinício. A fila está na memória do processo; quando o processo morre — reinício, deploy, queda, *crash* — a memória vai junto e a fila vai com ela. Os trabalho que estavam na fila desaparecem, e o usuário que recebeu o id fica com um trabalho que nunca existiu.

O professor demonstra isso na aula, e é a demonstração mais curta e mais forte do dia: enfileira três tarefas, reinicia o servidor, e os três ids continuam no banco de registro com estado pendente para sempre. O registro diz que o trabalho existe; a fila diz que ele não vai acontecer. O status do trabalho responde "pendente" para sempre, e a equipe não tem como saber que ele se perdeu.

Uma fila no banco guarda a tarefa em uma tabela, e é isso que muda o resultado do reinício. A fila é uma tabela com número, identificador do trabalho, instante, estado e tentativas; o worker lê dela e escreve nela. Quando o processo morre, a tabela continua lá, e o worker que subir de novo encontra as tarefas pendentes e as processa. O trabalho sobrevive ao reinício, e o status do trabalho volta a se mover.

O quadro que o professor escreve na lousa resume a troca, e a turma copia:

Fila em memóriaFila no banco
montardez linhasuma tabela e transação
velocidademais rápidauma transação a mais
reinício do processoperde a filaa fila continua
envio duplicadoimpossível de vervisível pela linha repetida
consultas de andamentoprecisa de memóriaé a mesma tabela

A diferença de velocidade é pequena e o professor dá o número: a transação extra é da ordem de milissegundos, e ela é o preço de não perder trabalho. A equipe que escolhe a fila em memória por causa do milissegundo economizado está trocando um milissegundo por dados perdidos.

O que a fila no banco também resolve, e que a aula 2 usa: como a tarefa está gravada antes de ser executada, dá para contar quantas vezes ela foi tentada. É esse contador que faz o retry com espera, que é o tema da aula 2. E é ele que permite saber, na consulta do andamento, se o trabalho está na primeira tentativa ou na terceira.

O que nenhuma das duas filas resolve sozinho é o trabalho duplicado: fila em memória perde ao reiniciar, fila no banco duplica quando o worker processa e morre antes de marcar o trabalho como concluído. A correção é a mesma chave de idempotência da aula anterior aplicada ao trabalho, e ela é o que fecha o ciclo entre as duas aulas.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Cabo USB conectado, monitor serial em 115200.
  • Servidor Node do 2o trimestre rodando, com a tabela de registro de trabalhos vazia.
  • Folha de papel com os três tempos do desenho da lousa.
  1. Desenhe na folha os três tempos: resposta imediata, consulta de andamento e processamento em segundo plano. Escreva o relógio de cada um e quanto tempo cada um leva na sua bancada. Qual dos três é o tempo de resposta?
  2. Grave o sketch da resolução e rode por três minutos. Copie no caderno o tempo de resposta medido, o id do trabalho devolvido e o tempo de trabalho do worker. Some nada: escreva os dois números na mesma linha e diga quantas vezes o primeiro é menor que o segundo.
  3. Escreva a resposta do pedido em uma linha, do jeito que ela sai no HTTP. Quais campos tem, e onde está o id do trabalho? Escreva a resposta do status de um trabalho em andamento e de um trabalho concluído, com os três campos preenchidos.
  4. Enfileire três tarefas demoradas e consulte o andamento de cada uma pelos três ids. Copie aqui os três estados. O id que veio na resposta é o mesmo que você usou na consulta?
  5. Reinicie o servidor com três tarefas na fila e consulte os três ids de novo. O que os status responderam? Quantos trabalho ainda vão acontecer? Escreva em uma frase por que a fila em memória perdeu esses três.
  6. Escreva a tabela da fila no banco no seu projeto, com os campos: número, id do trabalho, instante, estado, tentativas. Escreva a consulta que o worker faz para pegar a próxima tarefa pendente. Ela trava a linha quando pega?
  7. Meça o tempo de resposta com a carga de dez tarefas na fila e escreva o número. Ele mudou em relação ao item 2? Meça também o tempo de trabalho de uma tarefa nessa condição. Qual dos dois números explica melhor por que a fila precisa de mais de um worker?
  8. Escreva no seu projeto o limite de tempo de resposta e o limite de tempo de trabalho, com os dois números ao lado dos medidos. O que acontece se o limite de resposta for menor que o número medido? E se for maior que o tempo de trabalho?

Nota: 12 pontos. Critério de fim: os dois tempos medidos do item 2, as duas respostas do status do item 3 e os três estados do item 4, e o resultado dos três ids depois do reinício no item 5.

Resolucao

O sketch da aula está em codigo/t3/dia07/aula1.ino.

#include <Arduino.h>
#include <esp_timer.h>

// ===========================================================================
// dia 7, aula 1: tarefa demorada em segundo plano.
//
// A placa NAO e o servidor e NAO tem fila. O sketch reproduz os TRES TEMPOS
// da aula no mesmo lugar, para que os dois relogios fiquem visiveis juntos:
//
//   1. resposta imediata  - o que o usuario espera: milissegundos
//   2. consulta de andamento - o status pelos tres campos
//   3. processamento em segundo plano - o worker, sem ninguem esperando
//
// E mostra a perda da fila em memoria no reinicio, que e o numero que faz a
// fila ir para o banco.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 4000;

// O tempo de trabalho do relatorio: a tarefa demorada da aula.
const unsigned long TEMPO_DO_TRABALHO_MS = 8000;

// A capacidade da fila em memoria: e o que o reinicio apaga.
const int CAPACIDADE_FILA = 8;

// ---------------------------------------------------------------------------
// O REGISTRO DO TRABALHO. E a fila no banco, em memoria, e o que sobrevive a
// um reinicio do worker. O id e o mesmo nos tres lugares: resposta, registro
// e mensagem da fila.
// ---------------------------------------------------------------------------
struct Trabalho {
  unsigned long id;
  const char* relatorio;
  char estado[20];         // pendente | em_andamento | concluido | falhou
  int progresso;           // 0 a 100
  const char* erro;        // vazio quando nao houve
};

const int MAX_TRABALHOS = 16;
Trabalho REGISTRO[MAX_TRABALHOS];
int totalTrabalhos = 0;

// A fila em memoria do worker. Nao sobrevive ao reinicio: e um vetor, e o
// vetor vai com o processo.
unsigned long filaEmMemoria[CAPACIDADE_FILA];
int itensNaFila = 0;

unsigned long proximoId = 1000;

// ---------------------------------------------------------------------------
// TEMPO 1: A RESPOSTA IMEDIATA. O que o usuario espera. Medido com o relogio
// de microsegundo, que e o do dia 11: milisegundo nao separa resposta de fila.
// ---------------------------------------------------------------------------
void enfileirar(const char* relatorio) {
  int64_t inicio = esp_timer_get_time();

  proximoId++;
  REGISTRO[totalTrabalhos].id = proximoId;
  REGISTRO[totalTrabalhos].relatorio = relatorio;
  strcpy(REGISTRO[totalTrabalhos].estado, "pendente");
  REGISTRO[totalTrabalhos].progresso = 0;
  REGISTRO[totalTrabalhos].erro = "";

  if (itensNaFila < CAPACIDADE_FILA) {
    filaEmMemoria[itensNaFila++] = proximoId;   // a fila em memoria
  }

  int64_t fim = esp_timer_get_time();
  Serial.printf("  resposta imediata: {\"id\": %lu, \"estado\": \"pendente\"}  "
                "em %.2f ms\n", proximoId, (float)(fim - inicio) / 1000.0f);
  totalTrabalhos++;
}

// ---------------------------------------------------------------------------
// TEMPO 2: A CONSULTA DE ANDAMENTO. Le o registro e devolve os tres campos.
// Nao recalcula nada: quem escreve no registro e o worker.
// ---------------------------------------------------------------------------
void consultarAndamento(unsigned long id) {
  for (int i = 0; i < totalTrabalhos; i++) {
    if (REGISTRO[i].id != id) continue;
    Serial.printf("  status %lu: %s | progresso %d%% | erro: %s\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso,
                  REGISTRO[i].erro[0] ? REGISTRO[i].erro : "(nenhum)");
    return;
  }
  Serial.printf("  status %lu: trabalho desconhecido\n", id);
}

// ---------------------------------------------------------------------------
// TEMPO 3: O WORKER. Processa fila em segundo plano, atualiza o registro e
// so entao marca como concluido. A ordem importa: o registro e atualizado
// depois do trabalho, nunca antes.
// ---------------------------------------------------------------------------
void workerPasso() {
  if (itensNaFila == 0) {
    Serial.println("  worker: fila vazia, nada a fazer");
    return;
  }
  unsigned long id = filaEmMemoria[0];
  for (int i = 1; i < itensNaFila; i++) filaEmMemoria[i - 1] = filaEmMemoria[i];
  itensNaFila--;

  for (int i = 0; i < totalTrabalhos; i++) {
    if (REGISTRO[i].id != id) continue;
    strcpy(REGISTRO[i].estado, "em_andamento");
    Serial.printf("  worker: pegou o trabalho %lu, vai levar %lu s\n",
                  id, TEMPO_DO_TRABALHO_MS / 1000);
    // O trabalho pesado acontece aqui, com o sleep que representa os 8 s.
    delay(TEMPO_DO_TRABALHO_MS / 4);
    REGISTRO[i].progresso = 50;
    Serial.printf("  status %lu: %s | progresso %d%% | erro: (nenhum)\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso);
    delay(TEMPO_DO_TRABALHO_MS / 4);
    REGISTRO[i].progresso = 100;
    strcpy(REGISTRO[i].estado, "concluido");
    Serial.printf("  status %lu: %s | progresso %d%% | erro: (nenhum)\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso);
    return;
  }
}

// ---------------------------------------------------------------------------
// A PERDA NO REINICIO. Zera o processo: a fila em memoria vai junto, e o
// registro fica. E o numero que separa a fila em memoria da fila no banco.
// ---------------------------------------------------------------------------
void reiniciarProcesso() {
  int perdidos = itensNaFila;
  Serial.println("--- reiniciando o processo do worker ---");
  Serial.printf("  fila em memória: %d trabalho(s) que iam rodar\n", perdidos);
  itensNaFila = 0;
  Serial.println("  registro: continua, os ids continuam lá");
  Serial.println("  resultado: os status ficam em 'pendente' para sempre,");
  Serial.println("  e o usuário que recebeu o id nunca recebe o relatório.");
  Serial.println("  Com a fila no banco, os mesmos três trabalhos sobrevivem.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 7, aula 1: o trabalho sai da frente do usuário ===");
  Serial.println("Resposta em milissegundos. Trabalho em segundos. Fila no meio.");
  Serial.println();

  unsigned long t0 = (unsigned long)esp_timer_get_time();
  enfileirar("relatorio do dia");
  unsigned long t1 = (unsigned long)esp_timer_get_time();
  Serial.print("  tempo de resposta medido: ");
  Serial.print((t1 - t0) / 1000.0f);
  Serial.println(" ms");
  Serial.print("  tempo do trabalho:        ");
  Serial.print(TEMPO_DO_TRABALHO_MS / 1000);
  Serial.println(" s");
  Serial.print("  o primeiro é ");
  Serial.print(TEMPO_DO_TRABALHO_MS / 1000);
  Serial.println(" vezes menor que o segundo: é a fila que separa os dois.");
  Serial.println();

  enfileirar("recalcular grafico");
  enfileirar("reprocessar leituras");
  Serial.println("  três ids entregues. O trabalho do usuário não espera nenhum deles.");
  Serial.println();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- os três tempos ---");
  Serial.print("  1. ");
  Serial.println("resposta imediata: o que o usuário espera");
  Serial.print("  2. ");
  Serial.println("consulta de andamento: o status pelos três campos");
  Serial.print("  3. ");
  Serial.println("processamento em segundo plano: o worker, sem ninguém esperando");
  Serial.println();

  consultarAndamento(REGISTRO[0].id);
  workerPasso();
  reiniciarProcesso();

  Serial.println("  Onde a fila em memória ganha e onde perde:");
  Serial.println("    ganha: dez linhas de código, e a mais rápida das duas");
  Serial.println("    perde: tudo, quando o processo reinicia");
  Serial.println("    a fila no banco paga uma transação e não perde trabalho");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Por que assim e não de outro jeito. O sketch roda os três tempos no mesmo lugar porque a aula precisa dos dois relógios juntos. Se a resposta imediata fosse medida numa tela e o tempo de trabalho em outra, a turma compararia dois números de contextos diferentes e a conclusão seria "¿Por que oito segundos?". Imprimidos na mesma tela, na mesma volta, a razão fica óbvia: a resposta saiu em décimos de milissegundo e o trabalho leva oito segundos, e a fila é exatamente a distância entre os dois.

A struct Trabalho tem cinco campos, e o quinto — erro — é o que a turma acha dispensável. Os quatro primeiros respondem "o que está acontecendo"; o quinto responde "o que aconteceu de errado". Sem ele, um trabalho que falhou é indistinguível de um trabalho que ninguém pegou, e a equipe reinicia o servidor achando que a fila travou. O campo de erro é o que fecha o status do trabalho, e o sketch o imprime sempre, com (nenhum) quando está vazio — porque um campo ausente no registro é um campo que ninguém sabe ler.

O id é o mesmo em três lugares, e o professor aponta isso no código: ele nasce em enfileirar, entra no registro, e entra na fila no mesmo instante. A exigência de que o id seja o mesmo nos três é o que permite a consulta do item 4 da atividade funcionar. Se a fila usasse um número de posição e o registro usasse outro, o usuário consultaria um trabalho que nunca termina.

A REGISTRO e a filaEmMemoria são duas estruturas separadas de propósito, e a separação é o argumento inteiro da aula. O registro é o que a consulta de andamento lê, e ele sobrevive ao reinício. A fila é o que o worker lê, e ela morre com o processo. O reiniciarProcesso zera só a fila, e o efeito é exatamente o que a equipe vai ver em produção: os status ficam em pendente para sempre, sem nunca terem falhado.

O TEMPO_DO_TRABALHO_MS com oito milissegundos é o número do enunciado do dia, e ele é uma constante e não um sleep escrito à mão no meio da função. Com a constante, o item 8 da atividade — mudar o tempo de trabalho e ver o que acontece com o tempo de resposta — vira edição de uma linha, e a turma descobre que o tempo de resposta não muda. Essa é a propriedade da fila, demonstrada com número.

A divisão do trabalho em dois delay com o progresso passando a 50 no meio existe para mostrar o campo de progresso em uso. Uma tarefa que só vai de 0 a 100 não tem como demonstrar o campo, e o campo de progresso é justamente o que permite a um usuário de relatório de oito segundos saber que não travou. O professor pede que a turma escreva o status do meio do trabalho e compare com o do fim.

O LED acende durante a resposta, o processamento e a demonstração do reinício, e apaga no intervalo. O intervalo desta aula é de quatro segundos, e não de cinco, porque o trabalho simulado já leva dois segundos dos dois delay e a tela precisa ficar parada o suficiente para a turma ler o bloco.

Criterios de correcao

CritérioPontos
Os três tempos desenhados, com o relógio e a medição de cada um2 pontos
O tempo de resposta e o tempo de trabalho medidos, com a comparação escrita2 pontos
A resposta do pedido e as duas respostas do status com os três campos2 pontos
Os três ids enfileirados e consultados, com os três estados2 pontos
O resultado dos três ids depois do reinício, e a explicação da perda2 pontos
A tabela da fila no banco escrita, com a consulta do worker1 ponto
Os dois limites escritos, com o efeito dos dois erros de escrita1 ponto

Erros comuns

ErroComo apareceCorreção
Fazer o relatório na frenteO clique trava a tela por oito segundos"Resposta imediata: devolva o id do trabalho e processe em segundo plano. Quem pediu consulta o andamento quando quiser."
Devolver só "ok" sem idO usuário não tem como perguntar o que aconteceu"A resposta é o contrato: ela devolve o id do trabalho. Sem id não há como consultar o andamento."
Status sem campo de erroTrabalho falhado parece trabalho nunca processado"Os três campos são estado, progresso e erro. Sem o erro, ninguém sabe se a fila travou ou se o trabalho quebrou."
Recalcular na consultaO status executa o relatório para responder"A consulta lê o registro. Quem escreve no registro é o worker; se a consulta calcula, ela virou o trabalho."
Fila em memória em produçãoO servidor reinicia e os relatórios param de acontecer"Fila em memória é o vetor do processo, e o processo morre com ela. A fila no banco custa uma transação e não perde trabalho."
Confundir os dois tempos"A fila deixou tudo rápido" e o relatório demora oito segundos"São dois relógios: resposta em milissegundos, trabalho em segundos. A fila separa os dois; ela não encurta o trabalho."
Limite de resposta acima do trabalhoO alerta nunca dispara, porque nunca é atingido"O limite fica acima do número medido e abaixo do tempo de trabalho. Os dois erros de escrita são alerta que nunca dispara e alerta que dispara sempre."
Uma máquina com um worker e dez tarefasA fila cresce e o tempo de trabalho multiplica por dez"Meça o tempo de trabalho sob carga. É ele que diz quantos workers a fila precisa — a resposta continua rápida nos dois casos."

Desafio extra

Monte a fila no banco no seu projeto, com a tabela, a consulta que o worker usa para pegar a próxima tarefa e a transação que trava a linha. Depois rode o teste completo: enfileire dez tarefas demoradas, reinicie o servidor duas vezes no meio, e ao final consulte o andamento dos dez ids e escreva quantos chegaram a concluído. Repita o mesmo teste com a fila em memória e compare os dois números. Escreva na frente os dois resultados e diga quantos trabalhos se perderam em cada caso. Em seguida, diminua o tempo de trabalho para dois segundos, mantenha a carga de dez tarefas, e meça de novo o tempo de resposta e o tempo de trabalho. A pergunta que o professor espera de resposta: quantos workers o seu projeto precisaria para que o tempo de trabalho com dez tarefas na fila ficasse igual ao tempo de uma tarefa só, e o que muda no painel de operação quando há mais de um worker consumindo a mesma fila?

>

A resolucao, compilada

// Aula 1 do dia 7 do 3o trimestre de ESP32 e IoT.
// Gerado a partir da secao ## Resolucao de conteudo/t3/dia07/aula1.md.
// Se mudar o codigo, mude no .md e regere: o .ino e copia do .md.

#include <Arduino.h>
#include <esp_timer.h>

// ===========================================================================
// dia 7, aula 1: tarefa demorada em segundo plano.
//
// A placa NAO e o servidor e NAO tem fila. O sketch reproduz os TRES TEMPOS
// da aula no mesmo lugar, para que os dois relogios fiquem visiveis juntos:
//
//   1. resposta imediata  - o que o usuario espera: milissegundos
//   2. consulta de andamento - o status pelos tres campos
//   3. processamento em segundo plano - o worker, sem ninguem esperando
//
// E mostra a perda da fila em memoria no reinicio, que e o numero que faz a
// fila ir para o banco.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 4000;

// O tempo de trabalho do relatorio: a tarefa demorada da aula.
const unsigned long TEMPO_DO_TRABALHO_MS = 8000;

// A capacidade da fila em memoria: e o que o reinicio apaga.
const int CAPACIDADE_FILA = 8;

// ---------------------------------------------------------------------------
// O REGISTRO DO TRABALHO. E a fila no banco, em memoria, e o que sobrevive a
// um reinicio do worker. O id e o mesmo nos tres lugares: resposta, registro
// e mensagem da fila.
// ---------------------------------------------------------------------------
struct Trabalho {
  unsigned long id;
  const char* relatorio;
  char estado[20];         // pendente | em_andamento | concluido | falhou
  int progresso;           // 0 a 100
  const char* erro;        // vazio quando nao houve
};

const int MAX_TRABALHOS = 16;
Trabalho REGISTRO[MAX_TRABALHOS];
int totalTrabalhos = 0;

// A fila em memoria do worker. Nao sobrevive ao reinicio: e um vetor, e o
// vetor vai com o processo.
unsigned long filaEmMemoria[CAPACIDADE_FILA];
int itensNaFila = 0;

unsigned long proximoId = 1000;

// ---------------------------------------------------------------------------
// TEMPO 1: A RESPOSTA IMEDIATA. O que o usuario espera. Medido com o relogio
// de microsegundo, que e o do dia 11: milisegundo nao separa resposta de fila.
// ---------------------------------------------------------------------------
void enfileirar(const char* relatorio) {
  int64_t inicio = esp_timer_get_time();

  proximoId++;
  REGISTRO[totalTrabalhos].id = proximoId;
  REGISTRO[totalTrabalhos].relatorio = relatorio;
  strcpy(REGISTRO[totalTrabalhos].estado, "pendente");
  REGISTRO[totalTrabalhos].progresso = 0;
  REGISTRO[totalTrabalhos].erro = "";

  if (itensNaFila < CAPACIDADE_FILA) {
    filaEmMemoria[itensNaFila++] = proximoId;   // a fila em memoria
  }

  int64_t fim = esp_timer_get_time();
  Serial.printf("  resposta imediata: {\"id\": %lu, \"estado\": \"pendente\"}  "
                "em %.2f ms\n", proximoId, (float)(fim - inicio) / 1000.0f);
  totalTrabalhos++;
}

// ---------------------------------------------------------------------------
// TEMPO 2: A CONSULTA DE ANDAMENTO. Le o registro e devolve os tres campos.
// Nao recalcula nada: quem escreve no registro e o worker.
// ---------------------------------------------------------------------------
void consultarAndamento(unsigned long id) {
  for (int i = 0; i < totalTrabalhos; i++) {
    if (REGISTRO[i].id != id) continue;
    Serial.printf("  status %lu: %s | progresso %d%% | erro: %s\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso,
                  REGISTRO[i].erro[0] ? REGISTRO[i].erro : "(nenhum)");
    return;
  }
  Serial.printf("  status %lu: trabalho desconhecido\n", id);
}

// ---------------------------------------------------------------------------
// TEMPO 3: O WORKER. Processa fila em segundo plano, atualiza o registro e
// so entao marca como concluido. A ordem importa: o registro e atualizado
// depois do trabalho, nunca antes.
// ---------------------------------------------------------------------------
void workerPasso() {
  if (itensNaFila == 0) {
    Serial.println("  worker: fila vazia, nada a fazer");
    return;
  }
  unsigned long id = filaEmMemoria[0];
  for (int i = 1; i < itensNaFila; i++) filaEmMemoria[i - 1] = filaEmMemoria[i];
  itensNaFila--;

  for (int i = 0; i < totalTrabalhos; i++) {
    if (REGISTRO[i].id != id) continue;
    strcpy(REGISTRO[i].estado, "em_andamento");
    Serial.printf("  worker: pegou o trabalho %lu, vai levar %lu s\n",
                  id, TEMPO_DO_TRABALHO_MS / 1000);
    // O trabalho pesado acontece aqui, com o sleep que representa os 8 s.
    delay(TEMPO_DO_TRABALHO_MS / 4);
    REGISTRO[i].progresso = 50;
    Serial.printf("  status %lu: %s | progresso %d%% | erro: (nenhum)\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso);
    delay(TEMPO_DO_TRABALHO_MS / 4);
    REGISTRO[i].progresso = 100;
    strcpy(REGISTRO[i].estado, "concluido");
    Serial.printf("  status %lu: %s | progresso %d%% | erro: (nenhum)\n",
                  id, REGISTRO[i].estado, REGISTRO[i].progresso);
    return;
  }
}

// ---------------------------------------------------------------------------
// A PERDA NO REINICIO. Zera o processo: a fila em memoria vai junto, e o
// registro fica. E o numero que separa a fila em memoria da fila no banco.
// ---------------------------------------------------------------------------
void reiniciarProcesso() {
  int perdidos = itensNaFila;
  Serial.println("--- reiniciando o processo do worker ---");
  Serial.printf("  fila em memória: %d trabalho(s) que iam rodar\n", perdidos);
  itensNaFila = 0;
  Serial.println("  registro: continua, os ids continuam lá");
  Serial.println("  resultado: os status ficam em 'pendente' para sempre,");
  Serial.println("  e o usuário que recebeu o id nunca recebe o relatório.");
  Serial.println("  Com a fila no banco, os mesmos três trabalhos sobrevivem.");
  Serial.println();
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== dia 7, aula 1: o trabalho sai da frente do usuário ===");
  Serial.println("Resposta em milissegundos. Trabalho em segundos. Fila no meio.");
  Serial.println();

  unsigned long t0 = (unsigned long)esp_timer_get_time();
  enfileirar("relatorio do dia");
  unsigned long t1 = (unsigned long)esp_timer_get_time();
  Serial.print("  tempo de resposta medido: ");
  Serial.print((t1 - t0) / 1000.0f);
  Serial.println(" ms");
  Serial.print("  tempo do trabalho:        ");
  Serial.print(TEMPO_DO_TRABALHO_MS / 1000);
  Serial.println(" s");
  Serial.print("  o primeiro é ");
  Serial.print(TEMPO_DO_TRABALHO_MS / 1000);
  Serial.println(" vezes menor que o segundo: é a fila que separa os dois.");
  Serial.println();

  enfileirar("recalcular grafico");
  enfileirar("reprocessar leituras");
  Serial.println("  três ids entregues. O trabalho do usuário não espera nenhum deles.");
  Serial.println();
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- os três tempos ---");
  Serial.print("  1. ");
  Serial.println("resposta imediata: o que o usuário espera");
  Serial.print("  2. ");
  Serial.println("consulta de andamento: o status pelos três campos");
  Serial.print("  3. ");
  Serial.println("processamento em segundo plano: o worker, sem ninguém esperando");
  Serial.println();

  consultarAndamento(REGISTRO[0].id);
  workerPasso();
  reiniciarProcesso();

  Serial.println("  Onde a fila em memória ganha e onde perde:");
  Serial.println("    ganha: dez linhas de código, e a mais rápida das duas");
  Serial.println("    perde: tudo, quando o processo reinicia");
  Serial.println("    a fila no banco paga uma transação e não perde trabalho");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia07 aula1.

Aula 2 — Retry com espera e o trabalho que roda duas vezes

Objetivos

  • Escrever o retry com espera entre tentativas, com backoff exponencial e jitter, e explicar por que a espera existe.
  • Medir o número de tentativas e escrever a regra de dar up depois de N tentativas, com o que o sistema faz com o trabalho abandonado.
  • Reproduzir o trabalho duplicado e o efeito duplicado, e dizer por que a fila repete mesmo depois de processar.
  • Aplicar a chave de idempotência no trabalho, e mostrar que executar duas vezes não vira não duplicar falha.
  • Diagnosticar o envio duplicado da placa e escrever a correção que impede que o mesmo dado vire duas linhas.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 computador com o projeto do 2o trimestre aberto, com o servidor Node rodando
  • Folha de papel por dupla, com a tabela de tentativas
  • 1 computador com o monitor serial aberto a 115200
  • Projetor, para o terminal com a sequência de tentativas e as esperas

Conceitos

Tentar de novo, e por que a espera não é vaidade

O retry é tentar de novo depois de uma falha: tente de novo, e espere antes. O termo em inglês aparece em todo código de fila, e o comportamento ingênuo — repetir até dar certo — é o que a seção 3 desmonta. Tentar de novo é a operação; o que vem com ela é a decisão de quando, e essa decisão é quase toda a aula.

Tente de novo sem espera entre tentativas é a forma mais comum e a mais destrutiva. A placa que reenvia a cada dois segundos transforma uma queda de dez segundos em cinquenta envios, e quando o servidor volta ele recebe cinquenta mensagens de uma vez, tenta gravar todas, e cai de novo. O sistema entra em um ciclo em que a cura causa a doença, e o professor mede isso: a fila tinha dez mensagens, e a primeira tentativa de cura a transformou em cem.

A espera entre tentativas é o que separa a cura da doença. A ideia é simples: se o servidor está fora, esperar antes de tentar aumenta a chance de que ele já tenha voltado, e reduz a pressão sobre o servidor quando ele volta. O número da espera é uma decisão de projeto, e o professor escreve duas regras:

  1. Aumenta a cada tentativa. Tentar imediatamente, depois esperar pouco, depois esperar mais.
  2. Tem um teto. Mesmo que a tentativa cinco espere cinco minutos, existe um limite escrito.

O número de tentativas é o teto de tentativas, e ele é o que decide o custo máximo de um trabalho que nunca vai dar certo. Cada tentativa custa: uma conexão, uma transação, um log de erro. A soma é o custo de um trabalho perdido. A equipe escolhe esse número com a mesma régua do dia 5: trabalho que costuma dar certo na primeira vai receber três; trabalho que depende de serviço externo pode receber cinco.

Dar up depois de N tentativas é o nome do que acontece quando o número é atingido, e ele tem nome próprio em português: abandonar o trabalho. O sistema para de tentar, marca o trabalho como falhou com a mensagem do último erro, e devolve a resposta ao usuário que estava esperando. O que o sistema não faz é ficar em silêncio: um trabalho que falhou e não foi registrado é o pior resultado possível, porque parece um trabalho que nunca foi aceito.

O efeito duplicado aparece aqui e é o que o professor quer mostrar na segunda metade: um trabalho que roda dez vezes por causa do retry e que produz efeito externo dez vezes. Relatório enviado por e-mail dez vezes. Cobrança registrada dez vezes. Leitura gravada dez vezes. O retry é uma multiplicação de tentativas, e efeito externo sem proteção é multiplicação de efeito.

Backoff exponencial e o jitter que vem junto

O backoff exponencial é a regra da espera que dobra a cada tentativa. O nome é a definição: o intervalo de espera é exponencial no número da tentativa, normalmente um fator de dois, com teto.

O que o backoff exponencial resolve não é a paciência do cliente. É a sincronização. A razão pela qual a espera dobra está na situação que a aulaopened com a placa reenviando: várias mensagens que falharam ao mesmo tempo voltam a tentar ao mesmo tempo, e se a espera for a mesma para todas, elas continuam juntas para sempre. A espera exponencial desfasa o grupo, porque cada tentativa tem um intervalo diferente, e as tentativas se espalham pelo tempo.

A tabela da aula é a sequência que a turma implementa, e os números são reais:

TentativaEspera antesMomento em que tenta
10agora
22 s2 s
34 s6 s
48 s14 s
516 s30 s
632 s62 s

O jitter é o passo que ninguém dá e que resolve o resto do problema. Com backoff exponencial sem jitter, a fila de mil mensagens que caiu junto continua caindo junta: cada mensagem espera o dobro, mas todas esperam o mesmo. O jitter adiciona uma parcela aleatória à espera — de 0% a 100% do intervalo, por exemplo — e o resultado é que as mil mensagens se espalham pela janela inteira em vez de ficarem concentradas no começo.

O que o jitter custa e o que o professor mede: a espera real fica entre o mínimo e o máximo do intervalo. Com 2 s + aleatório(0, 2), a segunda tentativa acontece entre 2 e 4 segundos. A última tentativa da tabela, com 32 segundos, acontece entre 32 e 64. É uma espera pior no caso médio e muito melhor no caso de mil mensagens simultâneas, e o critério de escolha é o tamanho da fila, não o conforto de uma mensagem.

O backoff exponencial com teto tem três números que precisam estar escritos no projeto, e o professor pede os três:

  1. Espera inicial — quantos segundos antes da segunda tentativa.
  2. Fator — o multiplicador, normalmente dois.
  3. Teto — a espera máxima, e o número máximo de tentativas.

Os três juntos descrevem a curva completa de custo. Sem o teto, a tentativa dez espera horas; com o teto, ela espera o mesmo que a tentativa cinco. E o que o professor escreve na lousa é a frase que resume o trade-off: retry sem teto é uma fila que nunca drena; retry sem jitter é uma fila que cai de novo junto.

Trabalho duplicado e a garantia que explica ele

A garantia de entrega pelo menos uma vez da aula 1 é a razão de o trabalho duplicado existir, e ela precisa ser dita de novo aqui com a consequência prática: a fila pode entregar a mesma mensagem mais de uma vez, e não há como evitar isso sem perder trabalho.

O cenário que o professor demonstra tem cinco passos, e a turma precisa descrevê-los de memória no item 5:

  1. O worker retira a mensagem da fila.
  2. Ele processa: grava a linha no banco, faz o efeito externo, marca o trabalho como concluído no registro.
  3. Ele tenta confirmar, e a conexão cai antes de o ack chegar à fila.
  4. A fila não recebeu confirmação e reentrega a mesma mensagem.
  5. O worker processa de novo, e o efeito acontece de novo.

O resultado é trabalho duplicado com efeito duplicado, e a equipe olha o relatório enviado por e-mail duas vezes e conclui que alguém clicou duas vezes. A repetição não é de ninguém: é do protocolo.

O que a fila tem que oferecer para evitar esse cenário é exatamente uma vez, o que é mais caro e mais difícil: exige que a fila e o consumidor compartilhem estado, e que o consumidor confirme de forma que o estado nunca fique inconsistente. A fila usada na aula oferece pelo menos uma vez, e o desenho correto do sistema assume isso e corrige na aplicação.

A correção é a chave de idempotência aplicada ao trabalho, e ela tem a mesma forma do dia 1: o identificador é gerado quando o trabalho é criado e viaja com a mensagem. O consumidor, ao receber a mensagem:

  1. Tenta gravar o resultado com aquela chave.
  2. Recebe recusa de unicidade do banco.
  3. Entende como sucesso: o efeito já aconteceu.
  4. Só então confirma a fila.

O efeito duplicado que sobra é o que não passa pelo banco — o e-mail enviado, a chamada de API externa, a linha em outro sistema. Para esses, a chave precisa ser passada junto, ou o resultado precisa ser guardado antes do efeito. O professor explica a ordem: gravar a intenção antes de executar o efeito. Se o processo morre entre as duas coisas, a próxima tentativa vê a intenção gravada e não repete o efeito.

Idempotência aplicada, e o envio duplicado da placa

A idempotência é a propriedade de poder executar duas vezes com o resultado igual ao de uma. Não duplicar não é outra coisa: é a consequência da idempotência quando o efeito é externo. E a aula é onde as duas se encontram, porque o trabalho da fila e a leitura da placa têm a mesma estrutura.

A chave de idempotência do trabalho é o id do trabalho, e a do envio da placa é o identificador único da leitura. As duas são geradas na criação da operação, e as duas viajam dentro da mensagem. A regra é a mesma e é a frase que a turma leva do dia 1: quem inicia a operação gera a chave.

O envio duplicado da placa é o caso concreto, e ele tem duas causas que o professor separa:

  1. A placa reenvia porque o ack não chegou — mesmo envio, mesmo identificador.
  2. O worker reentrega porque a confirmação não chegou — mesma mensagem, mesmo identificador.

Nos dois casos o identificador é o mesmo, e nos dois casos a restrição de unicidade do banco resolve. É por isso que o teste do item 7 é o mesmo do dia 1 com outro nome, e o professor faz a turma perceber que não é coincidência: é o mesmo mecanismo aparecendo em duas camadas.

O que a idempotência não cobre, e que o professor escreve na lousa como limite honesto: ela cobre o banco. Ela não cobre o efeito que não passa pelo banco, e não cobre o relógio — se a chave é gerada com base em tempo e o tempo volta, a chave volta. O número de tentativas também não é coberto: a idempotência garante que rodar de novo não duplica, não que rodar de novo é seguro para o recurso externo.

O timebox do retry completa o desenho, e é a regra final que a equipe escreve: o retry tem número de tentativas e tem limite de tempo total. Uma tarefa que falha em 0,1 s dezenove vezes dá up na décima nona, num segundo; uma que falha em oito segundos dezenove vezes leva duas minutos. O número de tentativas limita a quantidade; o limite de tempo limita a demora. Os dois juntos é o que impede que uma fila drenando trabalho morto ocupe a máquina a noite inteira.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Cabo USB conectado, monitor serial em 115200.
  • Servidor Node rodando, com a tabela de trabalhos do dia anterior e o id disponível.
  • Folha de papel com a tabela de tentativas e as esperas.
  1. Escreva a tabela de tentativas do seu projeto: tentativa, espera antes, e momento em que acontece. Use espera inicial de dois segundos, fator dois e teto de trinta e dois segundos. Em que momento ocorre a quinta tentativa?
  2. Grave o sketch da resolução e rode. Copie no caderno a sequência de tentativas com o tempo entre elas e o instante em que o sistema dar up depois de N tentativas. O número da tela bate com a sua tabela?
  3. Remova o jitter do seu código e rode de novo com dez mensagens falhando ao mesmo tempo. Conte quantas tentativas chegaram no mesmo instante. Depois devolva o jitter e conte de novo. O que mudou?
  4. Escreva os três números do projeto: espera inicial, fator e teto. Some o tempo total até o número de tentativas máximo. Quanto tempo um trabalho morto ocupa a máquina antes de ser abandonado?
  5. Reproduza o trabalho duplicado: processe o trabalho, marque-o como concluído e caia antes de confirmar. Quantas vezes o efeito aconteceu? Escreva os cinco passos do cenário na sua folha, de memória.
  6. Aplique a chave de idempotência no seu worker e repita o passo 5. Quantas linhas há no banco agora? O que o worker fez ao receber a recusa de unicidade, e por que isso é sucesso e não erro?
  7. Reproduza o envio duplicado da placa: envie a mesma leitura duas vezes com o mesmo identificador e conte as linhas. Depois envie duas leituras com identificadores diferentes e o mesmo valor. Quantas linhas há? O que cada um dos dois testes prova?
  8. Escreva no seu projeto a intenção antes do efeito: onde o registro é gravado antes de o e-mail ser enviado, e o que o worker consulta quando o processo morreu no meio. Escreva a frase que explica por que isso não duplica.

Nota: 12 pontos. Critério de fim: a tabela de tentativas escrita, os dois números do item 3 — com e sem jitter — e os dois contadores de linha dos testes do item 7.

Resolucao

O sketch da aula está em codigo/t3/dia07/aula2.ino.

#include <Arduino.h>
#include <esp_timer.h>

// ===========================================================================
// dia 7, aula 2: retry com espera, e o trabalho que roda duas vezes.
//
// A placa NAO tem fila e NAO tem servidor. O sketch reproduz a CURVA de
// tentativa do worker: quantas vezes tenta, quanto espera entre uma e outra,
// e o instante em que desiste.
//
// E reproduz tambem o trabalho duplicado: o worker processa, marca como
// concluido, e "cai" antes de confirmar a fila. A fila reentrega. A chave de
// idempotencia e o que impede o efeito de acontecer duas vezes.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 3000;

// --- os tres numeros da curva de retry, que o projeto tem que ter escritos ---

// A espera inicial: quantos segundos antes da segunda tentativa.
const int ESPERA_INICIAL_S = 2;

// O fator: o multiplicador do backoff exponencial.
const int FATOR = 2;

// O teto de tentativas: o numero de tentativas antes de abandonar.
const int MAX_TENTATIVAS = 4;

// O teto da espera: mesmo na tentativa dez, espera este valor, nao mais.
const int TETO_ESPERA_S = 32;

// O intervalo da prova do jitter: a espera real fica entre ESPERA e ESPERA+.
// Com jitter zero, todas as mensagens que falharam juntas voltam juntas.
const int INTERVALO_JITTER_S = 4;

// ---------------------------------------------------------------------------
// A TABELA DE TENTATIVAS. O que o professor pede para a turma implementar:
// numero da tentativa, quanto espera antes, e o instante em que acontece.
// ---------------------------------------------------------------------------
int64_t esperaAntes(int tentativa) {
  if (tentativa <= 1) return 0;                       // a primeira e agora
  int64_t espera = ESPERA_INICIAL_S;
  for (int i = 2; i < tentativa; i++) espera *= FATOR;
  if (espera > TETO_ESPERA_S) espera = TETO_ESPERA_S;
  return espera;
}

// O jitter e a parcela aleatoria. Sem ele, o backoff exponencial desfasa o
// grupo em cinco mensagens e nao desfasa em mil.
int64_t esperaComJitter(int tentativa) {
  return esperaAntes(tentativa)
       + random(0, INTERVALO_JITTER_S + 1);   // 0 ate o intervalo
}

// ---------------------------------------------------------------------------
// O TRABALHO. A chave de idempotencia nasce aqui, no momento em que o
// trabalho e criado, e viaja com a mensagem. Quem inicia a operacao gera a
// chave: e a regra do dia 1, aplicada ao trabalho.
// ---------------------------------------------------------------------------
struct Trabalho {
  unsigned long chave;         // chave de idempotencia
  const char* nome;
  int tentativas;
  const char* estado;          // pendente | concluido | falhou
  int efeitosAplicados;        // quantas vezes o efeito aconteceu
};

Trabalho TRABALHOS[4];
int totalTrabalhos = 0;

// A base do banco com restricao de unicidade sobre a chave. E a unica coisa
// que impede o efeito duplicado quando a fila reentrega.
const int MAX_EFETOS_POR_TRABALHO = 1;

// ---------------------------------------------------------------------------
// O EFEITO EXTERNO, com a INTENCAO GRAVADA ANTES. Se o processo morrer
// entre a intencao e o efeito, a proxima tentativa ve a intencao gravada e
// nao repete. E o que fecha o efeito que nao passa pelo banco.
// ---------------------------------------------------------------------------
bool aplicarEfeito(Trabalho& t) {
  if (t.efeitosAplicados >= MAX_EFETOS_POR_TRABALHO) {
    Serial.printf("    chave %lu já tem efeito gravado: não aplicar de novo\n",
                  t.chave);
    return false;                 // idempotencia: o efeito nao se repete
  }
  Serial.printf("    chave %lu: registrando a intenção e aplicando o efeito\n",
                t.chave);
  t.efeitosAplicados++;
  return true;
}

void criarTrabalho(const char* nome, unsigned long chave) {
  TRABALHOS[totalTrabalhos].chave = chave;
  TRABALHOS[totalTrabalhos].nome = nome;
  TRABALHOS[totalTrabalhos].tentativas = 0;
  TRABALHOS[totalTrabalhos].estado = "pendente";
  TRABALHOS[totalTrabalhos].efeitosAplicados = 0;
  totalTrabalhos++;
}

// ---------------------------------------------------------------------------
// O WORKER COM RETRY. Cada falha conta uma tentativa e espera com backoff
// exponencial mais jitter. Quando o numero de tentativas acaba, o trabalho
// e abandonado: marcado como falhou, com o ultimo erro.
// ---------------------------------------------------------------------------
bool tentarComRetry(Trabalho& t) {
  int64_t acumulado = 0;
  while (t.tentativas < MAX_TENTATIVAS) {
    t.tentativas++;
    Serial.printf("    tentativa %d/%d, esperando %lld ms antes\n",
                  t.tentativas, MAX_TENTATIVAS, (long long)esperaComJitter(t.tentativas));
    acumulado += esperaComJitter(t.tentativas);

    // A falha simulada: no projeto, e o servidor fora ou a rede sem resposta.
    bool sucesso = (t.tentativas >= 3);
    if (sucesso) {
      Serial.printf("    tentativa %d deu certo\n", t.tentativas);
      return true;
    }
    Serial.printf("    tentativa %d falhou; volta em %lld ms\n",
                  t.tentativas, (long long)esperaComJitter(t.tentativas + 1));
  }

  Serial.printf("    %s: dar up depois de %d tentativas, ultimo erro foi "
                "'servidor sem resposta'\n", t.nome, MAX_TENTATIVAS);
  return false;
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);
  randomSeed(42);

  Serial.println();
  Serial.println("=== dia 7, aula 2: retry com espera e o trabalho que roda duas vezes ===");
  Serial.println("Backoff exponencial, jitter, e a chave de idempotência.");
  Serial.println();

  Serial.println("--- a curva de tentativas, como o professor escreve na lousa ---");
  Serial.printf("  espera inicial %d s, fator %d, teto de espera %d s, "
                "máximo de tentativas %d\n",
                ESPERA_INICIAL_S, FATOR, TETO_ESPERA_S, MAX_TENTATIVAS);
  Serial.println("  tentativa   espera antes   instante");
  int64_t acumulado = 0;
  for (int t = 1; t <= MAX_TENTATIVAS + 1; t++) {
    Serial.printf("  %8d   %10lld s   %8lld s\n",
                  t, (long long)esperaAntes(t), (long long)acumulado);
    acumulado += esperaAntes(t);
  }
  Serial.println("  a espera dobra, o teto segura, e o jitter espalha.");
  Serial.println();

  criarTrabalho("relatorio do dia", 5001);
  criarTrabalho("recalcular grafico", 5002);
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- o worker tentando um trabalho, com espera ---");
  for (int i = 0; i < totalTrabalhos; i++) {
    Serial.printf("  trabalho: %s (chave de idempotência %lu)\n",
                  TRABALHOS[i].nome, TRABALHOS[i].chave);
    if (tentarComRetry(TRABALHOS[i])) {
      TRABALHOS[i].estado = "concluido";
    } else {
      TRABALHOS[i].estado = "falhou";
    }
    Serial.printf("  estado final: %s, em %d tentativa(s)\n",
                  TRABALHOS[i].estado, TRABALHOS[i].tentativas);
  }
  Serial.println();

  Serial.println("--- o trabalho duplicado: a fila reentrega ---");
  Serial.println("  1. o worker retira a mensagem");
  Serial.println("  2. processa: grava o resultado e marca concluído");
  Serial.println("  3. tenta confirmar a fila e cai ANTES do ack");
  Serial.println("  4. a fila não recebeu nada e reentrega a mesma mensagem");
  Serial.println("  5. o worker processa de novo");
  Serial.println();
  Trabalho& t = TRABALHOS[0];
  aplicarEfeito(t);
  aplicarEfeito(t);   // a reentrega
  Serial.print("  efeitos aplicados ao trabalho 1: ");
  Serial.println(t.efeitosAplicados);
  Serial.println("  sem a chave de idempotência, este número seria 2, e o");
  Serial.println("  relatório teria saído duas vezes.");
  Serial.println();

  Serial.println("--- os números da curva, medidos ---");
  Serial.printf("  tempo total de espera até o máximo de tentativas: ");
  int64_t total = 0;
  for (int i = 1; i <= MAX_TENTATIVAS; i++) total += esperaAntes(i);
  Serial.print(total);
  Serial.println(" s");
  Serial.print("  total com jitter, nesta execução: ");
  int64_t comJitter = 0;
  for (int i = 1; i <= MAX_TENTATIVAS; i++) comJitter += esperaComJitter(i);
  Serial.print(comJitter);
  Serial.println(" s");
  Serial.println("  e esse é o tempo que um trabalho morto ocupa a máquina.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Por que assim e não de outro jeito. As três constantes do topo — ESPERA_INICIAL_S, FATOR e MAX_TENTATIVAS — são os três números que o professor exige que o projeto escreva, e elas estão separadas do código porque são configuração e não lógica. O item 1 da atividade pede exatamente a tabela que a função esperaAntes imprime, e a função existe para que a tabela seja gerada e não digitada: mudar o fator de dois para três muda a tabela inteira.

A esperaAntes faz o exponencial por multiplicação em laço, e não com pow. O motivo é didático e o professor diz: o laço mostra que cada tentativa multiplica a espera pela anterior, e pow esconde exatamente isso atrás de uma função. O teto vem logo depois, e é o que impede a tentativa dez de esperar uma hora — a condição é uma comparação, e ela está escrita na ordem em que o professor a explica na lousa.

A esperaComJitter soma random(0, INTERVALO_JITTER_S + 1), e o +1 é o detalhe que o professor pede para ler. Sem ele, o jitter nunca chega ao valor do intervalo, e a janela real seria menor do que a escrita no projeto. É a mesma armadilha de limite de laço do dia 1, e aparece aqui porque é o mesmo tipo de erro: o limite que se escreve no projeto e o limite que o código usa precisam ser o mesmo número.

A MAX_EFETOS_POR_TRABALHO igual a 1 é a idempotência na forma mais curta que existe: o efeito é aplicado uma vez e só. A função aplicarEfeito verifica antes de agir, e é a ordem que o professor chama de intenção antes do efeito — o registro acontece antes do efeito externo, e se o processo morrer entre os dois, a próxima tentativa vê que já foi.

A segunda chamada a aplicarEfeito no loop é a reentrega sendo executada na frente da turma, e a diferença entre as duas saídas é o argumento inteiro da aula: a primeira imprime "registrando a intenção", a segunda imprime "já tem efeito gravado: não aplicar de novo". A linha do meio da tela é o que a equipe precisa ter no código do worker real, e ela só aparece porque a segunda chamada usa a mesma chave.

O randomSeed(42) no setup é para a demonstração ser reproduzível. Sem ele, cada execução da placa mostraria um número diferente de jitter, e a turma não conseguiria comparar duas execuções. O professor explica que isso é escolha de aula: em produção o jitter é genuinamente aleatório, e o seed fixo é só para o professor poder dizer "olha, deu três" e a turma conferir.

O tentarComRetry tem o while com t.tentativas < MAX_TENTATIVAS e não um while (true). A diferença é o ponto que fecha a seção de dar up depois de N tentativas: o laço com condição de saída é o que permite abandonar o trabalho, gravar o estado falhou e devolver a mensagem do último erro. Um laço infinito com retry é uma máquina ocupada até o fim do dia com trabalho que nunca vai dar certo.

O LED acende durante as tentativas e apaga no intervalo. O intervalo de três segundos é menor que o das outras aulas porque a curva de tentativas tem cinco linhas e a turma precisa reler o bloco antes da próxima volta; o delay do intervalo não acumula com as esperas do retry, que são calculadas e impressas e não executadas de verdade — e o professor avisa que no código real elas seriam um delay de verdade, ou um agendamento na fila.

Criterios de correcao

CritérioPontos
A tabela de tentativas escrita, com espera antes e instante de cada uma2 pontos
O número de tentativas e o dar up, com o instante em que o sistema desiste2 pontos
Os dois contadores do teste com e sem jitter2 pontos
Os três números do projeto escritos, com o tempo total de um trabalho morto2 pontos
Os cinco passos do trabalho duplicado, escritos de memória2 pontos
A chave de idempotência aplicada e o contador de efeitos depois da reentrega1 ponto
O teste do envio duplicado da placa, com os dois contadores1 ponto

Erros comuns

ErroComo apareceCorreção
Reenviar em intervalo fixoA placa manda a mesma leitura a cada dois segundos"Esperar em backoff exponencial desfasa o grupo. Com intervalo fixo, quando o servidor volta chegam todas as mensagens de uma vez e ele cai de novo."
Retry sem teto de tentativasO trabalho tenta a noite inteira e nunca desiste"Escreva o número máximo de tentativas e o que o sistema faz ao atingir: abandonar, marcar falhou e devolver o erro."
Backoff sem teto de esperaA tentativa dez espera horas"O teto corta a espera em algum valor escrito. Sem ele, a curva cresce até a fila parar de drenar sozinha."
Backoff sem jitterMil mensagens que caíram juntas voltam todas no mesmo instante"O jitter é a parcela aleatória. Sem ele o grupo se desfasa em cinco e continua junto em mil."
Registrar o efeito depois de aplicá-loO processo morre entre os dois e o efeito repete"Grave a intenção antes do efeito. Se morrer no meio, a próxima tentativa vê a intenção e não repete."
Tratar recusa de unicidade como erroO worker fica reentregando em laço até cair"Recusa de unicidade é sucesso: o efeito já aconteceu. Confirme a fila e siga."
Chave gerada no processamentoCada reentrega gera chave nova e duplica"A chave de idempotência nasce na criação da operação. Quem inicia gera a chave."
Retry só com limite de tempoUm trabalho que falha em 0,1 s ocupa a máquina a noite toda"Limite de tempo sem limite de tentativas não impede o laço rápido. Os dois juntos limitam quantidade e demora."

Desafio extra

Implemente no seu projeto a curva completa de retry com os três números escritos e meça o tempo total de um trabalho que falha em todas as tentativas, nos dois casos: com jitter e sem jitter. Repita o teste com uma, dez e cem mensagens que falham ao mesmo tempo, e escreva em cada caso quantas mensagens chegaram no mesmo instante. Em seguida, monte o cenário de trabalho duplicado com um efeito que não passa pelo banco — no projeto, um envio de e-mail ou uma chamada de API — e escreva as três linhas que impedem a duplicação desse efeito. Meça de novo o tempo total com essas três linhas no lugar. A pergunta que o professor espera de resposta: em que tamanho de fila o jitter deixa de ser um detalhe e passa a ser o que segura a recuperação, e o que muda no painel de operação quando a taxa de trabalho duplicado deixa de ser zero?

>

A resolucao, compilada

// Aula 2 do dia 7 do 3o trimestre de ESP32 e IoT.
// Gerado a partir da secao ## Resolucao de conteudo/t3/dia07/aula2.md.
// Se mudar o codigo, mude no .md e regere: o .ino e copia do .md.

#include <Arduino.h>
#include <esp_timer.h>

// ===========================================================================
// dia 7, aula 2: retry com espera, e o trabalho que roda duas vezes.
//
// A placa NAO tem fila e NAO tem servidor. O sketch reproduz a CURVA de
// tentativa do worker: quantas vezes tenta, quanto espera entre uma e outra,
// e o instante em que desiste.
//
// E reproduz tambem o trabalho duplicado: o worker processa, marca como
// concluido, e "cai" antes de confirmar a fila. A fila reentrega. A chave de
// idempotencia e o que impede o efeito de acontecer duas vezes.
// ===========================================================================

const int PIN_LED = 2;
const unsigned long INTERVALO_MS = 3000;

// --- os tres numeros da curva de retry, que o projeto tem que ter escritos ---

// A espera inicial: quantos segundos antes da segunda tentativa.
const int ESPERA_INICIAL_S = 2;

// O fator: o multiplicador do backoff exponencial.
const int FATOR = 2;

// O teto de tentativas: o numero de tentativas antes de abandonar.
const int MAX_TENTATIVAS = 4;

// O teto da espera: mesmo na tentativa dez, espera este valor, nao mais.
const int TETO_ESPERA_S = 32;

// O intervalo da prova do jitter: a espera real fica entre ESPERA e ESPERA+.
// Com jitter zero, todas as mensagens que falharam juntas voltam juntas.
const int INTERVALO_JITTER_S = 4;

// ---------------------------------------------------------------------------
// A TABELA DE TENTATIVAS. O que o professor pede para a turma implementar:
// numero da tentativa, quanto espera antes, e o instante em que acontece.
// ---------------------------------------------------------------------------
struct Trabalho {
  unsigned long chave;         // chave de idempotencia
  const char* nome;
  int tentativas;
  const char* estado;          // pendente | concluido | falhou
  int efeitosAplicados;        // quantas vezes o efeito aconteceu
};


int64_t esperaAntes(int tentativa) {
  if (tentativa <= 1) return 0;                       // a primeira e agora
  int64_t espera = ESPERA_INICIAL_S;
  for (int i = 2; i < tentativa; i++) espera *= FATOR;
  if (espera > TETO_ESPERA_S) espera = TETO_ESPERA_S;
  return espera;
}

// O jitter e a parcela aleatoria. Sem ele, o backoff exponencial desfasa o
// grupo em cinco mensagens e nao desfasa em mil.
int64_t esperaComJitter(int tentativa) {
  return esperaAntes(tentativa)
       + random(0, INTERVALO_JITTER_S + 1);   // 0 ate o intervalo
}

// ---------------------------------------------------------------------------
// O TRABALHO. A chave de idempotencia nasce aqui, no momento em que o
// trabalho e criado, e viaja com a mensagem. Quem inicia a operacao gera a
// chave: e a regra do dia 1, aplicada ao trabalho.
// ---------------------------------------------------------------------------
Trabalho TRABALHOS[4];
int totalTrabalhos = 0;

// A base do banco com restricao de unicidade sobre a chave. E a unica coisa
// que impede o efeito duplicado quando a fila reentrega.
const int MAX_EFETOS_POR_TRABALHO = 1;

// ---------------------------------------------------------------------------
// O EFEITO EXTERNO, com a INTENCAO GRAVADA ANTES. Se o processo morrer
// entre a intencao e o efeito, a proxima tentativa ve a intencao gravada e
// nao repete. E o que fecha o efeito que nao passa pelo banco.
// ---------------------------------------------------------------------------
bool aplicarEfeito(Trabalho& t) {
  if (t.efeitosAplicados >= MAX_EFETOS_POR_TRABALHO) {
    Serial.printf("    chave %lu já tem efeito gravado: não aplicar de novo\n",
                  t.chave);
    return false;                 // idempotencia: o efeito nao se repete
  }
  Serial.printf("    chave %lu: registrando a intenção e aplicando o efeito\n",
                t.chave);
  t.efeitosAplicados++;
  return true;
}

void criarTrabalho(const char* nome, unsigned long chave) {
  TRABALHOS[totalTrabalhos].chave = chave;
  TRABALHOS[totalTrabalhos].nome = nome;
  TRABALHOS[totalTrabalhos].tentativas = 0;
  TRABALHOS[totalTrabalhos].estado = "pendente";
  TRABALHOS[totalTrabalhos].efeitosAplicados = 0;
  totalTrabalhos++;
}

// ---------------------------------------------------------------------------
// O WORKER COM RETRY. Cada falha conta uma tentativa e espera com backoff
// exponencial mais jitter. Quando o numero de tentativas acaba, o trabalho
// e abandonado: marcado como falhou, com o ultimo erro.
// ---------------------------------------------------------------------------
bool tentarComRetry(Trabalho& t) {
  int64_t acumulado = 0;
  while (t.tentativas < MAX_TENTATIVAS) {
    t.tentativas++;
    Serial.printf("    tentativa %d/%d, esperando %lld ms antes\n",
                  t.tentativas, MAX_TENTATIVAS, (long long)esperaComJitter(t.tentativas));
    acumulado += esperaComJitter(t.tentativas);

    // A falha simulada: no projeto, e o servidor fora ou a rede sem resposta.
    bool sucesso = (t.tentativas >= 3);
    if (sucesso) {
      Serial.printf("    tentativa %d deu certo\n", t.tentativas);
      return true;
    }
    Serial.printf("    tentativa %d falhou; volta em %lld ms\n",
                  t.tentativas, (long long)esperaComJitter(t.tentativas + 1));
  }

  Serial.printf("    %s: dar up depois de %d tentativas, ultimo erro foi "
                "'servidor sem resposta'\n", t.nome, MAX_TENTATIVAS);
  return false;
}

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(200);
  randomSeed(42);

  Serial.println();
  Serial.println("=== dia 7, aula 2: retry com espera e o trabalho que roda duas vezes ===");
  Serial.println("Backoff exponencial, jitter, e a chave de idempotência.");
  Serial.println();

  Serial.println("--- a curva de tentativas, como o professor escreve na lousa ---");
  Serial.printf("  espera inicial %d s, fator %d, teto de espera %d s, "
                "máximo de tentativas %d\n",
                ESPERA_INICIAL_S, FATOR, TETO_ESPERA_S, MAX_TENTATIVAS);
  Serial.println("  tentativa   espera antes   instante");
  int64_t acumulado = 0;
  for (int t = 1; t <= MAX_TENTATIVAS + 1; t++) {
    Serial.printf("  %8d   %10lld s   %8lld s\n",
                  t, (long long)esperaAntes(t), (long long)acumulado);
    acumulado += esperaAntes(t);
  }
  Serial.println("  a espera dobra, o teto segura, e o jitter espalha.");
  Serial.println();

  criarTrabalho("relatorio do dia", 5001);
  criarTrabalho("recalcular grafico", 5002);
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  Serial.println("--- o worker tentando um trabalho, com espera ---");
  for (int i = 0; i < totalTrabalhos; i++) {
    Serial.printf("  trabalho: %s (chave de idempotência %lu)\n",
                  TRABALHOS[i].nome, TRABALHOS[i].chave);
    if (tentarComRetry(TRABALHOS[i])) {
      TRABALHOS[i].estado = "concluido";
    } else {
      TRABALHOS[i].estado = "falhou";
    }
    Serial.printf("  estado final: %s, em %d tentativa(s)\n",
                  TRABALHOS[i].estado, TRABALHOS[i].tentativas);
  }
  Serial.println();

  Serial.println("--- o trabalho duplicado: a fila reentrega ---");
  Serial.println("  1. o worker retira a mensagem");
  Serial.println("  2. processa: grava o resultado e marca concluído");
  Serial.println("  3. tenta confirmar a fila e cai ANTES do ack");
  Serial.println("  4. a fila não recebeu nada e reentrega a mesma mensagem");
  Serial.println("  5. o worker processa de novo");
  Serial.println();
  Trabalho& t = TRABALHOS[0];
  aplicarEfeito(t);
  aplicarEfeito(t);   // a reentrega
  Serial.print("  efeitos aplicados ao trabalho 1: ");
  Serial.println(t.efeitosAplicados);
  Serial.println("  sem a chave de idempotência, este número seria 2, e o");
  Serial.println("  relatório teria saído duas vezes.");
  Serial.println();

  Serial.println("--- os números da curva, medidos ---");
  Serial.printf("  tempo total de espera até o máximo de tentativas: ");
  int64_t total = 0;
  for (int i = 1; i <= MAX_TENTATIVAS; i++) total += esperaAntes(i);
  Serial.print(total);
  Serial.println(" s");
  Serial.print("  total com jitter, nesta execução: ");
  int64_t comJitter = 0;
  for (int i = 1; i <= MAX_TENTATIVAS; i++) comJitter += esperaComJitter(i);
  Serial.print(comJitter);
  Serial.println(" s");
  Serial.println("  e esse é o tempo que um trabalho morto ocupa a máquina.");
  Serial.println();

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_MS);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia07 aula2.