Programação assíncrona em Node.js – Callbacks e Promises

Computadores são assíncronos por padrão. Isto significa que coisas podem acontecer de maneira independente do fluxo do programa principal. Nos computadores atuais, cada programa roda por um tempo específico e então pára sua execução para permitir que outro programa execute um pouco. Isto acontece tão rápido que é quase impossível perceber e temos geralmente a falsa impressão de que o computador está de fato fazendo várias coisas ao MESMO tempo, quando na verdade ele está fazendo UMA coisa de cada vez, um pouco de cada.

Claro, isso tendo uma CPU em mente. A realidade para UMA CPU é essa.

Programas internamente usam interrupções, um sinal que é emitido para o processador quando o programa quer a atenção do sistema. Logo, é normal para os programas serem assíncronos: eles começam, são deixados processando sozinhos e quando algo novo requer a atenção do sistema, eles emitem esse aviso.

No entanto, normalmente as linguagens de programação são síncronas (executam o código em série, uma linha depois da outra) e algumas fornecem mecanismos de assincronicidade através de bibliotecas. C, Java, C#, PHP, Go, Ruby, Swift, Python e JavaScript. Todas elas são síncronas por padrão e várias delas permitem assincronicidade usando threads, criando novos fluxos do processo.

Em Node.js, por outro lado, as coisas são diferentes, graças ao Event Loop. Resumidamente, dependendo do tipo de processamento necessário, a atividade sai da thread principal e é enviada para um pool de threads em background automaticamente e só retorna ao event loop quando terminar.

Como o Node se organiza para isso? Inicialmente através dos callbacks!

Callbacks em Node.js

Um callback é um listener, uma função que será disparada QUANDO e SE um evento acontecer. Isso já era usado desde os primórdios do JavaScript, quando ele foi inventado para rodar no front-end dos sites. Criávamos um código para ser disparado no clique de um botão, ou seja, no FUTURO. Não era na sequência linear do programa, era pra ser assíncrono (fora da sincronia normal), entende?

Como no JavaScript mesmo as funções são objetos, podemos armazená-las para serem disparadas mais tarde e esse princípio é a base dos callbacks. Um callback, resumidamente é uma função para ser “chamada de volta” (“to be called back” em Inglês) quando outra função terminar e você encontra isso desde o JS “raiz” (vanilla) como no exemplo abaixo.

Um dos desafios desse modelo é como fazer a gestão de erros. E uma solução encontrada é o que é feito no Node.js: o primeiro parâmetro de qualquer callback é o objeto de erro e isso é um padrão em Node.js, é o jeito default de se trabalhar com a tecnologia.

Essa simplicidade e praticidade é excelente para cenários simples. No entanto, outro problema com os callbacks, esse jamais sanado é o aninhamento de callbacks, o famoso callback hell (inferno de callbacks)!

Afinal, como lidar quando um callback também possui um callback? E se esse callback do callback possuir outro callback? O exemplo abaixo em JavaScript puro ilustra um pouco isso, em apenas 4 níveis:

Em uma tecnologia assíncrona como Node.js, isso não é algo incomum e o uso de callbacks em cenários complexos acaba se tornando um pesadelo rapidamente.

A partir da versão 6 do ECMAScript (ES6, de 2015), foi introduzido Promises, uma nova e mais elegante maneira de lidar com o encadeamento de funções assíncronas (async function chaining) e vai ser alvo de estudo neste artigo.

Promises em Node.js

Uma promise (promessa, em Inglês) é comumente definida como um proxy para um valor que eventualmente estará disponível. Embora este recurso seja mais antigo que 2015, foi neste ano que ele se tornou padrão na implementação ECMAScript e mesmo que em 2017 tenha sido implementado async/await na linguagem, Promises ainda é a base e deve ser entendida.

Basicamente, uma vez que uma promise é chamada, ela inicia em pending state (pendente). Isto significa que a função caller (que chamou a promise) continua sua execução enquanto espera pela promise terminar seu próprio processamento e retornar ao caller com algum feedback.

Quando esse retorno acontece, a promise é retornada em resolved state ou rejected state.

Criando uma Promise em Node.js

Para criar uma promise é bem simples, usa-se o construtor da própria classe e passa-se uma function como único argumento, que possui funções de resolve e reject na sua assinatura. Dentro dessa function, você deve chamar resolve quando sua execução for bem sucedida, ou reject, caso contrário.

Abaixo um pequeno exemplo:

Como pode ver, criei uma função que só aceita pares (ignore o fato dela ser beeeem simples, foque nos conceitos). Essa função retorna uma promise para permitir que seu conteúdo rode de maneira assíncrona. A promise que será retornada avalia o argumento ‘numero’ e se ele for par, retorna uma promise resolved, mas caso contrário, retorna uma promise rejected. Ambas funções de retorno (resolve e reject) permitem a passagem de um objeto por parâmetro que vai ser lido por quem estiver aguardando um feedback dessa promise.

Consumindo uma promise em Node.js

Usar uma promise é ainda mais fácil que criar uma e vou usar como base o código anterior. Quando você for chamar a function soAceitaPares, você vai chamá-la normalmente e depois captura o seu retorno positivo (resolved) com a função then e o retorno negativo (rejected) com a função catch, como abaixo.

Ao executar esse código, você verá que ele vai imprimir primeiro a linha teste, depois o conteúdo do then e NÃO vai chegar nem perto do conteúdo do catch. Agora se trocar o argumento de soAceitaPares para um número ímpar, verá que ele continua imprimindo o teste, executa o conteúdo do catch e não do then.

Por que ele imprime o teste primeiro? Porque visualmente parece síncrono, mas na verdade a função soAceitaPares é chamada e o código continua executando, imprime o teste e só quando a função terminar é que vai executar o then ou o catch.

Quando usamos bibliotecas de terceiros que implementam promises (a maioria das modernas), é assim que tratamos seu retorno.

Mas e quando o resultado de uma promise é uma função que retorna outra promise? Será que não caímos no mesmo problema do callback hell?

Encadeando promises em Node.js (chaining)

Uma promise pode ser retornada por outra promise, criando uma cadeia ou corrente de promises (chain) e é pra isso que justamente ela foi criada, afinal, para cenários simples os callbacks são excelentes.

Vamos criar outra função de exemplo, que também retorna uma promise:

Nesta função, usei outra abordagem de criar promises. Se o número for ímpar, a promise é criada já com o tipo rejeitada retornando um erro. Agora se for par, ela é criada já com o tipo resolvida e retorna a metade do número original. Na prática, ela funciona de maneira exatamente igual à instanciação de promise que fiz anteriormente, escolha a que for mais conveniente ao seu código (eu intercalo conforme vai ficar melhor no código).

Sabemos que cada uma destas duas funções são assíncronas e que se queremos ir para a segunda (dividePelaMetade) só depois de verificar o retorno da primeira (soAceitaPares), não podemos simplesmente usar um if como abaixo, que não funciona.

Para garantir que dividePelaMetade só seja executada depois que soAceitaPares retornar dizendo se o número é par ou não, temos de encadear o consumo das promises, como abaixo:

Assim, a função soAceitaPares é chamada e depois o código continua executando (imprime teste). Quando soAceitaPares terminar, se for sucesso, executará o primeiro then (dividePelaMetade), caso contrário, vai pro catch. Note que ignorei o retorno (result) da função, por que ele é inútil neste caso (mas em casos reais geralmente ele é útil).

Quando dividePelaMetade terminar, se for sucesso, executará o segundo then, cuja variável result2 é o retorno da promise (resolve), ou seja, o número dividido. Caso contrário, ele vai pro catch com o erro.

Em uma situação que a segunda função também retornasse uma promise, teríamos um terceiro then e assim por diante. Já o catch é apenas um, sempre no final, a menos que seu catch dispare um erro, que pode ser capturado por um segundo catch, mas não recomendo.

Mas e quando você tem várias promises que não são exatamente executadas uma depois da outra, mas que todas precisam ter sido finalizada para se tomar uma decisão?

Sincronizando promises em Node.js

Imagine a situação da imagem abaixo.

Fork e Join

Esse é um problema clássico da computação: um processo é “forkado” em vários subprocessos (threads, mas imagine aqui Promises), cada um executando de maneira independente. Porém, em dado momento posterior, o resultado de todos deve ser computado de uma única maneira.

Como sincronizamos (join) promises em Node.js?

Através da função Promise.all()

Como cada Promise também é um objeto em JavaScript, podemos armazenar todas as promises em um array e depois usando Promise.all() processamos todas elas e o array de resultados é passado em um único then, ou no catch, caso alguma dê erro.

No exemplo acima, declarei um array de números e um array de promises. O primeiro é usado em um foreach porque quero dividir todos eles pela metade. Essa função de divisão é assíncrona e retorna uma promise, que eu facilmente adiciono no array de promises.

Como não sabemos quando todas as divisões vão terminar, eu uso o Promise.all na linha seguinte sobre o array de promises e com isso, o then somente será executado quando TODAS promises retornarem. Se todas forem bem resolvidas, um array de results será passado ao then. Caso contrário, se apenas UMA já der erro, o catch será disparado.

Mantive a impressão de teste no final para você ver que as promises são todas assíncronas, ou seja, o teste (síncrono) vai ser impresso antes de todas as promises terminarem!

Experimente colocar um número ímpar no array e verá que somente o catch do Promise.all será executado.

Esse é um recurso muito útil, principalmente nestes cenários de uso de foreach com promises, mas também em diversos outros de junção (join) de threads.

E por hoje é isso, espero que este artigo tenha te ajudado a entender como as promises funcionam!

O código completo deste artigo pode ser visto abaixo:

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu curso sobre programação web com Node.js!

Curso Node.js e MongoDB

Processamento assíncrono de tarefas com filas no RabbitMQ e Node.js

Um caso de uso de Node.js muito comum, como já expliquei em outros artigos antes, é o de construção de webapis e microsserviços que aguentam grande carga de requisições. Até aqui, isso não deve ser novidade para você.

No entanto, uma fraqueza do Node (e do JavaScript em geral) é a sua performance não tão boa quando o assunto é processamento pesado e/ou tarefas bloqueantes que demoram um tempo razoável para serem concluídas. Isso no Node.js pode ser um grande ofensor uma vez que o event loop trabalha com single thread, certo? Bloqueie esta thread principal e os demais clientes chamando sua API terão uma experiência bem ruim…

Seja nos casos em que o volume de requisições exceda a sua capacidade de resolvê-las rapidamente ou nos casos em que o processamento seja demorado, adotar uma arquitetura que opere de forma assíncrona não apenas garante que você vai conseguir atender todas requisições como vai fazê-lo em um tempo adequado.

No tutorial de hoje vou falar de como construir uma arquitetura simples, porém robusta, usando Node.js e RabbitMQ para processamento assíncrono de requisições recebidas em uma web API RESTful.

Processamento Assíncrono

O primeiro conceito que você tem de entender é que apesar do HTTP ser síncrono, já faz quase 20 anos que a Internet (e os sistemas que rodam nela) já entendeu que trabalhar de maneira assíncrona é uma forma de proporcionar experiências cada vez melhores aos usuários.

Em uma requisição síncrona, a request é enviada ao servidor, que a processa e devolve uma response. Tudo de uma vez só. Enquanto a response não é retornada, a conexão fica presa e o usuário fica esperando. Se demorar demais, a conexão pode ser encerrada abruptamente e o cliente terá de fazê-la de novo, sem saber exatamente o que acontecer nesta segunda chamada.

Em uma requisição assíncrona, a request é enviada ao servidor, que registra a mesma e automaticamente responde ao cliente que vai realizar a tarefa em breve, avisando-o de alguma maneira quando ela for concluída. Um outro processo cuida de processar essa requisição armazenada e notificar o cliente de alguma forma, se necessário.

A imagem abaixo ilustra um pouco dessa diferença.

Sync vs Async
Sync vs Async

Obviamente o segundo modelo é um pouco mais complexo de lidar, mas possui algumas vantagens muito interessantes.

Enquanto que processamento síncrono dá uma resposta mais direta e rápida para o cliente quando o servidor não está sobrecarregado de requisições, é quando a carga de chamadas às suas APIs é muito alta que ele se mostra inviável. Neste caso, apenas registrar as requisições, para processá-las em uma fila, por exemplo, garante que todos vão ser atendidos, cada um no seu tempo e nenhuma request vai ser dropada.

Assim, pensando nessa arquitetura, eu proponho neste tutorial o uso de RabbitMQ, uma tecnologia gratuita e open-source escrita em Erlang para ser usada como fila para as suas requisições, visando processamento assíncrono pelo Node.js.

RabbitMQ

O MQ no nome do Rabbit vem de Message Queue ou Fila de Mensagens, o que é exatamente o que ele é. O RabbitMQ é hoje a tecnologia de fila mais popular do mercado e fornece integração com todas as tecnologias comerciais mais utilizadas, incluindo aí Node.js. Ele é construído com a linguagem funcional Erlang e implementando o protocolo AMQP (Advanced Message Queue Protocol ou Protocolo Avançado de Filas de Mensagens).

Um dos possíveis casos de uso do RabbitMQ é a construção de arquiteturas para processamento assíncrono usando o padrão Producer/Consumer (Produtor/Consumidor), onde de um lado eu vou ter um produtor que envia mensagens pra uma fila (queue) e de outro lado um consumidor que é notificado que uma nova mensagem chegou, para processá-la. Existem arquiteturas mais complexas possíveis de serem feitas, com N para N, mas meu intuito aqui é manter simples neste primeiro momento.

Para efeitos mais práticos e simples, imagine que de um lado eu tenho uma API (producer) que recebe uma grande carga de requisições, ela enfileira essas requisições no Rabbit e um worker (consumer) vai processando essas mensagens uma a uma, conforme ele vai conseguindo dar conta.

A imagem abaixo ilustra uma possibilidade como essa, onde aquele canal no meio é onde fica o RabbitMQ.

Producer/Consumer
Producer/Consumer

Para rodar o RabbitMQ na sua máquina você vai precisar ter o Erlang instalado e depois pode baixar o Rabbit e executá-lo via linha de comando mesmo. A área de downloads dá as melhores instruções para instalação conforme o seu sistema operacional.

Com tudo instalado, eu costumo subir um servidor de filas do Rabbit usando o utilitário rabbitmq-server dentro da pasta sbin, como no comando abaixo (em Windows ajuste o path do cd e você não precisa do ./ no início do comando também)

Quando o servidor sobe corretamente você deve ver algo parecido com a imagem abaixo no seu terminal.

RabbitMQ funcionando
RabbitMQ funcionando

Cliente RabbitMQ em Node.js

O RabbitMQ possui clientes nas mais variadas tecnologias, incluindo Node.js como já mencionei antes. Uma vez com o servidor do RabbitMQ rodando, crie uma nova pasta e rode um npm init para iniciar um novo projeto Node.js nela.

Você tem de instalar as seguintes dependências no seu projeto:

  • express
  • body-parser
  • amqplib

Vamos criar um módulo JavaScript que vai encapsular a nossa lógica de produzir e consumir mensagens, bem como de criar a fila no Rabbit. Esse módulo depois será usado pela webapi e pelo worker (note que uso promises aqui).

Esse módulo é bem simples, possui 4 funções e expõe apenas duas, uma que vai ser usada pelo producer (sendToQueue) e outra pelo consumer (consume).

A primeira função, connect, é autoexplicativa, ela carrega a dependência do pacote amqplib e conecta-se na URL do servidor RabbitMQ, retornando o canal de comunicação. Essa função é usada pelas demais e é a recomendação, abrir sempre o canal de comunicação para garantir que não terá problemas com conexão.

A segunda função createQueue, é chamada pelas outras duas para garantir a existência, mas não se preocupe pois ela é idempotente, ou seja, depois de criada, ela não criará repetida e nem dará erro de que já existe. É mais uma recomendação de boa prática.

A terceira função, sendToQueue, será usada pelo producer (webapi) e na minha implementação ele está esperando um objeto JSON como argumento da função, sendo que ele serializa o mesmo para ser enviado ao Rabbit. Você pode ter várias filas diferentes, então esta função espera como argumento o nome da fila onde você vai adicionar essa mensagem.

E por fim, a função consume, que será usada pelo consumer (worker). Essa função espera um callback que é a função do cliente dessa lib que vai ser disparada toda vez que entrar uma mensagem nova na fila, é a função que vai processar a requisição agendada.

Agora vamos criar o produtor e o consumidor.

Produtor e Consumidor

Nossa API de exemplo será muito simples, uma vez que o objetivo aqui não é ensinar a criar webapis. Apenas copie e cole o código abaixo em um arquivo webapi.js na raiz do projeto:

Essa API espera um POST em um endpoint /task com um JSON no body da requisição. Ela apenas pega esse body e joga pra fila.

Da mesma forma, nosso worker de exemplo será muito simples, uma vez que não é o foco do artigo. Apenas copie e cole o código abaixo em um arquivo worker.js na raiz do projeto:

Esse worker é muito simples, ele apenas fica escutando a fila1 (a mesma em que o producer vai jogar as mensagens) e quando chega alguma coisa lá, o callback pega a mensagem e apenas imprime o conteúdo no console.

Testando e Além

Uma vez que você já tenha o servidor de RabbitMQ rodando, testar é muito simples, basta subir a webapi via terminal e depois subir o worker em outro terminal, em qualquer ordem.

Você deve começar o teste pelo producer, ou seja, abra o POSTMAN e envie um objeto JSON qualquer via POST para localhost:3000/task que isso deve disparar a mensagem pra fila que deve ser consumida pelo worker quase imediatamente.

Claro, este é um consumer meeega simples. Consumers reais vão processar dados da mensagem, fazer operações em banco e até mesmo chamar outras APIs se necessário, principalmente para avisar que essa requisição já foi processada. Você pode querer também alterar a interface para dar uma resposta ao usuário e por aí vai.

Você pode ainda explorar mais possibilidades desta arquitetura como o padrão com múltiplos consumers concorrentes (1xN) ou ainda o padrão Pub-Sub (Publish/Subscribe) onde podemos ter uma relação de NxN.

Mas enfim, a ideia deste artigo era te ajudar a fazer o Rabbit funcionar com Node.js e espero que você tenha conseguido. Caso contrário, apenas baixe os meus fontes usando o formulário ao final do tutorial.

Até a próxima!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu curso sobre programação web com Node.js!

Curso Node.js e MongoDB

Lidando com SOAP em Node.js

Se você começou a programar nos últimos 5 anos talvez nem saiba do que estou falando. Ou se sabe, é culpa de algum sistema legado com o qual você tem de lidar. Não te culpo por isso e “tamo junto”.

SOAP, ou Simple Object Access Protocol (não confundir com SOA), é um protocolo para criação de serviços web distribuídos e descentralizados, os famosos web services, que permitem a troca de informações entre sistemas, usando o formato XML. Eu não sei quando isso começou, mas a versão mais recente é de 2007 (1.2) e foi a forma mais popular de conectar sistemas na primeira década dos anos 2000, antes do surgimento do protocolo REST que incentivou as web APIs com JSON e mais tarde os micro serviços, formas mais leves para trocar informações entre sistemas pela web.

Todas as tecnologias web e mobile mais recentes oferecem suporte nativo (ou próximo disso) para lidar com Web APIs REST, mas pouco se fala de conexões com SOAP. O intuito deste tutorial é justamente ajudar quem precisa lidar com Node no client e SOAP no server.

#1 – Preparando o projeto

Este tutorial é intermediário e espera que você já saiba programar em Node.js. Caso ainda não possua esse conhecimento básico, conheça meus livros e meu curso.

Dito isso, crie um novo projeto na sua máquina com o nome de projeto-soap, crie um index.js vazio, rode um npm init para criar o package.json e instale a dependência do pacote Node SOAP que vamos utilizar, como abaixo:

Neste projeto, vamos criar um cliente que vai consumir um Web Service SOAP aberto e gratuito, disponível na Internet. Vou usar aqui, para fins de estudo o WS Dilbert, disponibilizado publicamente pela GComputer, cujo endereço é http://www.gcomputer.net/webservices/dilbert.asmx, mais especificamente o Web Method Daily Dilbert, que espera uma data como argumento e retorna uma frase como resposta, do famoso cartoon Dilbert.

Se você não está familiarizado com a documentação de Web Services SOAP, não é muito difícil de entender, desde que esteja minimamente acostumado com XML. Basicamente cada requisição, que pode ser enviada via HTTP (mais comum) ou RPC, é envelopada com seus argumentos e meta-informações em uma estrutura XML e retornada no mesmo formato, com seu valor de retorno e meta-informações. Por utilizar o formato XML, requests/responses SOAP geralmente são mais onerosos que seus equivalentes RESTful com JSON.

Estrutura SOAP
Estrutura SOAP

Para que clientes web consigam se comunicar com web services é necessário que eles entendam essa estrutura de comunicação XML, o que se faz através da leitura e interpretação do WSDL, ou Web Service Description Language, que fica disponível sempre no mesmo endereço do WS, apenas colocando o respectivo parâmetro no final: http://www.gcomputer.net/webservices/dilbert.asmx?wsdl

Dito isso e com a URL do WSDL em mãos, vamos iniciar a codificação do nosso cliente Node.

#2 – Criando o SOAP client

Fazer um SOAP client usando o pacote node-soap é muito fácil e nem dá para acreditar na trabalheira que é fazer sem um pacote profissional como esse.

Vamos começar criando um cliente através da conexão da biblioteca com o endereço do WSDL do nosso webservice, como abaixo:

Este código é bem simples e auto-explicativo, onde carregamos o pacote soap, a URL do WSDL e usamos a function createClient passando a url pra ela. Se tudo der certo, deve imprimir o conteúdo do objeto client no console.

O próximo passo, uma vez que temos um cliente SOAP instanciado e configurado para o WSDL de um web service em questão, é chamarmos algum de seus web methods. Ao acessar a página do web service encontramos dois deles, como mostra na imagem abaixo.

Web Service Dilbert
Web Service Dilbert

Temos dois WebMethods neste Web Service, o DailyDilbert e o TodaysDilbert (atenção à caixa alta e baixa, para chamar corretamente). Nós vamos usar o primeiro deles que, se clicarmos no mesmo (ou consultando diretamente o WSDL) vemos que ele espera apenas um parâmetro: ADate e retorna apenas um elemento: DailyDilbertResult, como mostrado na imagem abaixo.

WebMethod
WebMethod

Sendo assim, em nosso client SOAP, basta chamarmos esta function passando o argumento esperado e teremos o retorno desejado, como abaixo.

Coloque pra rodar e você vai ver que funciona. Ou quase. Infelizmente este webservice do Dilbert consome um site que não funciona mais e ele vai te retornar uma mensagem de erro dentro do DailyDilbertResult. 🙁

Eu demorei tanto tempo para terminar de escrever este tutorial que o site em questão mudou, hahaha. Mas não tem problema, se você receber uma mensagem de que não foi possível criar um canal seguro SSL, é porque você conseguiu se comunicar corretamente com o WebService, ele é que não conseguiu se comunicar com o site da comics.com onde ele pegava as frases do Dilbert.

Caso venha alguma coisa no objeto err, aí sim é um erro de verdade.

Eu cheguei a pensar em usar outro web service SOAP público, mas eles são raros hoje em dia e esse cumpre o papel de fazer a troca de mensagens corretamente, então fiquei com ele mesmo gerando uma única mensagem sempre. Acabei achando esse aqui depois que já tinha terminado de escrever o tutorial, então use ele como um exercício, pra ver se realmente aprendeu.

Então é isso por hoje. Tutorial curto e objetivo mas tenho certeza que vai ajudar quem estiver tendo que comunicar Node com SOAP. A título de curiosidade, não é incomum em apps mobile ter uma camada intermediária que faz esta tradução de SOAP pra REST para que o app possa trabalhar sempre apenas com REST.

Um abraço e sucesso!

Prof. Luiz

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu curso sobre programação web com Node.js!

Curso Node.js e MongoDB