Construindo uma Ferramenta de Validação de Recebedores de Pássaros em Lote Assíncronos
Zachary Samuels
26 de mai. de 2022
1 min read

Principais Conclusões
O autor construiu uma ferramenta de validação de destinatários em massa para validar milhões de endereços de e-mail de forma eficiente usando a API de Validação de Destinatários do Bird.
O Node.js provou ser mais rápido e escalável que o Python devido ao seu I/O não bloqueante e à ausência de limitações do GIL.
A ferramenta lê arquivos CSV de forma assíncrona, chama a API de validação para cada e-mail e grava os resultados em um novo CSV em tempo real.
A abordagem evita gargalos de memória e melhora a taxa de processamento para cerca de 100.000 validações em menos de um minuto.
Melhorias futuras podem incluir um melhor manuseio de tentativas, uma interface amigável ou migração para ambientes sem servidor para escalabilidade.
Destaques de Perguntas e Respostas
Qual é o propósito da Ferramenta de Validação de Destinatários Assíncrona em Massa?
Ele valida grandes volumes de endereços de email, integrando-se diretamente com a API de Validação de Destinatários do Bird, devolvendo resultados verificados rapidamente sem uploads manuais.
Por que o Python foi inicialmente usado e depois substituído pelo Node.js?
O Global Interpreter Lock (GIL) do Python limitou a concorrência, enquanto o Node.js permitia a execução assíncrona verdadeira, resultando em chamadas de API paralelas muito mais rápidas.
Como a ferramenta lida com arquivos grandes sem ficar sem memória?
Em vez de carregar todos os dados de uma vez, o script processa cada linha CSV individualmente - enviando a solicitação de validação e imediatamente escrevendo os resultados em um novo arquivo CSV.
Que problema a ferramenta resolve para os desenvolvedores?
Ele permite validação de listas de e-mails em grande escala, superando o limite de 20MB do validador baseado na interface do SparkPost e eliminando a necessidade de carregar múltiplos arquivos manualmente.
Quão rápido é a versão final do programa?
Cerca de 100.000 validações completas em 55 segundos, em comparação a mais de um minuto usando a versão da interface do usuário.
Quais problemas foram encontrados nos sistemas Windows?
A conexão de pooling do cliente HTTP Node.js causou erros "ENOBUFS" após muitas solicitações concorrentes, que foram corrigidos configurando a reutilização de conexões do axios.
Quais melhorias futuras são sugeridas?
Adicionando tratamento de erros e tentativas, criando uma interface de front-end ou implementando a ferramenta como uma Função do Azure sem servidor para melhor escalabilidade e resiliência.
Para alguém que está procurando um programa simples e rápido que aceite um CSV, chame a API de validação de destinatários e gere um CSV, este programa é para você.
Ao construir aplicações de email, os desenvolvedores frequentemente precisam integrar vários serviços e APIs. Entender os fundamentos da API de email na infraestrutura de nuvem fornece a base para construir ferramentas robustas como o sistema de validação em massa que criaremos neste guia.
Uma das perguntas que ocasionalmente recebemos é: como posso validar listas de emails em massa com validação de destinatários? Existem duas opções aqui: uma é fazer o upload de um arquivo através da interface do SparkPost para validação, e a outra é fazer chamadas individuais por email para a API (como a API é uma validação de email único).
A primeira opção funciona muito bem, mas tem uma limitação de 20Mb (cerca de 500.000 endereços). E se alguém tiver uma lista de emails contendo milhões de endereços? Isso poderia significar dividir em milhares de uploads de arquivos CSV.
Como fazer upload de milhares de arquivos CSV parece um pouco fora da realidade, peguei esse caso de uso e comecei a me perguntar quão rápido eu poderia fazer a API funcionar. Neste post do blog, explicarei o que eu tentei e como eventualmente cheguei a um programa que poderia realizar cerca de 100.000 validações em 55 segundos (Enquanto na interface obtive cerca de 100.000 validações em 1 minuto e 10 segundos).
Abordagem | Validações Testadas | Tempo para Completar | Aproximadamente Vazão |
|---|---|---|---|
Ferramenta Node.js assíncrona em massa | 100.000 | 55 segundos | ~1.818 validações/segundo |
Upload da interface do SparkPost | 100.000 | 1 min 10 seg | ~1.428 validações/segundo |
E enquanto isso ainda levaria cerca de 100 horas para ser feito com cerca de 654 milhões de validações, este script pode ser executado em segundo plano economizando tempo significativo.
A versão final deste programa pode ser encontrada aqui.
Ao construir aplicações de email, os desenvolvedores frequentemente precisam integrar vários serviços e APIs. Entender os fundamentos da API de email na infraestrutura de nuvem fornece a base para construir ferramentas robustas como o sistema de validação em massa que criaremos neste guia.
Uma das perguntas que ocasionalmente recebemos é: como posso validar listas de emails em massa com validação de destinatários? Existem duas opções aqui: uma é fazer o upload de um arquivo através da interface do SparkPost para validação, e a outra é fazer chamadas individuais por email para a API (como a API é uma validação de email único).
A primeira opção funciona muito bem, mas tem uma limitação de 20Mb (cerca de 500.000 endereços). E se alguém tiver uma lista de emails contendo milhões de endereços? Isso poderia significar dividir em milhares de uploads de arquivos CSV.
Como fazer upload de milhares de arquivos CSV parece um pouco fora da realidade, peguei esse caso de uso e comecei a me perguntar quão rápido eu poderia fazer a API funcionar. Neste post do blog, explicarei o que eu tentei e como eventualmente cheguei a um programa que poderia realizar cerca de 100.000 validações em 55 segundos (Enquanto na interface obtive cerca de 100.000 validações em 1 minuto e 10 segundos).
Abordagem | Validações Testadas | Tempo para Completar | Aproximadamente Vazão |
|---|---|---|---|
Ferramenta Node.js assíncrona em massa | 100.000 | 55 segundos | ~1.818 validações/segundo |
Upload da interface do SparkPost | 100.000 | 1 min 10 seg | ~1.428 validações/segundo |
E enquanto isso ainda levaria cerca de 100 horas para ser feito com cerca de 654 milhões de validações, este script pode ser executado em segundo plano economizando tempo significativo.
A versão final deste programa pode ser encontrada aqui.
Ao construir aplicações de email, os desenvolvedores frequentemente precisam integrar vários serviços e APIs. Entender os fundamentos da API de email na infraestrutura de nuvem fornece a base para construir ferramentas robustas como o sistema de validação em massa que criaremos neste guia.
Uma das perguntas que ocasionalmente recebemos é: como posso validar listas de emails em massa com validação de destinatários? Existem duas opções aqui: uma é fazer o upload de um arquivo através da interface do SparkPost para validação, e a outra é fazer chamadas individuais por email para a API (como a API é uma validação de email único).
A primeira opção funciona muito bem, mas tem uma limitação de 20Mb (cerca de 500.000 endereços). E se alguém tiver uma lista de emails contendo milhões de endereços? Isso poderia significar dividir em milhares de uploads de arquivos CSV.
Como fazer upload de milhares de arquivos CSV parece um pouco fora da realidade, peguei esse caso de uso e comecei a me perguntar quão rápido eu poderia fazer a API funcionar. Neste post do blog, explicarei o que eu tentei e como eventualmente cheguei a um programa que poderia realizar cerca de 100.000 validações em 55 segundos (Enquanto na interface obtive cerca de 100.000 validações em 1 minuto e 10 segundos).
Abordagem | Validações Testadas | Tempo para Completar | Aproximadamente Vazão |
|---|---|---|---|
Ferramenta Node.js assíncrona em massa | 100.000 | 55 segundos | ~1.818 validações/segundo |
Upload da interface do SparkPost | 100.000 | 1 min 10 seg | ~1.428 validações/segundo |
E enquanto isso ainda levaria cerca de 100 horas para ser feito com cerca de 654 milhões de validações, este script pode ser executado em segundo plano economizando tempo significativo.
A versão final deste programa pode ser encontrada aqui.
Meu primeiro erro: usar Python
Python é uma das minhas linguagens de programação favoritas. Ela se destaca em muitas áreas e é incrivelmente simples. No entanto, uma área em que ela não se destaca é em processos concorrentes. Embora o Python tenha a capacidade de executar funções assíncronas, ele possui o que é conhecido como o Bloqueio Global do Interpretador Python ou GIL.
“O Bloqueio Global do Interpretador Python ou GIL, em palavras simples, é um mutex (ou um bloqueio) que permite que apenas uma thread mantenha o controle do interpretador Python.
Isso significa que apenas uma thread pode estar em um estado de execução em qualquer momento. O impacto do GIL não é visível para desenvolvedores que executam programas de uma única thread, mas pode ser um gargalo de desempenho em código que depende fortemente da CPU e em código multi-threaded.
Uma vez que o Bloqueio do Interpretador Global (GIL) permite que apenas uma thread execute de cada vez, mesmo em sistemas de múltiplos núcleos, ele ganhou a reputação de ser um recurso “infame” do Python (veja o artigo da Real Python sobre o GIL).
No início, eu não estava ciente do GIL, então comecei a programar em Python. No final, mesmo que meu programa fosse assíncrono, ele estava travando, e não importava quantas threads eu adicionasse, eu ainda obtinha apenas cerca de 12-15 iterações por segundo.
A parte principal da função assíncrona em Python pode ser vista abaixo:
async def validateRecipients(f, fh, apiKey, snooze, count): h = { 'Authorization': apiKey, 'Accept': 'application/json' } with tqdm(total=count) as pbar: async with aiohttp.ClientSession() as session: for address in f: for i in address: thisReq = requests.compat.urljoin(url, i) async with session.get(thisReq, headers=h, ssl=False) as resp: content = await resp.json() row = content['results'] row['email'] = i fh.writerow(row) pbar.update(1)
Então, eu desisti de usar Python e voltei à prancheta...
Decidi utilizar o NodeJS devido à sua capacidade de realizar operações de I/O não bloqueantes de maneira extremamente eficiente. Outra excelente opção para lidar com processamento de API assíncrono é construir consumidores de webhook sem servidor com Azure Functions, que podem lidar eficientemente com cargas de trabalho variáveis. Eu também estou bastante familiarizado com programação em NodeJS.
Utilizando aspectos assíncronos do Node.js, essa abordagem funcionou bem. Para mais detalhes sobre programação assíncrona em Node.js, veja o guia da RisingStack sobre programação assíncrona em Node.js.
Python é uma das minhas linguagens de programação favoritas. Ela se destaca em muitas áreas e é incrivelmente simples. No entanto, uma área em que ela não se destaca é em processos concorrentes. Embora o Python tenha a capacidade de executar funções assíncronas, ele possui o que é conhecido como o Bloqueio Global do Interpretador Python ou GIL.
“O Bloqueio Global do Interpretador Python ou GIL, em palavras simples, é um mutex (ou um bloqueio) que permite que apenas uma thread mantenha o controle do interpretador Python.
Isso significa que apenas uma thread pode estar em um estado de execução em qualquer momento. O impacto do GIL não é visível para desenvolvedores que executam programas de uma única thread, mas pode ser um gargalo de desempenho em código que depende fortemente da CPU e em código multi-threaded.
Uma vez que o Bloqueio do Interpretador Global (GIL) permite que apenas uma thread execute de cada vez, mesmo em sistemas de múltiplos núcleos, ele ganhou a reputação de ser um recurso “infame” do Python (veja o artigo da Real Python sobre o GIL).
No início, eu não estava ciente do GIL, então comecei a programar em Python. No final, mesmo que meu programa fosse assíncrono, ele estava travando, e não importava quantas threads eu adicionasse, eu ainda obtinha apenas cerca de 12-15 iterações por segundo.
A parte principal da função assíncrona em Python pode ser vista abaixo:
async def validateRecipients(f, fh, apiKey, snooze, count): h = { 'Authorization': apiKey, 'Accept': 'application/json' } with tqdm(total=count) as pbar: async with aiohttp.ClientSession() as session: for address in f: for i in address: thisReq = requests.compat.urljoin(url, i) async with session.get(thisReq, headers=h, ssl=False) as resp: content = await resp.json() row = content['results'] row['email'] = i fh.writerow(row) pbar.update(1)
Então, eu desisti de usar Python e voltei à prancheta...
Decidi utilizar o NodeJS devido à sua capacidade de realizar operações de I/O não bloqueantes de maneira extremamente eficiente. Outra excelente opção para lidar com processamento de API assíncrono é construir consumidores de webhook sem servidor com Azure Functions, que podem lidar eficientemente com cargas de trabalho variáveis. Eu também estou bastante familiarizado com programação em NodeJS.
Utilizando aspectos assíncronos do Node.js, essa abordagem funcionou bem. Para mais detalhes sobre programação assíncrona em Node.js, veja o guia da RisingStack sobre programação assíncrona em Node.js.
Python é uma das minhas linguagens de programação favoritas. Ela se destaca em muitas áreas e é incrivelmente simples. No entanto, uma área em que ela não se destaca é em processos concorrentes. Embora o Python tenha a capacidade de executar funções assíncronas, ele possui o que é conhecido como o Bloqueio Global do Interpretador Python ou GIL.
“O Bloqueio Global do Interpretador Python ou GIL, em palavras simples, é um mutex (ou um bloqueio) que permite que apenas uma thread mantenha o controle do interpretador Python.
Isso significa que apenas uma thread pode estar em um estado de execução em qualquer momento. O impacto do GIL não é visível para desenvolvedores que executam programas de uma única thread, mas pode ser um gargalo de desempenho em código que depende fortemente da CPU e em código multi-threaded.
Uma vez que o Bloqueio do Interpretador Global (GIL) permite que apenas uma thread execute de cada vez, mesmo em sistemas de múltiplos núcleos, ele ganhou a reputação de ser um recurso “infame” do Python (veja o artigo da Real Python sobre o GIL).
No início, eu não estava ciente do GIL, então comecei a programar em Python. No final, mesmo que meu programa fosse assíncrono, ele estava travando, e não importava quantas threads eu adicionasse, eu ainda obtinha apenas cerca de 12-15 iterações por segundo.
A parte principal da função assíncrona em Python pode ser vista abaixo:
async def validateRecipients(f, fh, apiKey, snooze, count): h = { 'Authorization': apiKey, 'Accept': 'application/json' } with tqdm(total=count) as pbar: async with aiohttp.ClientSession() as session: for address in f: for i in address: thisReq = requests.compat.urljoin(url, i) async with session.get(thisReq, headers=h, ssl=False) as resp: content = await resp.json() row = content['results'] row['email'] = i fh.writerow(row) pbar.update(1)
Então, eu desisti de usar Python e voltei à prancheta...
Decidi utilizar o NodeJS devido à sua capacidade de realizar operações de I/O não bloqueantes de maneira extremamente eficiente. Outra excelente opção para lidar com processamento de API assíncrono é construir consumidores de webhook sem servidor com Azure Functions, que podem lidar eficientemente com cargas de trabalho variáveis. Eu também estou bastante familiarizado com programação em NodeJS.
Utilizando aspectos assíncronos do Node.js, essa abordagem funcionou bem. Para mais detalhes sobre programação assíncrona em Node.js, veja o guia da RisingStack sobre programação assíncrona em Node.js.
Meu segundo erro: tentar ler o arquivo na memória
Minha ideia inicial era a seguinte:

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, carregue os e-mails em um array e verifique se estão no formato correto. Terceiro, faça uma chamada assíncrona à API de validação do destinatário. Quarto, aguarde os resultados e os carregue em uma variável. E finalmente, coloque essa variável em um arquivo CSV.
Isso funcionou muito bem para arquivos menores. O problema surgiu quando tentei processar 100.000 e-mails. O programa travou em cerca de 12.000 validações. Com a ajuda de um dos nossos desenvolvedores front-end, percebi que o problema estava em carregar todos os resultados em uma variável (e, portanto, acabar rapidamente com a memória). Se você quiser ver a primeira iteração deste programa, eu a vinculei aqui: Versão 1 (NÃO RECOMENDADA).

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, conte o número de e-mails no arquivo para fins de relatório. Terceiro, conforme cada linha é lida de forma assíncrona, chame a API de validação do destinatário e escreva os resultados em um arquivo CSV.
Assim, para cada linha lida, chamo a API e escrevo os resultados de forma assíncrona para não manter nenhum desses dados na memória a longo prazo. Também removi a verificação de sintaxe de e-mail após conversar com a equipe de validação do destinatário, pois eles me informaram que a validação do destinatário já possui verificações internas para verificar se um e-mail é válido ou não.
Minha ideia inicial era a seguinte:

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, carregue os e-mails em um array e verifique se estão no formato correto. Terceiro, faça uma chamada assíncrona à API de validação do destinatário. Quarto, aguarde os resultados e os carregue em uma variável. E finalmente, coloque essa variável em um arquivo CSV.
Isso funcionou muito bem para arquivos menores. O problema surgiu quando tentei processar 100.000 e-mails. O programa travou em cerca de 12.000 validações. Com a ajuda de um dos nossos desenvolvedores front-end, percebi que o problema estava em carregar todos os resultados em uma variável (e, portanto, acabar rapidamente com a memória). Se você quiser ver a primeira iteração deste programa, eu a vinculei aqui: Versão 1 (NÃO RECOMENDADA).

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, conte o número de e-mails no arquivo para fins de relatório. Terceiro, conforme cada linha é lida de forma assíncrona, chame a API de validação do destinatário e escreva os resultados em um arquivo CSV.
Assim, para cada linha lida, chamo a API e escrevo os resultados de forma assíncrona para não manter nenhum desses dados na memória a longo prazo. Também removi a verificação de sintaxe de e-mail após conversar com a equipe de validação do destinatário, pois eles me informaram que a validação do destinatário já possui verificações internas para verificar se um e-mail é válido ou não.
Minha ideia inicial era a seguinte:

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, carregue os e-mails em um array e verifique se estão no formato correto. Terceiro, faça uma chamada assíncrona à API de validação do destinatário. Quarto, aguarde os resultados e os carregue em uma variável. E finalmente, coloque essa variável em um arquivo CSV.
Isso funcionou muito bem para arquivos menores. O problema surgiu quando tentei processar 100.000 e-mails. O programa travou em cerca de 12.000 validações. Com a ajuda de um dos nossos desenvolvedores front-end, percebi que o problema estava em carregar todos os resultados em uma variável (e, portanto, acabar rapidamente com a memória). Se você quiser ver a primeira iteração deste programa, eu a vinculei aqui: Versão 1 (NÃO RECOMENDADA).

Primeiro, faça a ingestão de uma lista de e-mails em CSV. Em segundo lugar, conte o número de e-mails no arquivo para fins de relatório. Terceiro, conforme cada linha é lida de forma assíncrona, chame a API de validação do destinatário e escreva os resultados em um arquivo CSV.
Assim, para cada linha lida, chamo a API e escrevo os resultados de forma assíncrona para não manter nenhum desses dados na memória a longo prazo. Também removi a verificação de sintaxe de e-mail após conversar com a equipe de validação do destinatário, pois eles me informaram que a validação do destinatário já possui verificações internas para verificar se um e-mail é válido ou não.
Desmembrando o código final
Após ler e validar os argumentos do terminal, executo o seguinte código. Primeiro, leio o arquivo CSV de e-mails e conto cada linha. Há duas finalidades nesta função: 1) permite que eu relate com precisão o progresso do arquivo [como veremos mais adiante], e 2) permite que eu pare um cronômetro quando o número de e-mails no arquivo igualar as validações concluídas. Adicionei um cronômetro para que eu possa executar benchmarks e garantir que estou obtendo bons resultados.
let count = 0; // Line count require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) // Reads the infile and increases the count for each line .on("close", function () { // At the end of the infile, after all lines have been counted, run the recipient validation function validateRecipients.validateRecipients(count, myArgs); });
Em seguida, chamo a função validateRecipients. Observe que essa função é assíncrona. Após validar que o arquivo de entrada e o arquivo de saída são CSV, escrevo uma linha de cabeçalho e inicio um cronômetro de programa usando a biblioteca JSDOM.
async function validateRecipients(email_count, myArgs) { if ( // If both the infile and outfile are in .csv format extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; // Counter for each API call email_count++; // Line counter returns #lines - 1, this corrects the number of lines // Start a timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); // Outfile output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); // Write the headers in the outfile } }
O seguinte script é realmente a parte principal do programa, então vou dividi-lo e explicar o que está acontecendo. Para cada linha do arquivo de entrada:
Assincronamente, pega aquela linha e chama a API de validação de destinatários.
fs.createReadStream(myArgs[1]) .pipe(csv.parse({ headers: false })) .on("data", async (email) => { let url = SPARKPOST_HOST + "/api/v1/recipient-validation/single/" + email; await axios .get(url, { headers: { Authorization: SPARKPOST_API_KEY, }, }); // For each row read in from the infile, call the SparkPost Recipient Validation API });
Então, na resposta
Adicione o e-mail ao JSON (para poder imprimir o e-mail no CSV)
Valide se a razão é nula e, se sim, preencha um valor em branco (isso é para que o formato CSV seja consistente, já que em alguns casos a razão é dada na resposta)
Defina as opções e chaves para o módulo json2csv.
Converta o JSON para CSV e output (utilizando json2csv)
Escreva o progresso no terminal
Finalmente, se o número de e-mails no arquivo = validações concluídas, pare o cronômetro e imprima os resultados
.then(function (response) { response.data.results.email = String(email); // Adds the email as a value/key pair to the response JSON for output response.data.results.reason ? null : (response.data.results.reason = ""); // If reason is null, set it to blank so the CSV is uniform // Utilizes json-2-csv to convert the JSON to CSV format and output let options = { prependHeader: false, // Disables JSON values from being added as header rows for every line keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], // Sets the order of keys }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; // Increase the API counter process.stdout.write(`Done with ${completed} / ${email_count}\r`); // Output status of Completed / Total to the console without showing new lines // If all emails have completed validation if (completed == email_count) { const stop = window.performance.now(); // Stop the timer console.log( `All emails successfully validated in ${(stop - start) / 1000} seconds` ); } });
Um último problema que encontrei foi que, enquanto isso funcionou muito bem no Mac, eu me deparei com o seguinte erro usando o Windows após cerca de 10.000 validações:
Erro: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) com e-mail XXXXXXX@XXXXXXXXXX.XXX
Após fazer mais pesquisas, parece ser um problema com o pool de conexões do cliente HTTP do NodeJS não reutilizando conexões. Encontrei este artigo do Stackoverflow sobre o problema e, após mais investigações, encontrei uma boa configuração padrão para a biblioteca axios que resolveu esse problema. Ainda não tenho certeza por que esse problema ocorre apenas no Windows e não no Mac.
Após ler e validar os argumentos do terminal, executo o seguinte código. Primeiro, leio o arquivo CSV de e-mails e conto cada linha. Há duas finalidades nesta função: 1) permite que eu relate com precisão o progresso do arquivo [como veremos mais adiante], e 2) permite que eu pare um cronômetro quando o número de e-mails no arquivo igualar as validações concluídas. Adicionei um cronômetro para que eu possa executar benchmarks e garantir que estou obtendo bons resultados.
let count = 0; // Line count require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) // Reads the infile and increases the count for each line .on("close", function () { // At the end of the infile, after all lines have been counted, run the recipient validation function validateRecipients.validateRecipients(count, myArgs); });
Em seguida, chamo a função validateRecipients. Observe que essa função é assíncrona. Após validar que o arquivo de entrada e o arquivo de saída são CSV, escrevo uma linha de cabeçalho e inicio um cronômetro de programa usando a biblioteca JSDOM.
async function validateRecipients(email_count, myArgs) { if ( // If both the infile and outfile are in .csv format extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; // Counter for each API call email_count++; // Line counter returns #lines - 1, this corrects the number of lines // Start a timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); // Outfile output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); // Write the headers in the outfile } }
O seguinte script é realmente a parte principal do programa, então vou dividi-lo e explicar o que está acontecendo. Para cada linha do arquivo de entrada:
Assincronamente, pega aquela linha e chama a API de validação de destinatários.
fs.createReadStream(myArgs[1]) .pipe(csv.parse({ headers: false })) .on("data", async (email) => { let url = SPARKPOST_HOST + "/api/v1/recipient-validation/single/" + email; await axios .get(url, { headers: { Authorization: SPARKPOST_API_KEY, }, }); // For each row read in from the infile, call the SparkPost Recipient Validation API });
Então, na resposta
Adicione o e-mail ao JSON (para poder imprimir o e-mail no CSV)
Valide se a razão é nula e, se sim, preencha um valor em branco (isso é para que o formato CSV seja consistente, já que em alguns casos a razão é dada na resposta)
Defina as opções e chaves para o módulo json2csv.
Converta o JSON para CSV e output (utilizando json2csv)
Escreva o progresso no terminal
Finalmente, se o número de e-mails no arquivo = validações concluídas, pare o cronômetro e imprima os resultados
.then(function (response) { response.data.results.email = String(email); // Adds the email as a value/key pair to the response JSON for output response.data.results.reason ? null : (response.data.results.reason = ""); // If reason is null, set it to blank so the CSV is uniform // Utilizes json-2-csv to convert the JSON to CSV format and output let options = { prependHeader: false, // Disables JSON values from being added as header rows for every line keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], // Sets the order of keys }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; // Increase the API counter process.stdout.write(`Done with ${completed} / ${email_count}\r`); // Output status of Completed / Total to the console without showing new lines // If all emails have completed validation if (completed == email_count) { const stop = window.performance.now(); // Stop the timer console.log( `All emails successfully validated in ${(stop - start) / 1000} seconds` ); } });
Um último problema que encontrei foi que, enquanto isso funcionou muito bem no Mac, eu me deparei com o seguinte erro usando o Windows após cerca de 10.000 validações:
Erro: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) com e-mail XXXXXXX@XXXXXXXXXX.XXX
Após fazer mais pesquisas, parece ser um problema com o pool de conexões do cliente HTTP do NodeJS não reutilizando conexões. Encontrei este artigo do Stackoverflow sobre o problema e, após mais investigações, encontrei uma boa configuração padrão para a biblioteca axios que resolveu esse problema. Ainda não tenho certeza por que esse problema ocorre apenas no Windows e não no Mac.
Após ler e validar os argumentos do terminal, executo o seguinte código. Primeiro, leio o arquivo CSV de e-mails e conto cada linha. Há duas finalidades nesta função: 1) permite que eu relate com precisão o progresso do arquivo [como veremos mais adiante], e 2) permite que eu pare um cronômetro quando o número de e-mails no arquivo igualar as validações concluídas. Adicionei um cronômetro para que eu possa executar benchmarks e garantir que estou obtendo bons resultados.
let count = 0; // Line count require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) // Reads the infile and increases the count for each line .on("close", function () { // At the end of the infile, after all lines have been counted, run the recipient validation function validateRecipients.validateRecipients(count, myArgs); });
Em seguida, chamo a função validateRecipients. Observe que essa função é assíncrona. Após validar que o arquivo de entrada e o arquivo de saída são CSV, escrevo uma linha de cabeçalho e inicio um cronômetro de programa usando a biblioteca JSDOM.
async function validateRecipients(email_count, myArgs) { if ( // If both the infile and outfile are in .csv format extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; // Counter for each API call email_count++; // Line counter returns #lines - 1, this corrects the number of lines // Start a timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); // Outfile output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); // Write the headers in the outfile } }
O seguinte script é realmente a parte principal do programa, então vou dividi-lo e explicar o que está acontecendo. Para cada linha do arquivo de entrada:
Assincronamente, pega aquela linha e chama a API de validação de destinatários.
fs.createReadStream(myArgs[1]) .pipe(csv.parse({ headers: false })) .on("data", async (email) => { let url = SPARKPOST_HOST + "/api/v1/recipient-validation/single/" + email; await axios .get(url, { headers: { Authorization: SPARKPOST_API_KEY, }, }); // For each row read in from the infile, call the SparkPost Recipient Validation API });
Então, na resposta
Adicione o e-mail ao JSON (para poder imprimir o e-mail no CSV)
Valide se a razão é nula e, se sim, preencha um valor em branco (isso é para que o formato CSV seja consistente, já que em alguns casos a razão é dada na resposta)
Defina as opções e chaves para o módulo json2csv.
Converta o JSON para CSV e output (utilizando json2csv)
Escreva o progresso no terminal
Finalmente, se o número de e-mails no arquivo = validações concluídas, pare o cronômetro e imprima os resultados
.then(function (response) { response.data.results.email = String(email); // Adds the email as a value/key pair to the response JSON for output response.data.results.reason ? null : (response.data.results.reason = ""); // If reason is null, set it to blank so the CSV is uniform // Utilizes json-2-csv to convert the JSON to CSV format and output let options = { prependHeader: false, // Disables JSON values from being added as header rows for every line keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], // Sets the order of keys }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; // Increase the API counter process.stdout.write(`Done with ${completed} / ${email_count}\r`); // Output status of Completed / Total to the console without showing new lines // If all emails have completed validation if (completed == email_count) { const stop = window.performance.now(); // Stop the timer console.log( `All emails successfully validated in ${(stop - start) / 1000} seconds` ); } });
Um último problema que encontrei foi que, enquanto isso funcionou muito bem no Mac, eu me deparei com o seguinte erro usando o Windows após cerca de 10.000 validações:
Erro: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) com e-mail XXXXXXX@XXXXXXXXXX.XXX
Após fazer mais pesquisas, parece ser um problema com o pool de conexões do cliente HTTP do NodeJS não reutilizando conexões. Encontrei este artigo do Stackoverflow sobre o problema e, após mais investigações, encontrei uma boa configuração padrão para a biblioteca axios que resolveu esse problema. Ainda não tenho certeza por que esse problema ocorre apenas no Windows e não no Mac.
Próximos Passos
Para alguém que está procurando um programa simples e rápido que aceite um csv, chame a API de validação do destinatário e saia com um CSV, este programa é para você.
Algumas adições a este programa seriam as seguintes:
Construir um front-end ou uma interface mais fácil de usar
Melhorar o tratamento de erros e tentativas, pois se por algum motivo a API gerar um erro, o programa atualmente não tenta novamente a chamada
Considerar implementar como uma Função sem servidor do Azure para escalonamento automático e redução da gestão de infraestrutura
Eu também teria curiosidade em ver se resultados mais rápidos poderiam ser alcançados com outra linguagem como Golang ou Erlang/Elixir. Além da escolha da linguagem, limitações de infraestrutura também podem impactar o desempenho - aprendemos isso na prática quando enfrentamos limites de DNS não documentados na AWS que afetaram nossos sistemas de processamento de e-mails de alto volume.
Para desenvolvedores interessados em combinar o processamento de API com ferramentas de fluxo de trabalho visuais, veja como integrar o Flow Builder com as Google Cloud Functions para fluxos de trabalho de automação sem código.
Fique à vontade para me enviar qualquer comentário ou sugestão para expandir este projeto.
Para alguém que está procurando um programa simples e rápido que aceite um csv, chame a API de validação do destinatário e saia com um CSV, este programa é para você.
Algumas adições a este programa seriam as seguintes:
Construir um front-end ou uma interface mais fácil de usar
Melhorar o tratamento de erros e tentativas, pois se por algum motivo a API gerar um erro, o programa atualmente não tenta novamente a chamada
Considerar implementar como uma Função sem servidor do Azure para escalonamento automático e redução da gestão de infraestrutura
Eu também teria curiosidade em ver se resultados mais rápidos poderiam ser alcançados com outra linguagem como Golang ou Erlang/Elixir. Além da escolha da linguagem, limitações de infraestrutura também podem impactar o desempenho - aprendemos isso na prática quando enfrentamos limites de DNS não documentados na AWS que afetaram nossos sistemas de processamento de e-mails de alto volume.
Para desenvolvedores interessados em combinar o processamento de API com ferramentas de fluxo de trabalho visuais, veja como integrar o Flow Builder com as Google Cloud Functions para fluxos de trabalho de automação sem código.
Fique à vontade para me enviar qualquer comentário ou sugestão para expandir este projeto.
Para alguém que está procurando um programa simples e rápido que aceite um csv, chame a API de validação do destinatário e saia com um CSV, este programa é para você.
Algumas adições a este programa seriam as seguintes:
Construir um front-end ou uma interface mais fácil de usar
Melhorar o tratamento de erros e tentativas, pois se por algum motivo a API gerar um erro, o programa atualmente não tenta novamente a chamada
Considerar implementar como uma Função sem servidor do Azure para escalonamento automático e redução da gestão de infraestrutura
Eu também teria curiosidade em ver se resultados mais rápidos poderiam ser alcançados com outra linguagem como Golang ou Erlang/Elixir. Além da escolha da linguagem, limitações de infraestrutura também podem impactar o desempenho - aprendemos isso na prática quando enfrentamos limites de DNS não documentados na AWS que afetaram nossos sistemas de processamento de e-mails de alto volume.
Para desenvolvedores interessados em combinar o processamento de API com ferramentas de fluxo de trabalho visuais, veja como integrar o Flow Builder com as Google Cloud Functions para fluxos de trabalho de automação sem código.
Fique à vontade para me enviar qualquer comentário ou sugestão para expandir este projeto.



