Product

Solutions

Resources

Company

Product

Solutions

Resources

Company

Créer un outil de validation des destinataires d'oiseaux asynchrones en vrac

Zacharie Samuels

26 mai 2022

Email

1 min read

Créer un outil de validation des destinataires d'oiseaux asynchrones en vrac

Pour quelqu'un qui recherche un programme simple et rapide qui prend un csv, appelle l'API de validation des destinataires et produit un CSV, ce programme est fait pour vous.

Lors de la création d'applications de messagerie, les développeurs doivent souvent intégrer plusieurs services et API. Comprendre les fondamentaux de l'API email dans l'infrastructure cloud fournit la base pour créer des outils robustes comme le système de validation en masse que nous allons créer dans ce guide.

Une des questions que nous recevons occasionnellement est, comment puis-je valider en masse des listes d'e-mails avec la validation des destinataires? Il y a deux options ici, l'une est de télécharger un fichier via l'interface utilisateur de SparkPost pour la validation, et l'autre est de faire des appels individuels par e-mail à l'API (puisque l'API est une validation d'e-mail unique).

La première option fonctionne bien mais a une limitation de 20 Mo (environ 500 000 adresses). Que faire si quelqu'un a une liste d'e-mails contenant des millions d'adresses ? Cela pourrait signifier diviser cela en milliers de téléchargements de fichiers CSV.

Puisque le téléchargement de milliers de fichiers CSV semble un peu farfelu, j'ai pris ce cas d'utilisation et j'ai commencé à me demander quelle vitesse je pourrais obtenir avec l'API. Dans cet article de blog, je vais expliquer ce que j'ai essayé et comment je suis finalement arrivé à un programme qui pourrait réaliser environ 100 000 validations en 55 secondes (alors que dans l'interface utilisateur, j'obtenais environ 100 000 validations en 1 minute 10 secondes). Et bien que cela prenne encore environ 100 heures pour réaliser environ 654 millions de validations, ce script peut fonctionner en arrière-plan, économisant ainsi un temps considérable.

La version finale de ce programme peut être trouvée ici.

My first mistake: utiliser Python

Python est l'un de mes langages de programmation préférés. Il excelle dans de nombreux domaines et est incroyablement simple. Cependant, un domaine dans lequel il n'excelle pas est celui des processus concurrents. Bien que python ait la capacité d'exécuter des fonctions asynchrones, il possède ce qu'on appelle le verrou global de l'interpréteur Python ou GIL.

« Le verrou global de l'interpréteur Python ou GIL, en termes simples, est un mutex (ou un verrou) qui permet à un seul thread de maintenir le contrôle de l'interpréteur Python.

Cela signifie qu'un seul thread peut être en cours d'exécution à tout moment. L'impact du GIL n'est pas visible pour les développeurs qui exécutent des programmes monothreadés, mais il peut être un goulot d'étranglement de performance dans le code à encombrement CPU et multi-thread.

Étant donné que le verrou global de l'interpréteur (GIL) permet à un seul thread de s'exécuter à la fois, même sur les systèmes multicœurs, il a acquis une réputation de « fonction célèbre » de Python (voir l'article de Real Python sur le GIL).

Au début, je n'étais pas conscient du GIL, donc j'ai commencé à programmer en python. À la fin, même si mon programme était asynchrone, il était coincé, et peu importe le nombre de threads que j'ajoutais, je n'obtenais toujours qu'environ 12 à 15 itérations par seconde.

La partie principale de la fonction asynchrone en Python peut être vue ci-dessous :

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)

Donc, j'ai abandonné l'utilisation de Python et je suis retourné à la planche à dessin...

Je me suis décidé à utiliser NodeJS en raison de sa capacité à effectuer des opérations d'entrée/sortie non bloquantes extrêmement bien. Une autre option excellente pour gérer le traitement asynchrone de l'API est de construire des consommateurs de webhook sans serveur avec Azure Functions, qui peuvent gérer efficacement des charges de travail variables. Je suis également assez familier avec la programmation en NodeJS.

En utilisant les aspects asynchrones de Node.js, cette approche a bien fonctionné. Pour plus de détails sur la programmation asynchrone dans Node.js, voir le guide de RisingStack sur la programmation asynchrone dans Node.js.

Ma deuxième erreur : essayer de lire le fichier en mémoire

Mon idée initiale était la suivante :

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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, charger les e-mails dans un tableau et vérifier qu'ils sont au format correct. Troisièmement, appeler de manière asynchrone l'API de validation des destinataires. Quatrièmement, attendre les résultats et les charger dans une variable. Et enfin, sortir cette variable dans un fichier CSV.

Cela a très bien fonctionné pour les petits fichiers. Le problème est survenu lorsque j'ai essayé de traiter 100 000 e-mails. Le programme s'est bloqué autour de 12 000 validations. Avec l'aide de l'un de nos développeurs front-end, j'ai vu que le problème venait du chargement de tous les résultats dans une variable (et donc de l'épuisement rapide de la mémoire). Si vous souhaitez voir la première version de ce programme, je l'ai liée ici : Version 1 (NON RECOMMANDÉE).


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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, compter le nombre d'e-mails dans le fichier à des fins de reporting. Troisièmement, à mesure que chaque ligne est lue de manière asynchrone, appeler l'API de validation des destinataires et sortir les résultats dans un fichier CSV.

Ainsi, pour chaque ligne lue, j'appelle l'API et écris les résultats de manière asynchrone afin de ne pas conserver ces données en mémoire à long terme. J'ai également retiré la vérification de la syntaxe des e-mails après avoir parlé avec l'équipe de validation des destinataires, car ils m'ont informé que la validation des destinataires intègre déjà des vérifications pour savoir si un e-mail est valide ou non.

Mon idée initiale était la suivante :

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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, charger les e-mails dans un tableau et vérifier qu'ils sont au format correct. Troisièmement, appeler de manière asynchrone l'API de validation des destinataires. Quatrièmement, attendre les résultats et les charger dans une variable. Et enfin, sortir cette variable dans un fichier CSV.

Cela a très bien fonctionné pour les petits fichiers. Le problème est survenu lorsque j'ai essayé de traiter 100 000 e-mails. Le programme s'est bloqué autour de 12 000 validations. Avec l'aide de l'un de nos développeurs front-end, j'ai vu que le problème venait du chargement de tous les résultats dans une variable (et donc de l'épuisement rapide de la mémoire). Si vous souhaitez voir la première version de ce programme, je l'ai liée ici : Version 1 (NON RECOMMANDÉE).


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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, compter le nombre d'e-mails dans le fichier à des fins de reporting. Troisièmement, à mesure que chaque ligne est lue de manière asynchrone, appeler l'API de validation des destinataires et sortir les résultats dans un fichier CSV.

Ainsi, pour chaque ligne lue, j'appelle l'API et écris les résultats de manière asynchrone afin de ne pas conserver ces données en mémoire à long terme. J'ai également retiré la vérification de la syntaxe des e-mails après avoir parlé avec l'équipe de validation des destinataires, car ils m'ont informé que la validation des destinataires intègre déjà des vérifications pour savoir si un e-mail est valide ou non.

Mon idée initiale était la suivante :

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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, charger les e-mails dans un tableau et vérifier qu'ils sont au format correct. Troisièmement, appeler de manière asynchrone l'API de validation des destinataires. Quatrièmement, attendre les résultats et les charger dans une variable. Et enfin, sortir cette variable dans un fichier CSV.

Cela a très bien fonctionné pour les petits fichiers. Le problème est survenu lorsque j'ai essayé de traiter 100 000 e-mails. Le programme s'est bloqué autour de 12 000 validations. Avec l'aide de l'un de nos développeurs front-end, j'ai vu que le problème venait du chargement de tous les résultats dans une variable (et donc de l'épuisement rapide de la mémoire). Si vous souhaitez voir la première version de ce programme, je l'ai liée ici : Version 1 (NON RECOMMANDÉE).


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.


Premièrement, ingérer une liste d'e-mails au format CSV. Deuxièmement, compter le nombre d'e-mails dans le fichier à des fins de reporting. Troisièmement, à mesure que chaque ligne est lue de manière asynchrone, appeler l'API de validation des destinataires et sortir les résultats dans un fichier CSV.

Ainsi, pour chaque ligne lue, j'appelle l'API et écris les résultats de manière asynchrone afin de ne pas conserver ces données en mémoire à long terme. J'ai également retiré la vérification de la syntaxe des e-mails après avoir parlé avec l'équipe de validation des destinataires, car ils m'ont informé que la validation des destinataires intègre déjà des vérifications pour savoir si un e-mail est valide ou non.

Décomposition du code final

Après avoir lu et validé les arguments du terminal, j'exécute le code suivant. Tout d'abord, je lis le fichier CSV des e-mails et compte chaque ligne. Cette fonction a deux objectifs : 1) me permettre de reporter avec précision la progression du fichier [comme nous le verrons plus tard], et 2) me permettre d'arrêter un chronomètre lorsque le nombre d'e-mails dans le fichier équivaut aux validations terminées. J'ai ajouté un chronomètre afin de pouvoir exécuter des bancs d'essai et m'assurer d'obtenir de bons résultats.

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);
    });


 Je fais ensuite appel à la fonction validateRecipients. Notez que cette fonction est asynchrone. Après avoir vérifié que le infile et le outfile sont au format CSV, j'écris une ligne d'en-tête et démarre un chronomètre de programme en utilisant la bibliothèque 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
    }
}

Le script suivant constitue vraiment l'essentiel du programme, donc je vais le découper et expliquer ce qui se passe. Pour chaque ligne du 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
    });

Ensuite, sur la réponse

  • Ajouter l'e-mail au JSON (afin de pouvoir imprimer l'e-mail dans le CSV)

  • Vérifier si la raison est nulle, et si c'est le cas, renseigner une valeur vide (c'est pour que le format CSV soit cohérent, car dans certains cas, une raison est donnée dans la réponse)

  • Définir les options et les clés pour le module json2csv.

  • Convertir le JSON en CSV et produire un fichier (en utilisant json2csv)

  • Écrire la progression dans le terminal

  • Enfin, si le nombre d'e-mails dans le fichier = validations terminées, arrêter le chronomètre et imprimer les résultats


.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`
        );
    }
});

 

Un dernier problème que j'ai trouvé est que, bien que cela fonctionnait très bien sur Mac, j'ai rencontré l'erreur suivante sous Windows après environ 10 000 validations :

Erreur : connect ENOBUFS XX.XX.XXX.XXX:443 – Local (indéfini:indéfini) avec l'e-mail XXXXXXX@XXXXXXXXXX.XXX

Après avoir effectué des recherches supplémentaires, il semble s'agir d'un problème avec le pool de connexions du client HTTP NodeJS qui ne réutilise pas les connexions. J'ai trouvé cet article Stackoverflow sur le problème, et après des recherches plus approfondies, j'ai trouvé une bonne configuration par défaut pour la bibliothèque axios qui a résolu ce problème. Je ne suis toujours pas certain pourquoi ce problème ne survient que sous Windows et non sur Mac.

Étapes suivantes

Pour quelqu'un qui recherche un programme simple et rapide qui accepte un fichier CSV, appelle l'API de validation des destinataires et génère un CSV, ce programme est fait pour vous.

Voici quelques ajouts possibles à ce programme :

  • Construire une interface utilisateur ou un front-end plus facile à utiliser

  • Meilleure gestion des erreurs et des réessais, car si pour une raison quelconque l'API génère une erreur, le programme ne relance actuellement pas l'appel

  • Envisager la mise en œuvre en tant que fonction Azure sans serveur pour un dimensionnement automatique et une gestion réduite de l'infrastructure


Je serais également curieux de voir si des résultats plus rapides pourraient être obtenus avec un autre langage, tel que Golang ou Erlang/Elixir. Au-delà du choix de langage, les limites de l'infrastructure peuvent également affecter les performances - nous avons appris cela de première main lorsque nous avons atteint les limites DNS non documentées dans AWS qui ont affecté nos systèmes de traitement des e-mails à haut volume.

Pour les développeurs intéressés par la combinaison du traitement API avec les outils de flux de travail visuels, découvrez comment intégrer Flow Builder avec Google Cloud Functions pour des flux de travail d'automatisation sans code.

N'hésitez pas à me fournir tout retour ou suggestions pour l'expansion de ce projet.

Autres news

Lire la suite de cette catégorie

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

La plateforme AI-native complète qui évolue avec votre business.

Product

Solutions

Resources

Company

À venir bientôt

Social

Newsletter

Restez à jour avec Bird grâce aux mises à jour hebdomadaires dans votre boîte de réception.

S'inscrire

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

La plateforme AI-native complète qui évolue avec votre business.

Product

Solutions

Resources

Company

Social

Newsletter

Restez à jour avec Bird grâce aux mises à jour hebdomadaires dans votre boîte de réception.

S'inscrire