Construindo uma Ferramenta de Validação de Recebedores de Pássaros em Lote Assíncronos

Zachary Samuels

26 de mai. de 2022

Email

1 min read

Construindo uma Ferramenta de Validação de Recebedores de Pássaros em Lote Assíncronos

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:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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:

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:

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:

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.

Outras notícias

Leia mais desta categoria

A person is standing at a desk while typing on a laptop.

A plataforma completa nativa de IA que escalará com o seu negócio.

© 2026 Pássaro

A person is standing at a desk while typing on a laptop.

A plataforma completa nativa de IA que escalará com o seu negócio.

© 2026 Pássaro