Rocketseat

Acelerando o Job de Auditoria

Acelerando o Job de Auditoria

Conheça o projeto

Este desafio simula um sistema de auditoria de logs que leva 45 minutos para processar dados. O objetivo é criar um motor de agregação em Go demonstrando domínio progressivo de concorrência através de quatro implementações: sequencial, concorrente ingênua (com race conditions), com Mutex, e usando Worker Pool com Channels, conceitos essenciais para sistemas de alta performance.

Recursos

Materiais para você usar como base para o desenvolvimento

Instruções

Estrutura, regras e requisitos do projeto

Nesse seguinte cenário hipotético temos uma equipe de Business Intelligence (BI) precisa de um relatório de auditoria de todos os eventos da última hora. O problema? O job atual que processa esses dados está demorando 45 minutos para rodar. Ninguém consegue trabalhar assim.

Sua Missão: É criar do zero um novo motor de agregação em Go para resolver esse gargalo de uma vez por todas. Nossos servidores despejam milhares de arquivos de log JSON em um diretório de logs. Sua tarefa é usar o arsenal de concorrência do Go (Goroutines, Channels e Mutexes) para implementar esse job de agregação.

🎯 Objetivos

O objetivo aqui é que você demonstre sua habilidade em aplicar os seguintes conceitos:

  • Além do Básico: Provar que você domina a diferença entre processamento Síncrono (lento e seguro), concorrência (complexo e rápido) e Paralelismo (a execução que buscamos).
  • Visão de Raio-x (Race Conditions): Não apenas corrigir bugs de concorrência, mas antecipá-los. Você deve ser capaz de provar que um código "ingênuo" está quebrado (usando go run -race .) antes mesmo que ele corrompa dados em produção. Este é o tipo de bug que derruba sistemas.
  • Domínio dos Locks (Trade-offs): Saber quando usar um sync.Mutex para proteger seções críticas. E, mais importante, saber quando não usar. Você deve entender e medir o custo da Contenção de Lock — o ponto onde sua solução "concorrente" se torna mais lenta que a síncrona porque todas as goroutines estão paradas em uma fila.
  • Arquitetura de Concorrência (Padrões Reais):
    • Pipelines Escaláveis: Projetar pipelines de processamento (Fan-Out/Fan-In) que evitam a necessidade de locks em pontos quentes, usando channels para orquestrar o fluxo de dados.
    • Gerenciamento de Recursos (Worker Pools): Controlar o nível de paralelismo. Não queremos 10.000 goroutines abrindo 10.000 arquivos de uma vez. Você deve implementar um pool de workers para evitar que seu serviço sufoque a máquina.
    • Ciclo de Vida (Robustez): Dominar o sync.WaitGroup e o fechamento de channels para garantir que o sistema desligue de forma limpa, sem perder um único evento que estava "em trânsito".

💻 Suas Tarefas (O "Como")

Você deve criar um arquivo main.go que implemente as 4 funções a seguir. Na sua função main, você deve:

  1. Gerar um conjunto de arquivos de log de teste (use a função GenerateMockFiles fornecida).
  2. Chamar cada uma das 4 funções.
  3. Medir o tempo de execução (time.Since(start)) de cada uma e imprimir os resultados para comparação.

🎯 Parte 1: A Baseline (Processamento Síncrono)

Implemente a versão mais simples e direta. Ela deve processar um arquivo de cada vez, sequencialmente, na goroutine principal.

  • Assinatura: func ProcessSequential(files []string) *Report
  • Lógica:
    • Crie um Report usando NewReport().
    • Use um for...range padrão sobre a lista de files.
    • Dentro do loop, abra o arquivo, leia-o linha por linha (sugestão: bufio.Scanner).
    • Para cada linha válida, decodifique o JSON para um Event e chame report.AddEvent(event).
    • Para cada linha inválida, chame report.AddError().

🎯 Parte 2: Concorrência Ingênua (O Problema)

Agora, vamos tentar "acelerar" as coisas processando cada arquivo em sua própria Goroutine. Esta implementação é intencionalmente falha.

  • Assinatura: func ProcessConcurrentNaive(files []string) *Report
  • Lógica:
    1. Crie um único Report compartilhado.
    2. Crie um sync.WaitGroup para rastrear as goroutines.
    3. Itere sobre files. Para cada arquivo:
      • wg.Add(1)
      • Inicie uma goroutine go func(filename string) { ... }.
      • Dentro da goroutine: defer wg.Done(), processe o arquivo (abrir, ler, decodificar).
      • Para cada evento, chame report.AddEvent(event).
      • Para cada erro, chame report.AddError().
    4. Chame wg.Wait() para esperar todas as goroutines terminarem.
    5. Retorne o relatório.

🎯 Parte 3: Corrigindo com Mutex (A Solução de Memória Compartilhada)

Agora que você viu o problema, corrija-o usando a abordagem de "memória compartilhada protegida".

  • Assinatura: func ProcessConcurrentMutex(files []string) *Report
  • Lógica:
    1. Copie sua implementação da Parte 2 (incluindo WaitGroup e goroutines).
    2. A única diferença é: dentro da goroutine, em vez de chamar os métodos AddEvent e AddError, você deve chamar as versões seguras: report.AddEventSafe(event) e report.AddErrorSafe().

🎯 Parte 4: O Padrão Idiomático (Worker Pool & Channels)

Esta é a arquitetura mais escalável. Vamos refatorar para um padrão que evita a memória compartilhada, seguindo o lema do Go: "Não comunique compartilhando memória; compartilhe memória comunicando."

  • Assinatura: func ProcessPipeline(files []string, numWorkers int) *Report
  • Lógica:
    1. Canais: Crie dois canais:
      • jobs := make(chan string, len(files))
      • results := make(chan ProcessResult, 1000) (Use uma struct ProcessResult que possa conter ou um Event, ou um error).
    2. Workers (Fan-Out):
      • Crie um sync.WaitGroup para os workers.
      • Inicie numWorkers goroutines.
      • Cada worker deve ler filename do canal jobs (for filename := range jobs).
      • O worker processa o arquivo (abrir, ler, decodificar) e envia ProcessResults para o canal results.
    3. Aggregator (Fan-In):
      • Crie um NewReport().
      • Inicie uma única goroutine agregadora que será a única a ler do canal results (for res := range results).
      • Dentro dela, verifique o ProcessResult: se res.Err != nil, chame report.AddError(); senão, chame report.AddEvent(res.Event). (Note que usamos as versões sem lock, pois esta é a única goroutine que modifica o report).
    4. Coordenação:
      • Envie todos os files para o canal jobs.
      • close(jobs) (Sinaliza aos workers que não há mais trabalho).
      • Espere todos os workers terminarem (wg.Wait()).
      • Depois que eles terminarem, close(results) (Sinaliza ao agregador que não há mais resultados).
      • Espere o agregador terminar (use um canal done ou outro WaitGroup) e então retorne o report.

Resultados Esperados

  1. Os Dados Precisam Estar Corretos (Não-Negociável):
    • As implementações, Parte 1 , Parte 3 e Parte 4 devem produzir um relatório final com resultados idênticos.
    • A Parte 2 deve, previsivelmente, falhar em produzir o resultado correto ou, no mínimo, ser comprovadamente insegura.
  2. Robustez e Segurança:
    • A execução do go run -race . deve passar limpa (sem avisos) para as Partes 1, 3 e 4.
    • A execução do go run -race . na Parte 2 deve obrigatoriamente detectar e reportar a "DATA RACE". Você deve ser capaz de apontar no código exatamente onde e por que ela ocorre.
  3. Performance Mensurável:
    • Você deve fornecer métricas de benchmark (usando time.Since(start) ou o pacote testing) que comprovem a melhoria de desempenho das Partes 3 e 4 em relação à baseline (Parte 1) usando um conjunto de dados significativo (ex: 100 arquivos, 10.000 linhas/arquivo).
  4. Adesão aos Padrões Exigidos:
    • A Parte 3 deve usar sync.Mutex (ou sync.RWMutex) para garantir a segurança.
    • A Parte 4 deve obrigatoriamente usar o padrão Worker Pool e um pipeline de channels (Fan-Out/Fan-In), removendo a necessidade de um lock no agregador.

Tarefas

Use este checklist para ajudar a organizar a sua entrega

Resolução

Confira os resultados esperados do projeto

Paywall background

Envie o projeto para ver a resolução

Ao enviar seu projeto, você poderá conferir os resultados esperados

Projetos relacionados