Tarefas em background utilizando filas com Redis e Kue

Muitas vezes precisamos fazer operações em lotes tais como disparo de e-mails, chamada a API's externas que na maioria das vezes "travam" nossas requisições até essas ações serem concluídas, já pensou em não precisar espera-las para continuar utilizando sua aplicação?

Acontece que as vezes você é "obrigado" a esperar a requisição para saber o resultado dela como é o caso do envio de e-mail em lotes, é preciso saber se todos e-mails foram enviados por exemplo.

No final das contas você tem que escolher entre esperar o seu código retornar alguma coisa, seja o resultado esperado ou um erro, o que acaba poluindo muito o código ou retornar a requisição mais rápida sem saber o que exatamente aconteceu.

Solução

Para juntar o útil ao agradável você pode passar a responsabilidade para o background de forma responsável, através de uma fila de processamento. Com a fila de processamento você ganha com a requisição rápida e com o controle do que está acontecendo por baixo dos panos.

É natural as perguntas de quem assumirá as responsabilidades? Como saber se tudo foi feito com êxito? E se falhar? E elas serão tratadas de acordo com as libs que trabalharmos.

A combinação perfeita para isso se dá entre o Redis e o Kue, que discorreremos mais abaixo. O Redis para guardar valores e informações inerentes a fila e o Kue que assume a responsabilidade de manipular a fila em si.

Redis

Eu vos apresento o Redis.

O Redis é um banco de dados não relacional de chaves e valores, onde as chaves podem ser quaisquer valores e os valores idem. Ele é extremamente rápido, pois fica em memória e tem um fallback no disco para eventuais morte súbita no servidor.

Na verdade o Redis não assumirá a responsabilidade em si, ele trabalha junto com quem fize-lo, isso porque ele consegue guarda as informações que você trabalhará de maneira rápida e prática, isso pensando até mesmo em milhares de dados.

Kue

O Kue será o responsável por orquestrar a fila de processamento e ele faz isso de uma forma muito eficiente, e foi construído para se integrar com o Redis. Veja algumas vantagens:

  • Definir prioridade do processamento;
  • Definir o progresso e o acompanhamento do processamento;
  • Escutar eventos como: start, progress, failed e complete;
  • Definir um número de tentativas em caso de falha;
  • Definir o número de processamentos simultâneo;
  • Disponibiliza interface gráfica para acompanhamento;
  • Tempo de vida para o processamento;

Com ele você define um Job, que consiste na junção de um nome para o seu processo e uma função que irá executa-lo, e depois você chama esse Job  jogando ele na sua fila de processamento com os dados de entrada e algumas opções.

Definindo um Job:

const kue = require('kue')
const Queue = kue.createQueue()
const Mail = require('./Mail')

Queue.process('SendMail', async (data, done) => {
    await Mail.send(data)
    return done()
})

module.exports = Queue

Enviando um Job para o processamento:

const Queue = require('../services/Queue')
const User = require('../models/User')
class User {
  async store(req, res) {
    const user = await User.create(req.body);
    const email = {
      from: '"Rocketseat" <oi@rocketseat.com.br>',
      to: user.email,
      subject: 'Seja bem-vindo!'
    }
    const job = Queue.create('SendMail', email).save()
    return res.json(user)
  }
}

Concorrência

É possível definir quantos processos serão feitos em concorrência, isto é, quantos processos serão executados ao mesmo tempo. Isso é muito legal para ser usado para trabalhos em lote, pois reduz o tempo de execução caso tudo ocorra bem.

Queue.process('SendMail', 10, async (data, done) => {})

Acima foi definido a execução de até 10 processos em paralelo.

E o cliente?

Como estamos falando de processos em background o cliente não consegue saber se o trabalho foi feito, de alguma forma ele precisa ser notificado. Isso pode ocorrer através de e-mail, como foi o caso do exemplo, mas também pode ser pelo OneSignal ou socket.io.

Você pode enviar a mensagem para o usuário assim que o seu Job finalizar, escutando o evento complete:

// services/Queue.js
// ...

job.on('complete', (result) => {
 io.emit('Notification', result)
})

Tratando falhas

Outra coisa importante é lidar com as eventuais falhas. O Kue permite você definir o número de tentativas de retrabalho em caso de erro. Por exemplo, tente 5 vezes antes de marcar o trabalho como falho.

Queue.create('SendMail', email).attempts(5).save()

Como você pode escuta diversos eventos, temos o failed attempts e o failed para você tratar as tentativas de falha ou a falha em si:

job.on('failed attempts', (messageError, doneAttempts) => {
  io.emit('Notification', 
    `Ocorreu o seguinte erro: ${messageError}.
    Essa é a ${doneAttempts} tentativa.`
});

job.on('failed', (messageError) => {
  io.emit('Notification', `Ocorreu o seguinte erro: ${messageError}.`
});

Você pode utilizar o evento que escuta a falha da fila (Queue) com o Sentry, deixaria as coisas mais interessantes ainda.

Fim

A ideia desse post é te mostrar um caminho para melhorar a resposta da sua API de forma prática e menos verbosa possível, como deve ser feito em NodeJs. Como não é uma solução nova, porém muito boa, existe uma infinidade de materiais relacionados ao Kue e ao Redis.

E se você gostou desse post ou tem alguma dúvida, não exite em deixar o seu comentário e até mesmo compartilhar com seus amigos.

Abraços, até mais.