Executando verificação de segurança...
8

Como resolvi um problema usando processamento assíncrono com filas

Há alguns meses assumi o desenvolvimento de um projeto e logo de cara precisei resolver um problema que estava acontecendo na geração de relatórios. Nesse post eu vou explicar um pouco mais sobre esse problema e como resolvi usando processamento assíncrono com filas.

Cenário problemático

O sistema estava desenhado para quando o usuário clicar no botão de "Baixar relatório", um endpoint no backend era chamado. O endpoint buscava os dados do banco de dados, processava os dados e retornava para o frontend em formato csv, então o arquivo csv era baixado para a máquina do usuário.

Esse fluxo até que funcionava bem quando o relatório tinha poucos registros, mas quando o número de registros aumentava um pouco (cerca de 2000 registros), isso já gerava alguns problemas como timeout na requisição, e as vezes até travava a aplicação inteira pelo consumo exagerado de memória no processamento dos dados.

Solução

A solução adotada foi mudar o fluxo de download dos relatórios, agora quando o usuário solicita um relatório, o backend vai criar um "job" para esse relatório, adiciona-lo a uma fila de processamento e retornar a requisição imediatamente para o frontend informando que o job foi criado.

Agora, com esse job adicionado na fila, o sistema em um outro processo que vai consumir os jobs que chegam na fila, vai gerar o relatório e quando terminar vai notificar o usuário que o solicitou.

Além de mover o processamento do relatório para ser executado em segundo plano, o processamento também foi dividido em pequenas partes (chunks), ou seja, invés de buscar 1 milhão de registros no banco de dados, trazer isso para a memória da aplicação e processar, agora a aplicação busca os registros de 100 em 100 por exemplo, processa os 100, quando termina de processar, busca mais 100 até processar o relatório todo.

Dessa forma, o problema de timout foi resolvido, já que o endpoint não processa mais o relatório todo na mesma requisição. O problema de consumo de memória também foi resolvido, já que agora há um número limitado de registros que podem ser processados ao mesmo tempo.

Tecnologias

Para esse projeto, eu utilizei as ferramentas do ecosistema Laravel. Usei as filas do laravel, integrada com o redis, e para processar e agrupar os chunks usei os Job batches.

Conclusão

Independente da tecnologia, o mais importante é entender o conceito de filas e processamento assíncrono e saber quando utilizar. Nesse caso específico a solução proposta funcionou bem e o problema foi resolvido.

Pretendo postar mais sobre os meus aprendizados do dia a dia em formato de blog e talvez em vídeo também.

Para quem quiser acompanhar, deixei como fonte o link para o post no meu blog onde você pode se cadastrar para receber os meus próximos conteúdos.

Carregando publicação patrocinada...
3

muito bom!

chegou a revisar o processamento como um todo? as consultas no sql, possiveis regras/contas e tudo mais? falo pq para já começar a engasgar com só 2k registros pode ser alguma outra lógica que não está bacana

2

Sim, até deu uma melhorada depois de uma refatoração mas conforme ia aumentando a quantidade de registros a aplicação já abria o bico. Nesse caso a fila era a melhor opção mesmo. Não deu tanto trabalho, o Laravel facilita demais a implementação, mas é importante entender o que está acontecendo por debaixo dos panos.

3

Bacana, me identifiquei com o post pois recentemente tive esse mesmo problema com a diferença que o relatório que o sistema gera é em pdf ou planilha excel.
No meu caso utilizei streaming de dados com generator do python com fastapi e no front-end alterei o fluxo para abrir o download em uma aba separada, dessa forma o browser gerencia o download do arquivo e o fluxo fica livre para o usuário.

1

Aí sim, é uma otima solução também. Você consegue compartilhar alguma referencia que te ajudou na época?

Eu tentei essa abordagem também, mas não encontrei uma maneira do browser fazer o download via stream, eu ia ter que baixar como stream na memória e depois fazer o download na máquina do usuário, então preferi fazer via fila, salvar no bucket e depois notificar o usuário pra ele baixar o arquivo processado direto do bucket.

3

Sua solução é muito elegante, cumpre muito bem o papel.

Um pequeno exemplo de como eu fiz, adaptei para .csv:

Python / Fastapi

import csv
import io
import random
import string

def gerar_dados_csv(tamanho_desejado_mb):
    data = io.StringIO()
    writer = csv.writer(data)
    writer.writerow(["ID", "NAME", "AGE", "COUNTRY"])
    total_size = 0

    while total_size < tamanho_desejado_mb * 1024 * 1024:
        id = random.randint(1, 1000)
        name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
        age = random.randint(20, 60)
        country = random.choice(['Brazil', 'India', 'USA', 'Canada', 'UK'])

        writer.writerow([id, name, age, country])

        chunk = data.getvalue()
        total_size += len(chunk)

        data.seek(0)
        data.truncate(0)

        yield chunk


@router.get('/csv/{tamanho}')
async def get_pdf(tamanho: int):

    nome_arquivo = f'relatorio.csv'
    headers = {"Content-Disposition": f"attachment; filename={nome_arquivo}", 'x-filename': nome_arquivo}

    return StreamingResponse(gerar_dados_csv(tamanho), media_type='application/csv', headers=headers)

Javascript

  /**
   *  Método para realizar download em uma nova aba, com responsabilidade
   *  do processo de download a cargo do browser.
   *
   * @param {String} url para download
   */
  async function fileDownload(url) {

    try {
      // url completa, com path variables e query params.
      const _url = `${axiosAPI.defaults.baseURL}${url}`
      const anchorElement = document.createElement('a')

      anchorElement.setAttribute("href", _url)
      anchorElement.setAttribute("target", "_blank")

      document.body.appendChild(anchorElement)
      anchorElement.click()

      document.body.removeChild(anchorElement)
    }
    catch (error) {
      console.log('error: ', error)
      throw new Error("Erro ao baixar arquivo.")
    } 
  }
2

Opa meu amigo, muito criativa sua solução. Tenho um caso parecido em meu sistema, mas com planilhas exel e arquivos json, eu estava pensando em utilizar Workers no front-end para não travar a tela do usuáro e fazer o download via stream. Acho que é possível juntar isso com sua solução para ficar ainda mais eficiente ainda.

Uma dúvida que tive ao ler o seu post é, como que o back-end iria notificar o front-end?

"...vai gerar o relatório e quando terminar vai notificar o usuário que o solicitou."

1

A notificação vai depender do sistema, pode ser via email ou na aplicaçao (in-app). No meu caso a notificaçao foi na aplicação mesmo.

2
1