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
ecomplete
; - 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.