Construcción de una herramienta de validación de destinatarios de aves asíncrona masiva

Para alguien que busca un programa simple y rápido que acepte un csv, llame a la API de validación de receptores y produzca un CSV, este programa es para ti.

Author

Zacarías Samuels

Categoría

Correo electrónico

Construcción de una herramienta de validación de destinatarios de aves asíncrona masiva

Para alguien que busca un programa simple y rápido que acepte un csv, llame a la API de validación de receptores y produzca un CSV, este programa es para ti.

Author

Zacarías Samuels

Categoría

Correo electrónico

Construcción de una herramienta de validación de destinatarios de aves asíncrona masiva

Para alguien que busca un programa simple y rápido que acepte un csv, llame a la API de validación de receptores y produzca un CSV, este programa es para ti.

Author

Zacarías Samuels

Categoría

Correo electrónico

Una de las preguntas que ocasionalmente recibimos es, ¿cómo puedo validar listas de correos electrónicos en masa con validación de destinatarios? Aquí hay dos opciones, una es cargar un archivo a través de la interfaz de usuario de SparkPost para la validación, y la otra es hacer llamadas individuales por correo electrónico a la API (ya que la API es validación de correo electrónico única).


La primera opción funciona muy bien, pero tiene una limitación de 20Mb (alrededor de 500,000 direcciones). ¿Qué pasa si alguien tiene una lista de correos electrónicos que contiene millones de direcciones? Podría significar dividir eso en miles de cargas de archivos CSV.


Dado que cargar miles de archivos CSV parece un poco descabellado, tomé ese caso de uso y comencé a preguntarme cuán rápido podría hacer que la API funcionara. En esta publicación de blog, explicaré lo que intenté y cómo eventualmente llegué a un programa que podía realizar alrededor de 100,000 validaciones en 55 segundos (Mientras que en la interfaz de usuario obtuve alrededor de 100,000 validaciones en 1 minuto y 10 segundos). Y aunque esto aún tomaría alrededor de 100 horas para completarse con aproximadamente 654 millones de validaciones, este script puede ejecutarse en segundo plano ahorrando un tiempo significativo.


La versión final de este programa se puede encontrar aquí.


Mi primer error: usar Python

Python es uno de mis lenguajes de programación favoritos. Destaca en muchas áreas y es increíblemente sencillo. Sin embargo, una área en la que no destaca es en procesos concurrentes. Si bien Python tiene la capacidad de ejecutar funciones asincrónicas, tiene lo que se conoce como el Python Global Interpreter Lock o GIL.


“El Python Global Interpreter Lock o GIL, en palabras simples, es un mutex (o un bloqueo) que permite que solo un hilo mantenga el control del intérprete de Python.


Esto significa que solo un hilo puede estar en un estado de ejecución en un punto dado en el tiempo. El impacto del GIL no es visible para los desarrolladores que ejecutan programas de un solo hilo, pero puede ser un cuello de botella de rendimiento en código vinculado a CPU y de múltiples hilos.


Dado que el GIL permite que solo un hilo se ejecute a la vez, incluso en una arquitectura de múltiples hilos con más de un núcleo de CPU, el GIL ha ganado una reputación como una característica “infame” de Python.” (https://realpython.com/python-gil/)”


Al principio, no era consciente del GIL, así que comencé a programar en Python. Al final, a pesar de que mi programa era asincrónico, se quedaba bloqueado, y no importaba cuántos hilos añadiera, aún solo obtenía alrededor de 12-15 iteraciones por segundo.


La parte principal de la función asincrónica 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 diseño...


Decidí utilizar NodeJS debido a su capacidad para realizar operaciones de entrada/salida no bloqueantes extremadamente bien. También estoy bastante familiarizado con la programación en NodeJS.


Utilizando aspectos asincrónicos de NodeJS, esto terminó funcionando bien. Para más detalles sobre la programación asincrónica en NodeJS, consulte https://blog.risingstack.com/node-hero-async-programming-in-node-js/


Mi segundo error: intentar leer el archivo en memoria

Mi idea inicial fue la siguiente:



Primero, ingerir una lista CSV de correos electrónicos. Segundo, cargar los correos electrónicos en un arreglo y verificar que están en el formato correcto. Tercero, llamar asincrónicamente a la API de validación de destinatarios. Cuarto, esperar los resultados y cargarlos en una variable. Y finalmente, exportar esta variable a un archivo CSV.


Esto funcionó muy bien para archivos más pequeños. El problema surgió cuando intenté ejecutar 100,000 correos electrónicos. El programa se estancó alrededor de 12,000 validaciones. Con la ayuda de uno de nuestros desarrolladores front-end, vi que el problema estaba en cargar todos los resultados en una variable (y por lo tanto quedarse sin memoria rápidamente). Si deseas ver la primera iteración de este programa, lo he vinculado aquí: Versión 1 (NO RECOMENDADA).



Primero, ingerir una lista CSV de correos electrónicos. Segundo, contar el número de correos electrónicos en el archivo para fines de reportes. Tercero, a medida que se lee cada línea de manera asincrónica, llamar a la API de validación de destinatarios y exportar los resultados a un archivo CSV.


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


Desglosando el 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 en esta función, 1) me permite reportar 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. Agregué un temporizador para poder realizar pruebas de benchmark y asegurarme de que estoy obteniendo buenos resultados.


let count = 0; //Contador de líneas require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Lee el archivo de entrada y aumenta el conteo por cada línea .on("close", function () { //Al final del archivo de entrada, después de contar todas las líneas, ejecuta la función de validación de destinatarios validateRecipients.validateRecipients(count, myArgs); });

 

Luego llamo a la función validateRecipients. Nota que esta función es asincrónica. Después de validar que el archivo de entrada y el archivo de salida son CSV, escribo una fila de encabezado y comienzo un temporizador usando la biblioteca JSDOM.


async function validateRecipients(email_count, myArgs) { if ( //Si tanto el archivo de entrada como el archivo de salida están en formato .csv extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Contador para cada llamada a la API email_count++; //Contador de líneas devuelve #líneas - 1, esto se hace para corregir el número de líneas //Comienza un temporizador const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //Archivo de salida output.write( "Email,Valid,Resultado,Razón,Es_Rol,Es_Desechable,Es_Free,Confianza_de_Entrega\n" ); //Escribe los encabezados en el archivo de salida

 

El siguiente script es realmente la parte principal del programa, así que lo dividiré y explicaré lo que está sucediendo. Para cada línea del archivo 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, }, }) //Por cada fila leída del archivo de entrada, llama a la API de Validación de Destinatarios de SparkPost

 

Luego, en la respuesta

  • Agrega el correo electrónico al JSON (para poder imprimir el correo electrónico en el CSV)

  • Valida si la razón es nula, y si es así, pobla un valor vacío (esto es para que el formato CSV sea consistente, ya que en algunos casos se da una razón en la respuesta)

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

  • Convierte el JSON a CSV y exporta (utilizando json2csv)

  • Escribe el progreso en el terminal

  • Finalmente, si el número de correos electrónicos en el archivo = validaciones completadas, detiene el temporizador e imprime los resultados


.then(function (response) { response.data.results.email = String(email); //Agrega el correo electrónico como un par de valor/clave al JSON de respuesta para ser utilizado para la salida response.data.results.reason ? null : (response.data.results.reason = ""); //Si la razón es nula, configúralo en blanco para que el CSV sea uniforme //Utiliza json-2-csv para convertir el JSON al formato CSV y exportar let options = { prependHeader: false, //Desactiva que los valores JSON se agreguen como filas de encabezado para cada línea keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Establece el orden de las claves }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Aumenta el contador de la API process.stdout.write(`Hecho con ${completed} / ${email_count}\r`); //Salida del estado de Completado / Total en la consola sin mostrar nuevas líneas //Si todos los correos electrónicos han completado la validación if (completed == email_count) { const stop = window.performance.now(); //Detiene el temporizador console.log( `Todos los correos electrónicos validados exitosamente en ${ (stop - start) / 1000 } segundos` ); } })

 

Un último problema que encontré fue que mientras esto funcionaba muy bien en Mac, encontré el siguiente error usando Windows después de alrededor de 10,000 validaciones:


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


Después de hacer más investigaciones, parece ser un problema con el grupo de conexiones HTTP del cliente NodeJS que no reutiliza conexiones. Encontré este artículo de Stackoverflow sobre el problema, y tras investigar más, encontré una buena configuración predeterminada para la biblioteca axios que resolvió este problema. Aún no estoy seguro de por qué este problema solo ocurre en Windows y no en Mac.


Próximos pasos

Para alguien que busca un programa simple y rápido que tome un CSV, llame a la API de validación de destinatarios y exporte un CSV, este programa es para ti.


Algunas adiciones a este programa serían las siguientes:

  • Construir un front end o una interfaz de usuario más fácil de usar

  • Mejorar el manejo de errores y reintentos porque si por alguna razón la API lanza un error, el programa actualmente no reintenta la llamada


También tengo curiosidad por ver si se podrían lograr resultados más rápidos con otro lenguaje como Golang o Erlang/Elixir.


No dudes en proporcionarme cualquier retroalimentación o sugerencias para expandir este proyecto.

Sign up

La plataforma potenciada por IA para Marketing, Soporte y Finanzas

Al hacer clic en "Obtener una demostración" aceptas los términos de Bird's

Sign up

La plataforma potenciada por IA para Marketing, Soporte y Finanzas

Al hacer clic en "Obtener una demostración" aceptas los términos de Bird's

Sign up

La plataforma potenciada por IA para Marketing, Soporte y Finanzas

Al hacer clic en "Obtener una demostración" aceptas los términos de Bird's

Channels

Grow

Engage

Automate

APIs

Resources

Company

Socials

Crecer

Gestionar

Automatizar

Crecer

Gestionar

Automatizar