Reach

Grow

Manage

Automate

Reach

Grow

Manage

Automate

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

Correo electrónico

1 min read

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

Correo electrónico

1 min read

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.

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

La primera opción funciona muy bien pero tiene una limitación de 20Mb (aproximadamente 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 improbable, tomé ese caso de uso y comencé a preguntarme qué tan rápido podría hacer que la API funcionara. En esta publicación de blog, explicaré lo que intenté y cómo finalmente llegué a un programa que podrí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 todavía tomaría alrededor de 100 horas completar aproximadamente 654 millones de validaciones, este script puede ejecutarse en segundo plano ahorrando 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. Sobresale en muchas áreas y es increíblemente sencillo. Sin embargo, un área en la que no sobresale es en los procesos concurrentes. Aunque Python tiene la capacidad de ejecutar funciones asincrónicas, tiene lo que se conoce como The Python Global Interpreter Lock o GIL.

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

Esto significa que solo un hilo puede estar en un estado de ejecución en cualquier momento determinado. 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 dependiente de la CPU y multihilo.

Ya que el GIL permite que sólo un hilo se ejecute a la vez incluso en una arquitectura multihilo 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 estaba al tanto del GIL, así que comencé a programar en Python. Al terminar, aunque mi programa era asincrónico, se estaba bloqueando, y no importaba cuántos hilos agregara, todavía solo obtenía alrededor de 12-15 iteraciones por segundo.

La porción 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 dibujo…

Me decidí por 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, ve 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:

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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, cargar los correos electrónicos en un array y verificar que están en el formato correcto. Tercero, llamar asincrónicamente la API de validación de destinatarios. Cuarto, esperar a 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 detuvo alrededor de las 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, la he enlazado aquí: Version 1 (NOT RECOMMENDED).


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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, contar la cantidad de correos electrónicos en el archivo para propósitos de informes. Tercero, a medida que se lee cada línea de forma asincrónica, llamar a la API de validación de destinatarios y exportar los resultados a un archivo CSV.

Así, por cada línea leída, llamo a la API y escribo los resultados asincrónicamente para no mantener ninguno de estos datos en memoria a largo plazo. También eliminé la comprobación de sintaxis de 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 comprobaciones integradas para verificar si un correo electrónico es válido o no.

Mi idea inicial fue la siguiente:

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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, cargar los correos electrónicos en un array y verificar que están en el formato correcto. Tercero, llamar asincrónicamente la API de validación de destinatarios. Cuarto, esperar a 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 detuvo alrededor de las 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, la he enlazado aquí: Version 1 (NOT RECOMMENDED).


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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, contar la cantidad de correos electrónicos en el archivo para propósitos de informes. Tercero, a medida que se lee cada línea de forma asincrónica, llamar a la API de validación de destinatarios y exportar los resultados a un archivo CSV.

Así, por cada línea leída, llamo a la API y escribo los resultados asincrónicamente para no mantener ninguno de estos datos en memoria a largo plazo. También eliminé la comprobación de sintaxis de 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 comprobaciones integradas para verificar si un correo electrónico es válido o no.

Mi idea inicial fue la siguiente:

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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, cargar los correos electrónicos en un array y verificar que están en el formato correcto. Tercero, llamar asincrónicamente la API de validación de destinatarios. Cuarto, esperar a 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 detuvo alrededor de las 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, la he enlazado aquí: Version 1 (NOT RECOMMENDED).


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.


Primero, ingerir una lista de correos electrónicos en formato CSV. Segundo, contar la cantidad de correos electrónicos en el archivo para propósitos de informes. Tercero, a medida que se lee cada línea de forma asincrónica, llamar a la API de validación de destinatarios y exportar los resultados a un archivo CSV.

Así, por cada línea leída, llamo a la API y escribo los resultados asincrónicamente para no mantener ninguno de estos datos en memoria a largo plazo. También eliminé la comprobación de sintaxis de 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 comprobaciones integradas 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 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 al de validaciones completadas. Agregué un temporizador para poder ejecutar benchmarks y asegurarme de obtener 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 () { //Al final del infile, después de que todas las líneas han sido contadas, ejecuta la función de validación de destinatarios validateRecipients.validateRecipients(count, myArgs); });


 Luego llamo a la función validateRecipients. Note que esta función es asíncrona. Después de validar que tanto el infile como el outfile son CSV, escribo una fila de encabezado y comienzo un temporizador de programa usando la biblioteca JSDOM.

async function validateRecipients(email_count, myArgs) { if ( //Si tanto el infile como el outfile están en formato .csv extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Contador para cada llamada API email_count++; //El contador de líneas devuelve #lines - 1, esto se hace para corregir el número de líneas //Inicia un temporizador 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" ); //Escribir los encabezados en el outfile


El siguiente script es realmente el núcleo del programa, así que lo dividiré y explicaré qué está pasando. Por 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, }, }) //Para cada fila leída del infile, llamar al SparkPost Recipient Validation API


Luego, en la respuesta

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

  • Validar si reason es nulo, y si es así, rellenar con un valor vacío (esto es para que el formato del CSV sea consistente, ya que en algunos casos se proporciona reason en la respuesta)

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

  • Convertir el JSON a CSV y sacar el resultado (utilizando json2csv)

  • Escribir el progreso en el terminal

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


.then(function (response) { response.data.results.email = String(email); //Agrega el correo electrónico como par valor/clave al JSON de respuesta para ser utilizado en la salida response.data.results.reason ? null : (response.data.results.reason = ""); //Si reason es nulo, configurarlo en blanco para que el CSV sea uniforme //Utiliza json-2-csv para convertir el JSON a formato CSV y sacar el resultado let options = { prependHeader: false, //Desactiva que los valores JSON sean añadidos 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++; //Incrementar el contador de API process.stdout.write(`Done with ${completed} / ${email_count}\r`); //Sacar estado de Completado / Total al console sin mostrar nuevas líneas //Si todos los correos fueron validados completed == email_count) { const stop = window.performance.now(); //Detener el temporizador console.log( `Todos los correos electrónicos validados con éxito en ${ (stop - start) / 1000 } segundos` ); } })

 

Un problema final que encontré fue que, aunque esto funcionó 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 el correo electrónico XXXXXXX@XXXXXXXXXX.XXX

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

Siguientes pasos

Para alguien que esté buscando un programa sencillo y rápido que acepte un CSV, llame a la API de validación de destinatarios y genere 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 sencilla

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


También me interesaría ver si se podrían lograr resultados más rápidos con otro lenguaje, como Golang o Erlang/Elixir.

Por favor, siéntete libre de proporcionarme cualquier comentario o sugerencia para expandir este proyecto.

Únete a nuestro Newsletter.

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.

Únete a nuestro Newsletter.

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.

Únete a nuestro Newsletter.

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.

Pinterest logo
Uber logo
Square logo
Logo de Adobe
Meta logo
PayPal logo

Company

Configuración de privacidad

Newsletter

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.

Uber logo
Square logo
Logo de Adobe
Meta logo

Company

Configuración de privacidad

Newsletter

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.

Uber logo
Logo de Adobe
Meta logo

Reach

Grow

Manage

Automate

Recursos

Company

Newsletter

Mantente al día con Bird a través de actualizaciones semanales en tu buzón.

Al enviar, aceptas que Bird pueda contactarte sobre nuestros productos y servicios.

Puedes darte de baja en cualquier momento. Consulta el Aviso de Privacidad de Bird para obtener detalles sobre el procesamiento de datos.