المنتج

حلول

الموارد

شركة

المنتج

حلول

الموارد

شركة

بناء أداة التحقق من المستلمين لشركة Bird بشكل غير متزامن وبالجملة

زاكاري صامويلز

26‏/05‏/2022

البريد الإلكتروني

1 min read

بناء أداة التحقق من المستلمين لشركة Bird بشكل غير متزامن وبالجملة

بالنسبة للشخص الذي يبحث عن برنامج بسيط وسريع يقبل ملف بصيغة CSV، ويستدعي واجهة برمجة تطبيقات التحقق من المستلم، ويُخرج ملف CSV، فهذا البرنامج مناسب لك.

عند إنشاء تطبيقات البريد الإلكتروني، يحتاج المطورون في كثير من الأحيان إلى دمج خدمات وواجهات برمجة تطبيقات متعددة. فهم أساسيات واجهة برمجة التطبيقات للبريد الإلكتروني في بنية السحابة يوفر الأساس لبناء أدوات قوية مثل نظام التحقق الضخم الذي سنقوم بإنشائه في هذا الدليل.

أحد الأسئلة التي نتلقاها من حين لآخر هو، كيف يمكنني التحقق من قوائم البريد الإلكتروني بشكل جماعي باستخدام التحقق من المستلم? هناك خياران هنا، الأول هو تحميل ملف من خلال واجهة مستخدم SparkPost للتحقق، والآخر هو إجراء مكالمات فردية لكل بريد إلكتروني إلى واجهة برمجة التطبيقات (حيث إن واجهة برمجة التطبيقات تقوم بالتحقق من بريد إلكتروني واحد فقط).

يعمل الخيار الأول بشكل رائع ولكنه يقتصر عند حد 20 ميغابايت (حوالي 500,000 عنوان). ماذا لو كان لدى شخص ما قائمة بريد إلكتروني تحتوي على ملايين العناوين؟ قد يعني ذلك تقسيمه إلى 1,000 تحميل لملفات CSV.

نظراً لأن تحميل آلاف ملفات CSV يبدو شيئاً ليس عملياً، فقد أخذت هذا السيناريو وبدأت أتساءل عن مدى سرعة تشغيل واجهة برمجة التطبيقات. في هذه التدوينة، سأشرح ما جربته وكيف توصلت في النهاية إلى برنامج يمكنه القيام بحوالي 100,000 عملية تحقق في 55 ثانية (بينما في واجهة المستخدم حصلت على حوالي 100,000 عملية تحقق في 1 دقيقة و10 ثواني). وبينما يستغرق هذا حوالي 100 ساعة لإتمام حوالي 654 مليون عملية تحقق، يمكن لهذا البرنامج أن يعمل في الخلفية موفراً وقتاً كبيراً.

يمكن العثور على النسخة النهائية من هذا البرنامج هنا.

أول خطأ لي: استخدام Python

بايثون هي واحدة من لغات البرمجة المفضلة لدي. إنها تتميز في العديد من المجالات وهي مباشرة بشكل لا يصدق. ومع ذلك، فإن إحدى المجالات التي لا تبرز فيها هي العمليات المتزامنة. بينما لدى بايثون القدرة على تشغيل الوظائف غير المتزامنة، لديها ما يُعرف بقفل المترجم للغة بايثون أو GIL.

"قفل المترجم للغة بايثون أو GIL، ببساطة، هو متغير قفل يسمح فقط لمؤشر ترابط واحد بالتحكم في مترجم بايثون.

هذا يعني أن مؤشر ترابط واحد فقط يمكن أن يكون في حالة تنفيذ في أي وقت. تأثير GIL غير مرئي للمطورين الذين ينفذون برامج فردية الخيوط، ولكن يمكن أن يكون عائقًا في الأداء في الشفرة المرتبطة بالمعالج والمتعددة الخيوط.

نظرًا لأن قفل المترجم (GIL) يسمح فقط لمؤشر ترابط واحد بالتنفيذ في وقت واحد، حتى في الأنظمة متعددة النواة، فقد اكتسب سمعة "سيئة" كميزة من ميزات بايثون (انظر مقال Real Python حول GIL).

في البداية، لم أكن على علم بـ GIL، لذا بدأت البرمجة في بايثون. في النهاية، رغم أن برنامجي كان غير متزامن، إلا أنه كان يتم قفله، وبغض النظر عن عدد الخيوط التي أضفتها، إلا أنني كنت أحصل فقط على حوالي 12-15 تكرارًا في الثانية.

يمكن رؤية الجزء الرئيسي من الدالة غير المتزامنة في بايثون أدناه:

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)

لذا قمت بإلغاء استخدام بايثون والعودة إلى نقطة الصفر…

اخترت استخدام NodeJS بسبب قدرتها على أداء عمليات الإدخال والإخراج غير المحظورة بشكل ممتاز. خيار رائع آخر لمعالجة عمليات الـ API غير المتزامنة هو بناء مستهلكي webhook بدون خادم باستخدام Azure Functions، والتي يمكنها التعامل بكفاءة مع الأحمال المتغيرة. كما أنني مألوف جدًا بالبرمجة في NodeJS.

باستخدام الجوانب غير المتزامنة من Node.js، عمل هذا الحل بشكل جيد. لمزيد من التفاصيل حول البرمجة غير المتزامنة في Node.js، انظر دليل RisingStack للبرمجة غير المتزامنة في Node.js.

خطأي الثاني: محاولة قراءة الملف في الذاكرة

كانت فكرتي الأولية كما يلي:

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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، تحميل الإيميلات في مصفوفة والتحقق من أنها في الصيغة الصحيحة. ثالثًا، الاتصال بشكل غير متزامن بواجهة برمجة التطبيقات للتحقق من صحة المستلم. رابعًا، انتظر النتائج وحملها في متغير. وأخيرًا، إخراج هذا المتغير إلى ملف CSV.

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، عدّ عدد الرسائل الإلكترونية في الملف لأغراض التقرير. ثالثًا، مع قراءة كل سطر بشكل غير متزامن، قم بالاتصال بواجهة برمجة التطبيقات للتحقق من صحة المستلم وأخرج النتائج إلى ملف CSV.

وبذلك، لكل سطر تتم قراءته، أتصل بواجهة برمجة التطبيقات وأكتب النتائج بشكل غير متزامن حتى لا أحتفظ بأي من هذه البيانات في الذاكرة طويلة الأجل. كما قمت بإزالة التحقق من صحة بناء الجملة للبريد الإلكتروني بعد التحدث مع فريق التحقق من المستلم، حيث أبلغوني أن التحقق من المستلم يحتوي بالفعل على فحوصات مضمنة للتحقق مما إذا كان البريد الإلكتروني صحيحًا أم لا.

كانت فكرتي الأولية كما يلي:

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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، تحميل الإيميلات في مصفوفة والتحقق من أنها في الصيغة الصحيحة. ثالثًا، الاتصال بشكل غير متزامن بواجهة برمجة التطبيقات للتحقق من صحة المستلم. رابعًا، انتظر النتائج وحملها في متغير. وأخيرًا، إخراج هذا المتغير إلى ملف CSV.

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، عدّ عدد الرسائل الإلكترونية في الملف لأغراض التقرير. ثالثًا، مع قراءة كل سطر بشكل غير متزامن، قم بالاتصال بواجهة برمجة التطبيقات للتحقق من صحة المستلم وأخرج النتائج إلى ملف CSV.

وبذلك، لكل سطر تتم قراءته، أتصل بواجهة برمجة التطبيقات وأكتب النتائج بشكل غير متزامن حتى لا أحتفظ بأي من هذه البيانات في الذاكرة طويلة الأجل. كما قمت بإزالة التحقق من صحة بناء الجملة للبريد الإلكتروني بعد التحدث مع فريق التحقق من المستلم، حيث أبلغوني أن التحقق من المستلم يحتوي بالفعل على فحوصات مضمنة للتحقق مما إذا كان البريد الإلكتروني صحيحًا أم لا.

كانت فكرتي الأولية كما يلي:

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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، تحميل الإيميلات في مصفوفة والتحقق من أنها في الصيغة الصحيحة. ثالثًا، الاتصال بشكل غير متزامن بواجهة برمجة التطبيقات للتحقق من صحة المستلم. رابعًا، انتظر النتائج وحملها في متغير. وأخيرًا، إخراج هذا المتغير إلى ملف CSV.

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


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.


أولاً، إدخال قائمة من الرسائل الإلكترونية بصيغة CSV. ثانيًا، عدّ عدد الرسائل الإلكترونية في الملف لأغراض التقرير. ثالثًا، مع قراءة كل سطر بشكل غير متزامن، قم بالاتصال بواجهة برمجة التطبيقات للتحقق من صحة المستلم وأخرج النتائج إلى ملف CSV.

وبذلك، لكل سطر تتم قراءته، أتصل بواجهة برمجة التطبيقات وأكتب النتائج بشكل غير متزامن حتى لا أحتفظ بأي من هذه البيانات في الذاكرة طويلة الأجل. كما قمت بإزالة التحقق من صحة بناء الجملة للبريد الإلكتروني بعد التحدث مع فريق التحقق من المستلم، حيث أبلغوني أن التحقق من المستلم يحتوي بالفعل على فحوصات مضمنة للتحقق مما إذا كان البريد الإلكتروني صحيحًا أم لا.

تحليل الكود النهائي

بعد قراءة وتحقق من صحة وسائط المحطة، أقوم بتشغيل الكود التالي. أولاً، أقرأ ملف CSV للبريد الإلكتروني وأعد كل سطر. هناك غرضان من هذه الوظيفة، 1) يسمح لي بالإبلاغ بدقة عن تقدم الملف [كما سنرى لاحقًا]، و2) يسمح لي بإيقاف المؤقت عندما يساوي عدد الرسائل في الملف التحقق المكتمل. لقد أضفت مؤقتًا حتى أتمكن من إجراء اختبارات معيارية وضمان الحصول على نتائج جيدة.

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


 ثم أتصل بوظيفة validateRecipients. لاحظ أن هذه الوظيفة غير متزامنة. بعد التحقق من أن الملفات المدخلة والملفات الخارجة هي بصيغة CSV، أقوم بكتابة صف الرأس وبدء مؤقت البرنامج باستخدام مكتبة 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
    }
}

البرنامج النصي التالي هو جوهر البرنامج لذا سأقوم بتقسيمه وشرح ما يحدث. لكل سطر من الملف المدخل:

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

ثم، على الرد

  • أضف البريد الإلكتروني إلى JSON (لكي تتمكن من طباعة البريد الإلكتروني في CSV)

  • تحقق مما إذا كان السبب غير معرف، وإذا كان الأمر كذلك، قم بملء قيمة فارغة (وذلك لكي يكون تنسيق CSV متسقًا، حيث يتم إعطاء السبب في بعض الأحيان في الرد)

  • تعيين الخيارات والمفاتيح لوحدة json2csv.

  • تحويل JSON إلى CSV وإخراجها (باستخدام json2csv)

  • كتابة التقدم في المحطة

  • وأخيرًا، إذا كان عدد الرسائل في الملف = التحقق المكتمل، أوقف المؤقت واطبع النتائج


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

 

اكتشفت مشكلة أخيرة كانت أنه في حين أن هذا كان يعمل بشكل جيد على ماك، فقد واجهت الخطأ التالي عند استخدام ويندوز بعد حوالي 10,000 عملية تحقق:

خطأ: connect ENOBUFS XX.XX.XXX.XXX:443 - المحلية (غير محدد:غير محدد) مع البريد الإلكتروني XXXXXXX@XXXXXXXXXX.XXX

بعد إجراء بعض الأبحاث الإضافية، يبدو أن هناك مشكلة تتعلق بعدم إعادة استخدام اتصالات حوض عميل HTTP لـ NodeJS. لقد وجدت هذا مقالة في Stackoverflow حول المشكلة، وبعد التحري العميق، وجدت إعداد افتراضي جيد لمكتبة axios التي تم حل المشكلة بها. ما زلت غير متأكد من سبب حدوث هذه المشكلة فقط على ويندوز وليس على ماك.

الخطوات التالية

لشخص يبحث عن برنامج بسيط وسريع يستقبل ملف CSV، ويستدعي API التحقق من المستلم، ويُصدر ملف CSV، هذا البرنامج مناسب لك.

بعض الإضافات لهذا البرنامج ستكون كما يلي:

  • إنشاء واجهة أمامية أو واجهة مستخدم أسهل للاستخدام

  • تحسين معالجة الأخطاء والمحاولات لأن البرنامج حالياً لا يعيد المحاولة إذا حدث خطأ في استدعاء API لسبب ما

  • اعتبر التنفيذ كـ وظيفة Azure بدون خادم لتحقيق التوسع التلقائي وتقليل إدارة البنية التحتية


سأكون فضولياً أيضًا لمعرفة ما إذا كان يمكن تحقيق نتائج أسرع باستخدام لغة أخرى مثل Golang أو Erlang/Elixir. إلى جانب اختيار اللغة، يمكن أن تؤثر قيود البنية التحتية أيضًا على الأداء - لقد تعلمنا ذلك بأنفسنا عندما واجهنا قيود DNS غير موثقة في AWS التي أثرت على أنظمة معالجة البريد الإلكتروني ذات الحجم الكبير لدينا.

للمطورين المهتمين بدمج معالجة API مع أدوات سير العمل البصرية، اطلع على كيفية دمج Flow Builder مع Google Cloud Functions لأتمتة سير العمل بدون كود.

يرجى عدم التردد في تقديم أي ملاحظات أو اقتراحات لتوسيع هذا المشروع.

أخبار أخرى

اقرأ المزيد من هذه الفئة

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

منصة AI-native الكاملة التي تتماشى مع نمو عملك.

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

منصة AI-native الكاملة التي تتماشى مع نمو عملك.

المنتج

حلول

الموارد

اجتماعي

النشرة الإخبارية

ابقَ على اطلاع مع Bird من خلال التحديثات الأسبوعية إلى بريدك الوارد.

اشتراك

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

منصة AI-native الكاملة التي تتماشى مع نمو عملك.

المنتج

حلول

الموارد

اجتماعي

النشرة الإخبارية

ابقَ على اطلاع مع Bird من خلال التحديثات الأسبوعية إلى بريدك الوارد.

اشتراك