Bygga ett verktyg för validering av mottagare för asynkron bulköverföring Bird

Bygga ett verktyg för validering av mottagare för asynkron bulköverföring Bird

Bygga ett verktyg för validering av mottagare för asynkron bulköverföring Bird

May 26, 2022

Publicerad av

Publicerad av

Zachary Samuels

Zachary Samuels

Kategori:

Kategori:

E-post

E-post

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 validering av mottagare? 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 till API (as the API is single email validation).


Det första alternativet fungerar utmärkt men har en begränsning på 20 Mb (ca 500 000 adresser). Vad händer om någon har en e-postlista som innehåller miljontals adresser? Det kan innebära att man måste dela upp den i 1 000-tals CSV-filuppladdningar.


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 valideringar på 55 sekunder (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.


Den slutliga versionen av detta program kan hittas here.


Mitt första misstag: att använda Python

Python är ett av mina favoritprogrammeringsspråk. Det utmärker sig på många områden och är otroligt enkelt. Ett område som det dock inte utmärker sig inom är samtidiga processer. Python har visserligen möjlighet att köra asynkrona funktioner, men det har det som kallas Den Python Global Interpreter Lock eller GIL.


"Python Global Interpreter Lock eller GIL är, enkelt uttryckt, en mutex (eller ett lås) som gör att endast en tråd kan kontrollera Python-tolken.


Detta innebär att endast en tråd kan befinna sig i exekveringsstatus vid varje tidpunkt. Effekten av GIL är inte synlig för utvecklare som kör enkeltrådade program, men det kan vara en flaskhals för prestandan i CPU-bunden och flertrådad kod.


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/)”


Först var jag inte medveten om GIL, så jag började programmera i python. I slutet, trots att mitt program var asynkront, blev det låst, och oavsett hur många trådar jag lade till, fick jag fortfarande bara cirka 12-15 iterationer per sekund.


Huvuddelen av den asynkrona funktionen i Python kan ses nedan:

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)

 

Så jag skrotade Python och gick tillbaka till ritbordet...


Jag bestämde mig för att använda NodeJS på grund av dess förmåga att utföra icke-blockerande i/o-operationer extremt bra. Jag är också ganska bekant med programmering i 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/


Mitt andra misstag: försökte läsa filen i minnet

Min ursprungliga idé var följande:



Först läser du in en CSV-lista med e-postmeddelanden. Därefter laddar du e-postmeddelandena i en array och kontrollerar att de har rätt format. För det tredje anropar du API:et för mottagarvalidering asynkront. För det fjärde väntar du på resultaten och laddar dem i en variabel. Och slutligen matar du ut variabeln till en CSV-fil.


This worked very well for smaller files. Den issue became when I tried to run 100,000 emails through. The 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: Version 1 (REKOMMENDERAS INTE).



Först tar du in en CSV-lista med e-postmeddelanden. Sedan räknar du antalet e-postmeddelanden i filen för rapporteringsändamål. För det tredje, när varje rad läses asynkront, anropa API:et för mottagarvalidering och mata ut resultaten till en CSV-fil.


För varje rad som läses in anropar jag API:et och skriver ut resultaten asynkront för att inte lagra någon av dessa data i långtidsminnet. Jag tog också bort syntaxkontrollen för e-post efter att ha pratat med teamet för mottagarvalidering, eftersom de informerade mig om att mottagarvalidering redan har inbyggda kontroller för att kontrollera om ett e-postmeddelande är giltigt eller inte.


Uppdelning av den slutliga koden

Efter att ha läst in och validerat terminalargumenten kör jag följande kod. Först läser jag in CSV-filen med e-postmeddelanden och räknar varje rad. Det finns två syften med den här funktionen, 1) den gör att jag kan rapportera exakt om filens framsteg [som vi kommer att se senare], och 2) den gör att jag kan stoppa en timer när antalet e-postmeddelanden i filen är lika med slutförda valideringar. Jag lade till en timer så att jag kan köra riktmärken och se till att jag får bra resultat.


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

 

Sedan anropar jag funktionen validateRecipients. Observera att denna funktion är asynkron. Efter att ha kontrollerat att infilen och utfilen är CSV-filer skriver jag en rubrikrad och startar en programtimer med hjälp av JSDOM-biblioteket.


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

 

Följande skript är egentligen huvuddelen av programmet så jag kommer att dela upp det och förklara vad som händer. För varje rad i 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

 

Sedan, på svaret

  • Lägg till e-postmeddelandet i JSON (för att kunna skriva ut e-postmeddelandet i CSV-filen)

  • Validera om reason är null, och fyll i så fall i ett tomt värde (detta är för att CSV-formatet ska vara konsekvent, eftersom reason i vissa fall anges i svaret)

  • Ange alternativ och nycklar för modulen json2csv.

  • Konvertera JSON till CSV och mata ut (med hjälp av json2csv)

  • Skriv framsteg i terminalen

  • Slutligen, om antalet e-postmeddelanden i filen = slutförda valideringar, stoppa timern och skriv ut resultaten


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

 

Ett sista problem jag hittade var att även om detta fungerade bra på Mac, stötte jag på följande fel med Windows efter cirka 10 000 valideringar:


Fel: Anslut ENOBUFS XX.XX.XXX.XXX:443 - Lokal (odefinierad:odefinierad) med e-post 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 Stackoverflow-artikel on the issue, and after further digging, found a good standardkonfiguration for the axios library that resolved this issue. I am still not certain why this issue only happens on Windows and not on Mac.


Nästa steg

Om du letar efter ett enkelt och snabbt program som tar in en csv-fil, anropar API:et för mottagarvalidering och matar ut en csv-fil är det här programmet något för dig.


Några tillägg till detta program skulle vara följande:

  • Skapa en frontend eller ett enklare användargränssnitt för användning

  • Bättre hantering av fel och återupprepning eftersom programmet för närvarande inte återupprepar anropet om API av någon anledning ger ett fel


Jag skulle också vara nyfiken på att se om snabbare resultat kan uppnås med ett annat språk som Golang eller Erlang/Elixir.


Please feel free to provide me any feedback eller förslag for expanding this project.

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

The right message -> to the right person -> vid right time.

By clicking "See Bird" you agree to Bird's Meddelande om integritet.

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

The right message -> to the right person -> vid right time.

By clicking "See Bird" you agree to Bird's Meddelande om integritet.