
Voor iemand die op zoek is naar een eenvoudig en snel programma dat een csv-bestand verwerkt, de ontvanger-validatie API aanroept en een CSV oplevert, is dit programma voor jou.
Bij het bouwen van e-mailapplicaties moeten ontwikkelaars vaak meerdere diensten en API's integreren. Begrippen over fundamenten van e-mail API in cloud infrastructuur bieden de basis voor het bouwen van robuuste tools zoals het bulk validatiesysteem dat we in deze gids zullen maken.
Een van de vragen die we af en toe ontvangen is, hoe kan ik e-maillijsten bulk valideren met ontvangersvalidatie? Er zijn hier twee opties, de ene is een bestand uploaden via de SparkPost UI voor validatie, en de andere is het maken van individuele oproepen per e-mail naar de API (aangezien de API een enkele e-mailvalidatie is).
De eerste optie werkt geweldig maar heeft een limiet van 20MB (ongeveer 500.000 adressen). Wat als iemand een e-maillijst heeft met miljoenen adressen? Dat zou kunnen betekenen dat dit moet worden opgesplitst in 1.000’s CSV-bestand uploads.
Aangezien het uploaden van duizenden CSV-bestanden een beetje vergezocht lijkt, nam ik die gebruiksaanwijzing en begon ik me af te vragen hoe snel ik de API kon laten draaien. In deze blogpost zal ik uitleggen wat ik heb geprobeerd en hoe ik uiteindelijk tot een programma kwam dat rond 100.000 validaties in 55 seconden kon uitvoeren (terwijl ik in de UI ongeveer 100.000 validaties in 1 minuut en 10 seconden kreeg). En hoewel dit nog steeds ongeveer 100 uur zou duren om ongeveer 654 miljoen validaties uit te voeren, kan dit script op de achtergrond draaien wat aanzienlijke tijd bespaart.
De definitieve versie van dit programma kan worden gevonden hier.
Mijn eerste fout: het gebruik van Python
Python is een van mijn favoriete programmeertalen. Het blinkt uit in veel gebieden en is ongelooflijk eenvoudig. Eén gebied waarin het echter niet excelleert, zijn gelijktijdige processen. Hoewel Python de mogelijkheid heeft om asynchrone functies uit te voeren, heeft het wat bekend staat als The Python Global Interpreter Lock of GIL.
“The Python Global Interpreter Lock of GIL, in eenvoudige woorden, is een mutex (of een slot) die slechts één thread toestaat de controle over de Python-interpreter te houden.
Dit betekent dat slechts één thread op elk moment in een uitvoeringsstaat kan zijn. De impact van de GIL is niet zichtbaar voor ontwikkelaars die enkelvoudige, eendraadse programma's uitvoeren, maar het kan een prestatieknelpunt zijn in CPU-intensieve en meerdradige code.
Aangezien de GIL slechts één thread toestaat om tegelijkertijd uit te voeren, zelfs in een meerdradige architectuur met meer dan één CPU-kern, heeft de GIL de reputatie van een ‘beruchte’ eigenschap van Python.” (https://realpython.com/python-gil/)”
In eerste instantie was ik niet op de hoogte van de GIL, dus begon ik in Python te programmeren. Aan het eind, hoewel mijn programma asynchroon was, sloot het zich op, en hoeveel threads ik ook toevoegde, ik kreeg nog steeds maar ongeveer 12-15 iteraties per seconde.
Het belangrijkste deel van de asynchrone functie in Python is hieronder te zien:
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)
Dus stopte ik met het gebruik van Python en ging terug naar de tekentafel…
Ik besloot NodeJS te gebruiken vanwege zijn vermogen om niet-blokkerende i/o-bewerkingen extreem goed uit te voeren. Een andere uitstekende optie voor het verwerken van asynchrone API-verwerking is het bouwen van serverloze webhook-consumenten met Azure Functions, die efficiënt variabele workloads kan verwerken. Ik ben ook vrij bekend met programmeren in NodeJS.
Door gebruik te maken van de asynchrone aspecten van NodeJS, werkte dit goed. Voor meer details over asynchrone programmering in NodeJS, zie https://blog.risingstack.com/node-hero-async-programming-in-node-js/
Mijn tweede fout: proberen het bestand in het geheugen te lezen
Het uiteindelijke code ontleden
Na het lezen en valideren van de terminalargumenten voer ik de volgende code uit. Eerst lees ik het CSV-bestand met e-mails in en tel ik elke regel. Er zijn twee doelen van deze functie, 1) het stelt me in staat om nauwkeurig over de voortgang van het bestand te rapporteren [zoals we later zullen zien], en 2) het stelt me in staat om een timer te stoppen wanneer het aantal e-mails in het bestand gelijk is aan de voltooide validaties. Ik heb een timer toegevoegd zodat ik benchmarks kan uitvoeren en ervoor kan zorgen dat ik goede resultaten behaal.
let count = 0; //Lijn tellen require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Leest het invoerbestand en verhoogt de telling voor elke regel .on("close", function () { //Aan het einde van het invoerbestand, nadat alle regels zijn geteld, de functievalideringsfunctie validateRecipients.runnen validateRecipients(count, myArgs); });
Vervolgens roep ik de validateRecipients-functie aan. Merk op dat deze functie asynchroon is. Na validatie dat het invoer- en uitvoerbestand CSV-bestanden zijn, schrijf ik een kopregel en start ik een programmatimer met behulp van de JSDOM-bibliotheek.
async function validateRecipients(email_count, myArgs) { if ( //Als zowel het invoer- als uitvoerbestand in .csv formaat is extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Teller voor elke API-oproep email_count++; //Lijnteller retourneert #lines - 1, dit wordt gedaan om het aantal regels te corrigeren //Start een timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //Uitvoerbestand output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Schrijf de koppen in het uitvoerbestand
Het volgende script is eigenlijk de kern van het programma, dus ik zal het opbreken en uitleggen wat er gebeurt. Voor elke regel van het invoerbestand:
Neem die regel asynchroon en roep de recipient validation API aan.
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, }, }) //Voor elke rij die uit het invoerbestand wordt gelezen, roep de SparkPost Recipient Validation API aan
Vervolgens, bij de respons
Voeg de e-mail toe aan de JSON (om de e-mail in de CSV te kunnen printen)
Valideer of de reden nul is, en zo ja, vul een lege waarde in (dit is zodat het CSV-formaat consistent is, aangezien in sommige gevallen een reden wordt gegeven in de respons)
Stel de opties en sleutels in voor de json2csv-module.
Converteer de JSON naar CSV en voer uit (gebruik makend van json2csv)
Schrijf de voortgang in de terminal
Tenslotte, als het aantal e-mails in het bestand = voltooide validaties, stop de timer en print de resultaten uit
.then(function (response) { response.data.results.email = String(email); //Voegt de e-mail toe als een waarde/sleutelpaar aan de respons-JSON voor gebruik in de uitvoer response.data.results.reason ? null : (response.data.results.reason = ""); //Als de reden nul is, zet deze op leeg zodat de CSV uniform is //Maakt gebruik van json-2-csv om de JSON naar CSV-formaat te converteren en uitvoeren let options = { prependHeader: false, //Schakelt JSON-waarden uit om als koprijen voor elke regel toe te voegen keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Stelt de volgorde van sleutels in }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Verhoog de API-teller process.stdout.write(`Klaar met ${completed} / ${email_count}\r`); //Output status van Voltooid / Totaal naar de console zonder nieuwe regels te tonen //Als alle e-mails zijn gevalideerd if (completed == email_count) { const stop = window.performance.now(); //Stop de timer console.log( `Alle e-mails met succes gevalideerd in ${ (stop - start) / 1000 } seconden` ); } })
Een laatste probleem dat ik vond was hoewel dit goed werkte op Mac, ik de volgende fout ondervond op Windows na ongeveer 10.000 validaties:
Fout: connect ENOBUFS XX.XX.XXX.XXX:443 – Lokaal (undefined:undefined) met e-mail XXXXXXX@XXXXXXXXXX.XXX
Na verder onderzoek blijkt het een probleem te zijn met de NodeJS HTTP-client verbinding die geen verbindingen hergebruikt. Ik vond dit Stackoverflow-artikel over het probleem, en na verder graven vond ik een goede standaardconfiguratie voor de axios-bibliotheek die dit probleem oploste. Ik weet nog steeds niet zeker waarom dit probleem alleen op Windows optreedt en niet op Mac.
Volgende Stappen
Voor iemand die op zoek is naar een eenvoudig snel programma dat een csv-bestand inleest, de ontvanger verificatie API aanroept en een CSV produceert, is dit programma voor jou.
Enkele toevoegingen aan dit programma zouden de volgende zijn:
Bouw een front-end of eenvoudiger UI voor gebruik
Betere fout- en herhalingsafhandeling omdat het programma momenteel de oproep niet opnieuw probeert als om de een of andere reden de API een fout teruggeeft
Overweeg om het als een serverloze Azure Function te implementeren voor automatische schaalvergroting en verminderde infrastructuurbeheer
Ik zou ook benieuwd zijn of snellere resultaten zouden kunnen worden behaald met een andere taal zoals Golang of Erlang/Elixir. Naast taalkeuze kunnen infrastructuurbeperkingen ook invloed hebben op de prestaties - we hebben dit uit de eerste hand ervaren toen we ongedocumenteerde DNS-limieten in AWS bereikten die onze systemen voor het verwerken van grote hoeveelheden e-mails beïnvloedden.
Voor ontwikkelaars die geïnteresseerd zijn in het combineren van API-verwerking met visuele workflow tools, kijk hoe je Flow Builder met Google Cloud Functions kunt integreren voor no-code automatiseringsworkflows.
Voel je vrij om me feedback of suggesties te geven voor het uitbreiden van dit project.