Creación de una herramienta de validación de destinatarios asíncrona masiva Bird

Creación de una herramienta de validación de destinatarios asíncrona masiva Bird

Creación de una herramienta de validación de destinatarios asíncrona masiva Bird

May 26, 2022

Publicado por

Publicado por

Zachary Samuels

Zachary Samuels

-

Categoría:

Categoría:

Email

Email

Ready to see Bird
in action?

Ready to see Bird
in action?

Building a Bulk Asynchronous Bird Recipient Validation Tool

One of the questions we occasionally receive is, how can I bulk validate email lists with validación del destinatario? There are two options here, one is to upload a file through the SparkPost UI for validation, and the other is to make individual calls per email a la API (as the API is single email validation).


La primera opción funciona muy bien, pero tiene una limitación de 20Mb (unas 500.000 direcciones). ¿Qué pasa si alguien tiene una lista de correo electrónico con millones de direcciones? Podría significar dividirla en 1.000 cargas de archivos CSV.


Since uploading thousands of CSV files seems a little far-fetched, I took that use case and began to wonder how fast I could get the API to run. In this blog post, I will explain what I tried and how I eventually came to a program that could get around 100.000 validaciones en 55 segundos (Whereas in the UI I got around 100,000 validations in 1 minute 10 seconds). And while this still would take about 100 hours to get done with about 654 million validations, this script can run in the background saving significant time.


La versión final de este programa puede encontrarse here.


Mi primer error: usar Python

Python es uno de mis lenguajes de programación favoritos. Sobresale en muchas áreas y es increíblemente sencillo. Sin embargo, un área en la que no destaca es en los procesos concurrentes. Aunque python tiene la capacidad de ejecutar funciones asíncronas, tiene lo que se conoce como el Python Global Interpreter Lock o GIL.


"El Bloqueo Global del Intérprete de Python o GIL, en palabras simples, es un mutex (o un bloqueo) que permite que sólo un hilo mantenga el control del intérprete de Python.


Esto significa que sólo un hilo puede estar en un estado de ejecución en cualquier momento. El impacto del GIL no es visible para los desarrolladores que ejecutan programas de un solo hilo, pero puede suponer un cuello de botella de rendimiento en código multihilo y con CPU.


Since the GIL allows only one thread to execute at a time even in a multi-threaded architecture with more than one CPU core, the GIL has gained a reputation as an “infamous” feature of Python.” (https://realpython.com/python-gil/)”


Al principio, no conocía el GIL, así que empecé a programar en python. Al final, aunque mi programa era asíncrono, se bloqueaba, y no importaba cuántos hilos añadiera, seguía obteniendo sólo unas 12-15 iteraciones por segundo.


La parte principal de la función asíncrona en Python se puede ver a continuación:

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)

 

Así que dejé de usar Python y volví a la mesa de dibujo...


Me decidí a utilizar NodeJS debido a su capacidad para realizar operaciones i/o sin bloqueo extremadamente bien. También estoy bastante familiarizado con la programación en NodeJS.


Utilizing asynchronous aspects of NodeJS, this ended up working well. For more details about asynchronous programming in NodeJS, see https://blog.risingstack.com/node-hero-async-programming-in-node-js/


Mi segundo error: intentar leer el archivo en memoria

Mi idea inicial era la siguiente:



Primero, ingiere una lista CSV de correos electrónicos. En segundo lugar, cargar los correos electrónicos en una matriz y comprobar que están en el formato correcto. Tercero, llamar de forma asíncrona a la API de validación de destinatarios. Cuarto, esperar los resultados y cargarlos en una variable. Por último, envía esta variable a un archivo CSV.


This worked very well for smaller files. En issue became when I tried to run 100,000 emails through. En program stalled at around 12,000 validations. With the help of one of our front-end developers, I saw that the issue was with loading all the results into a variable (and therefore running out of memory quickly). If you would like to see the first iteration of this program, I have linked it here: Versión 1 (NO RECOMENDADA).



En primer lugar, ingiera una lista CSV de correos electrónicos. En segundo lugar, cuente el número de correos electrónicos en el archivo con fines informativos. En tercer lugar, a medida que se lee cada línea de forma asíncrona, se llama a la API de validación de destinatarios y se envían los resultados a un archivo CSV.


Así, por cada línea leída, llamo a la API y escribo los resultados de forma asíncrona para no mantener ninguno de estos datos en la memoria a largo plazo. También he eliminado la comprobación de la sintaxis del correo electrónico después de hablar con el equipo de validación de destinatarios, ya que me informaron de que la validación de destinatarios ya tiene controles integrados para comprobar si un correo electrónico es válido o no.


Desglose del código final

Después de leer y validar los argumentos del terminal, ejecuto el siguiente código. Primero, leo el archivo CSV de correos electrónicos y cuento cada línea. Hay dos propósitos de esta función, 1) me permite informar con precisión sobre el progreso del archivo [como veremos más adelante], y 2) me permite detener un temporizador cuando el número de correos electrónicos en el archivo es igual a las validaciones completadas. He añadido un temporizador para que pueda ejecutar los puntos de referencia y asegurarse de que estoy obteniendo buenos 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); });

 

A continuación, llamo a la función validateRecipients. Nótese que esta función es asíncrona. Después de validar que el archivo de entrada y el archivo de salida son CSV, escribo una fila de encabezado, e inicio un temporizador de programa utilizando la 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 is done to correct 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

 

El siguiente script es realmente el grueso del programa, así que lo dividiré y explicaré lo que ocurre. Para cada línea del infile:


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

 

A continuación, en la respuesta

  • Añadir el email al JSON (para poder imprimir el email en el CSV)

  • Validar si el motivo es nulo y, en caso afirmativo, rellenar un valor vacío (para que el formato CSV sea coherente, ya que en algunos casos el motivo se indica en la respuesta).

  • Establece las opciones y claves para el módulo json2csv.

  • Convertir el JSON a CSV y salida (utilizando json2csv)

  • Escribir el progreso en el terminal

  • Por último, si el número de correos electrónicos en el archivo = validaciones completadas, detenga el temporizador e imprima los resultados


.then(function (response) { response.data.results.email = String(email); //Adds the email as a value/key pair a la response JSON to be used 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` ); } })

 

Un problema final que encontré fue que mientras que esto funcionó muy bien en Mac, me encontré con el siguiente error utilizando Windows después de alrededor de 10.000 validaciones:


Error: connect ENOBUFS XX.XX.XXX.XXX:443 - Local (undefined:undefined) with email XXXXXXX@XXXXXXXXXX.XXX


After doing some further research, it appears to be an issue with the NodeJS HTTP client connection pool not reusing connections. I found this Artículo de Stackoverflow on the issue, and after further digging, found a good configuración por defecto for the axios library that resolved this issue. I am still not certain why this issue only happens on Windows and not on Mac.


Próximos pasos

Para alguien que está buscando un simple programa rápido que toma en un csv, llama a la API de validación de destinatarios, y las salidas de un CSV, este programa es para usted.


Algunas adiciones a este programa serían las siguientes:

  • Construir una interfaz de usuario más fácil de usar

  • Mejor manejo de errores y reintentos, ya que si por alguna razón la API arroja un error, el programa actualmente no reintenta la llamada.


También tendría curiosidad por ver si se pueden conseguir resultados más rápidos con otro lenguaje como Golang o Erlang/Elixir.


Please feel free to provide me any comentarios o sugerencias for expanding this project.

Your new standard in Marketing, Pay & Sales. It's Bird

The right message -> to the right person -> en el right time.

By clicking "See Bird" you agree to Bird's Confidencialidad.

Your new standard in Marketing, Pay & Sales. It's Bird

The right message -> to the right person -> en el right time.

By clicking "See Bird" you agree to Bird's Confidencialidad.