E aí, tudo certo?
Nesse artigo rápido, decidi escrever sobre geração de arquivos assíncronos em APIs, pois vi esse tema sendo debatido no Twitter, onde alguém pediu por alguma forma bacana de contruir uma. Entendi que o tema interessou algumas pessoas e cá estamos.
Quando falamos sobre uma API responsável por gerar um arquivo (.txt, .csv, etc), as primeiras dúvidas que surgem, geralmente são: Mas a resposta da API vai ser o arquivo? Vai ser um JSON? Se for um JSON, é o front que vai gerar o arquivo? Mas espera... Então a geração vai ser síncrona? Se for, o front vai ter que ficar aguardando? E se o arquivo for gigante? Enfim, many many doubts!
Vou compartilhar aqui uma estratégia que já usei e gostei, achei simples e dinâmica. Gosto de explicá-la usando a analogia do food truck. Quando você pede um hambúrguer em um food truck, como funciona? Uma das formas é a seguinte:
- Você faz o pedido no caixa, paga e recebe uma senha;
- Você fica de olho no painel de senhas e quando chega a sua, vai até a bancada e retira seu hambúrguer.
Notou que isso é assíncrono? Você não fica no caixa esperando o seu hambúrguer ficar pronto para que o caixa atenda a próxima pessoa. O caixa delega a geração do hambúrguer à alguém para que ele fique livre e atenda o próximo cliente.
A geração de arquivos através de uma API que eu vou mostrar aqui funciona exatamente da mesma forma. Vamos lá! Primeiramente, eu criei uma API que recebe uma requisição contendo os parâmetros para a geração do arquivo (um intervalo de datas, outros dados relevantes, etc). A única responsabilidade dessa API é responder com um hash, um UUID, ou qualquer código de identificação única. Ou seja, essa API nunca retorna um erro de negócio, sempre retorna um hash, e esse retorno deve ser instantâneo.
Esse hash é a "senha". Assim, a API já fica livre para antender uma próxima requisição. Aí você pergunta: Tá, mas e o arquivo? Pois bem. A API, antes de retornar o hash na resposta, monta um evento referente à geração do arquivo e publica esse evento através do nosso querido ApplicationEventPublisher!
Essa publicação, feita pela API, é capturada por um listener construído especificamente para isso. Esse listener é o responsável por dar seguimento à geração do arquivo. Dessa forma, temos a geração assíncrona. Ou seja, a API monta e publica o evento, logo em seguida devolve o hash como resposta da requisição e encerra seu trabalho. Ela não tem controle sobre quando o listener irá capturar o evento e gerar o arquivo, são processamentos diferentes.
E a pergunta final: Cadê o arquivo? Onde ele vai parar e como o usuário tem acesso a ele? Aqui, eu uso uma tabela de status de geração dos arquivos. Uma tabela simples, no banco de dados, que informa em que pé está a geração requisitada.
A chave dela é o hash gerado pela API. Antes da geração ser iniciada pelo listener, é inserido um registro referente à essa etapa com o hash e o status "PROCESSING". Caso a geração do arquivo ocorra com sucesso, é possível upar esse arquivo em um servidor, obter um link para download do mesmo e atualizar a tabela pelo hash, trocando o status para "DONE" e adicionando o link para download. Caso ocorra algum erro na geração do arquivo, é possível alterar o status para "ERROR" e adicionar uma descrição do erro que ocorreu. A granularidade desse erro fica a critério de você, querido padawan.
Por fim, uma boa pedida é ter uma segunda API para consultar o status da geração do arquivo, através do hash, e obter o link de download do mesmo, ou o erro. Não sei se essa é a melhor das melhores estratégias para gerar um arquivo assíncrono, mas veio bem a calhar e trouxe dinamismo à minha aplicação. Espero ter ajudado e até a próxima =)
Top comments (1)
A primeira pergunta que me vem a mente quando eu vejo uma implementação assíncrona é... por quê?
Temos dois casos comuns: a operação é lenta, e separar a requisição da geração diminui o risco de perda de conexão, instabilidade, excesso de threads no serviço, etc; o outro motivo comum é que o backend simplesmente dispara a execução para outro serviço backend, muito comum em arquiteturas orientadas a microsserviços.
Mas se essa requisição é lenta, ela é também geralmente cara - o motivo de uma operação ser lenta é porque ela consome recursos, já que ninguém mantém um Pentium 3 em produção hoje em dia, nesses tempos de cloud.
Nesse contexto, idempotência surge como um padrão natural para se usar junto com o disparo assíncrono de requisições. Se a operação é cara, repetir ela desnecessariamente é desperdício.
No aspecto de busca do resultado, também é possível usar um pouco de arquitetura para tornar a solução exponencialmente mais eficiente: pode-se usar métricas para determinar a duração da operação, e obter "tempo esperado" (p75), e "máximo tempo esperado" (p99), e informar o frontend para tentar buscar o resultado em p75+ jitter, fazer um retry em p99+ jitter, e daí pra frente exponential backoff até desistir.
Talvez não seja nem necessário manter o status do processo em banco ou memória. Se o arquivo tem tamanho 0 bytes, o status está em processamento. Se tem 5 bytes (ERROR), então retorna arquivo.error. Se tem mais que 5 bytes, retorna o arquivo. Cortamos várias operações de controle desnecessárias, e agora o sistema tem um gargalo a menos para escalabilidade. Um backend stateless que confere um file storage bucket pode escalar livremente, sem esbarrar em limites de conexões ou tráfego interzonal.
O melhor sistema é aquele que faz exatamente o que precisa ser feito, e nada mais além disso, com o menor número de dependências e linhas de código possível. Recomendo as palestras do Dan North, especialmente as que discutem "code that fits in your head".
Some comments have been hidden by the post's author - find out more