Een van de vragen die we af en toe ontvangen is, hoe kan ik e-maillijsten bulkvalideren met ontvanger validatie? Er zijn hier twee opties, één is om een bestand te uploaden via de SparkPost UI voor validatie, en de andere is om individuele aanvragen per e-mail naar de API te maken (aangezien de API eenvalidatie voor één e-mail is).
De eerste optie werkt prima maar heeft een limiet van 20Mb (ongeveer 500.000 adressen). Wat als iemand een e-maillijst heeft met miljoenen adressen? Het zou kunnen betekenen dat er 1.000’s van CSV-bestandsuploads moeten worden opgesplitst.
Aangezien het uploaden van duizenden CSV-bestanden een beetje vergezocht lijkt, nam ik deze use-case onder de loep 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 ben gekomen dat rond 100.000 validaties in 55 seconden aankon (terwijl ik in de UI rond 100.000 validaties in 1 minuut 10 seconden kreeg). En hoewel dit nog steeds ongeveer 100 uur zou duren om klaar te zijn met ongeveer 654 miljoen validaties, kan dit script op de achtergrond draaien en zo aanzienlijk tijd besparen.
De definitieve versie van dit programma is te vinden hier.
Mijn eerste fout: Python gebruiken
Python is een van mijn favoriete programmeertalen. Het blinkt uit op veel gebieden en is ongelooflijk eenvoudig. Echter, een gebied waarin het niet uitblinkt, is gelijktijdige processen. Hoewel Python het vermogen 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 lock) die slechts één draad toestaat om de controle over de Python interpreter te houden.
Dit betekent dat slechts één draad zich op elk moment in een uitvoeringsstaat kan bevinden. De impact van de GIL is niet zichtbaar voor ontwikkelaars die enkelvoudig gethreadtprogramma's uitvoeren, maar het kan een prestatieknelpunt zijn in CPU-belaste en multithreaded code.
Aangezien de GIL slechts één draad toestaat om tegelijkertijd uit te voeren, zelfs in een multithreaded architectuur met meer dan een CPU-kern, heeft de GIL een reputatie gekregen als een 'berucht' kenmerk van Python.” (https://realpython.com/python-gil/)”
In eerste instantie wist ik niet van het bestaan van de GIL, dus begon ik te programmeren in Python. Aan het einde, hoewel mijn programma asynchroon was, raakte het vergrendeld 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 gebruiken van Python en ging ik terug naar de tekentafel...
Ik besloot NodeJS te gebruiken vanwege zijn vermogen om niet-blokkerende i/o-operaties zeer goed uit te voeren. Ik ben ook redelijk bekend met programmeren in NodeJS.
Door de asynchrone aspecten van NodeJS te gebruiken, werkte dit uiteindelijk goed. Voor meer details over asynchroon programmeren in NodeJS, zie https://blog.risingstack.com/node-hero-async-programming-in-node-js/
Mijn tweede fout: proberen het bestand in geheugen te laden
Mijn oorspronkelijke idee was als volgt:
Eerst, een CSV-lijst van e-mails inlezen. Ten tweede, de e-mails in een array laden en controleren of ze in het juiste formaat zijn. Ten derde, asynchroon de ontvanger validatie API aanroepen. Ten vierde, wachten op de resultaten en deze in een variabele laden. En ten slotte, deze variabele naar een CSV-bestand uitvoeren.
Dit werkte erg goed voor kleinere bestanden. Het probleem ontstond toen ik probeerde 100.000 e-mails te verwerken. Het programma liep vast rond 12.000 validaties. Met hulp van een van onze front-end developers zag ik dat het probleem lag bij het laden van alle resultaten in een variabele (en daardoor snel een gebrek aan geheugen). Als je de eerste iteratie van dit programma wilt zien, heb ik hem hier gelinkt: Versie 1 (NIET AANBEVOLEN).
Eerst, een CSV-lijst van e-mails inlezen. Ten tweede, het aantal e-mails in het bestand tellen voor rapportage doeleinden. Ten derde, wanneer elke regel asynchroon gelezen is, de ontvanger validatie API aanroepen en de resultaten naar een CSV-bestand uitvoeren.
Dus voor elke gelezen regel roep ik de API aan en schrijf ik de resultaten asynchroon uit om geen van deze gegevens in langetermijngeheugen te bewaren. Ik heb ook de controle van e-mailsyntaxis verwijderd na overleg met het ontvanger validatie team, omdat ze me vertelden dat ontvanger validatie al controles ingebouwd heeft om te controleren of een e-mail geldig is of niet.
Uitleg van de uiteindelijke code
Na het inlezen en valideren van de terminalargumenten voer ik de volgende code uit. Eerst lees ik het CSV-bestand van e-mails in en tel ik elke regel. Er zijn twee doelen van deze functie, 1) het stelt me in staat nauwkeurig te rapporteren over de voortgang van het bestand [zoals we later zullen zien], en 2) het stelt me in staat een timer te stoppen wanneer het aantal e-mails in het bestand gelijk is aan voltooide validaties. Ik voegde een timer toe zodat ik benchmarks kan uitvoeren en kan zorgen dat ik goede resultaten krijg.
let count = 0; //Regelteller require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Leest de inkomende file en verhoogt het aantal voor elke regel .on("close", function () { //Aan het einde van de inkomende file, na het tellen van alle regels, voer de ontvanger validatie uit validateRecipients.validateRecipients(count, myArgs); });
Dan roep ik de validateRecipients functie aan. Merk op dat deze functie asynchroon is. Na validering dat het inkomende en uitgaande bestand CSV is, schrijf ik een hoofdrij en start ik een programma timer met de JSDOM bibliotheek.
async function validateRecipients(email_count, myArgs) { if ( //Als zowel het inkomende als uitgaande bestand in .csv formaat zijn extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Teller voor elke API oproep email_count++; //Regelteller retourneert aantal regels - 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]); //Uitgaande bestand output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Schrijf de koppen in het uitgaande bestand
Het volgende script is eigenlijk het grootste deel van het programma, dus ik zal het opbreken en uitleggen wat er gebeurt. Voor elke regel van de inkomende file:
Neem die regel asynchroon en roep de ontvanger validatie 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 regel ingelezen vanuit de inkomende file, roep de SparkPost Ontvanger Validatie API aan
Vervolgens, bij de respons
Voeg de email toe aan de JSON (om de email in de CSV te kunnen printen)
Valideer of reason null is, en zo ja, geef een lege waarde (dit is zodat het CSV-formaat consistent is, aangezien reason in sommige gevallen in de respons wordt gegeven)
Stel de opties en sleutels in voor de json2csv module.
Converteer de JSON naar CSV en voer uit (gebruikmakend van json2csv)
Schrijf voortgang in de terminal
Tot slot, als het aantal e-mails in het bestand gelijk is aan voltooide validaties, stop de timer en print de resultaten
.then(function (response) { response.data.results.email = String(email); //Voegt de email toe als een waarde/sleutel paar aan de respons JSON om te worden gebruikt voor uitvoer response.data.results.reason ? null : (response.data.results.reason = ""); //Als reason null is, stel het in op blanco zodat de CSV uniform is //Gebruikt json-2-csv om de JSON naar CSV-formaat te converteren en uit te voeren let options = { prependHeader: false, //Schakelt JSON-waarden uit van het toevoegen als hoofdrijen op elke regel 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`); //Uitvoerstaat van Voltooid / Totaal naar de console zonder nieuwe lijnen te laten zien //Als alle e-mails zijn gevalideerd if (completed == email_count) { const stop = window.performance.now(); //Stop de timer console.log( `Alle e-mails succesvol gevalideerd in ${ (stop - start) / 1000 } seconden` ); } })
Een laatste probleem dat ik vond was dat, hoewel dit geweldig werkte op Mac, kreeg ik de volgende fout op Windows na ongeveer 10.000 validaties:
Error: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) with email XXXXXXX@XXXXXXXXXX.XXX
Na verder onderzoek lijkt het een probleem te zijn met de NodeJS HTTP-client verbindingspool die verbindingen niet 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 oplost. Ik weet nog steeds niet zeker waarom dit probleem alleen op Windows voorkomt en niet op Mac.
Volgende Stappen
Voor iemand die een eenvoudig en snel programma zoekt dat een csv inleest, de ontvanger validatie API aanroept, en een CSV uitvoert, is dit programma iets voor jou.
Enkele toevoegingen aan dit programma zouden de volgende zijn:
Bouw een front end of eenvoudigere UI voor gebruik
Betere fout- en opniew proberen verwerking omdat als om de een of andere reden de API een fout gooit, het programma momenteel de oproep niet opnieuw probeert
Ik ben ook nieuwsgierig om te zien of snellere resultaten kunnen worden behaald met een andere taal zoals Golang of Erlang/Elixir.
Voel je vrij om me feedback of suggesties te geven voor het uitbreiden van dit project.