GNU Mailman
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)GNU Mailman
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) GNU Mailman
Barry Warsaw
إذا استمتعت بهذه الكتب، فقد تستمتع أيضًا بـ التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.
GNU Mailman هو برمجية حرة لإدارة قوائم البريد الإلكتروني. تقريبًا كل من يكتب البرمجيات الحرة والمفتوحة أو يستخدمها قد صادف قائمة بريدية. ويمكن أن تكون القوائم البريدية قائمةً للمناقشة أو قائمةً للإعلانات، بكل أشكال التنوّع بينهما. وأحيانًا تُبوَّب القوائم البريدية إلى مجموعات أخبار في Usenet أو إلى خدمات مماثلة مثل Gmane. وللقوائم البريدية عادةً أرشيفات تحتوي على السجل التاريخي لجميع الرسائل التي نُشرت إلى القائمة البريدية.
GNU Mailman موجود منذ مطلع تسعينيات القرن الماضي، حين كتب John Viega الإصدار الأول ليصل معجبي فرقة Dave Matthews Band الناشئة بأعضائها الذين كان قد تعارف معهم في الجامعة. ولقد لفت هذا الإصدار الأول انتباه مجتمع Python في منتصف التسعينيات، حين كان مركز عالم Python قد انتقل من CWI، وهو معهد أبحاث علمية في هولندا، إلى CNRI، أي مؤسسة مبادرات البحث الوطنية في ريستون بولاية فرجينيا بالولايات المتحدة. وفي CNRI كنا نشغّل قوائم بريدية متنوعة متعلقة بـ Python باستخدام Majordomo، وهو مدير قوائم بريدية مبني على Perl. وبالطبع لم يكن من المقبول أبدًا أن يحتفظ عالم Python بهذا القدر الكبير من شيفرة Perl. والأهم من ذلك، أننا وجدنا — بسبب تصميم Majordomo — أن تعديله لأغراضنا (مثل إضافة الحد الأدنى من إجراءات مكافحة الرسائل المزعجة) أمر صعب إلى حد بعيد.
كان Ken Manheimer محوريًا في كثير من أعمال GNU Mailman الأولى، وقد أسهم منذ ذلك الحين كثير من المطوّرين المتميزين في Mailman. اليوم يتولى Mark Sapiro صيانة الفرع المستقر 2.1، بينما يركّز Barry Warsaw، مؤلف هذا الفصل، على الإصدار الجديد 3.0.
لقد صمدت كثير من القرارات المعمارية الأصلية التي اتخذها John في الشيفرة حتى فرع Mailman 3، ولا تزال ظاهرة في الفرع المستقر. وفي الأقسام التالية سأصف بعض قرارات التصميم الأكثر إشكالًا في Mailman 1 وMailman 2، وكيف عالجناها في Mailman 3.
في أيام Mailman 1 الأولى، واجهنا مشكلات كثيرة تتعلق برسائل تضيع، أو بعيوب تجعل الرسائل تُسلَّم مرارًا وتكرارًا. وقد دفعنا هذا إلى صياغة مبدأين جامعين هما حاسمان لنجاح Mailman المتصل:
- لا ينبغي أن تضيع أي رسالة أبدًا.
- لا ينبغي أن تُسلَّم أي رسالة أكثر من مرة أبدًا.
في Mailman 2 أعدنا تصميم نظام معالجة الرسائل لضمان أن يبقى هذان المبدآن في الصدارة دائمًا. وقد ظل هذا الجزء من النظام مستقرًا لمدة عقد على الأقل حتى الآن، وهو أحد الأسباب الرئيسية التي جعلت Mailman منشورًا إلى هذا الحد. وبرغم تحديثنا لهذا النظام الفرعي في Mailman 3، فإن التصميم والتنفيذ يظلان إلى حد كبير كما هما.
10.1. تشريح الرسالة
من أبرز بنى البيانات الجوهرية في Mailman هي رسالة البريد الإلكتروني، التي يُمثّلها كائن رسالة. وتأخذ كثير من الواجهات والدوال وطرق النظام ثلاثة وسائط: كائن القائمة البريدية، وكائن الرسالة، وقاموس بيانات وصفية (metadata) يُستخدم لتسجيل الحالة والتواصل بينها أثناء مرور الرسالة عبر النظام.
الشكل 10.1: رسالة MIME من نوع multipart/mixed تحتوي على نص وصور وملف صوتي
للوهلة الأولى تبدو رسالة البريد الإلكتروني كائنًا بسيطًا. وهي تتألف من عدد من أزواج المفتاح والقيمة مفصولة بنقطتين، تُسمى الترويسات (headers)، يليها سطر فارغ يفصل الترويسات عن جسم الرسالة. ويفترض أن تكون هذه التمثيلية النصية سهلة التحليل والتوليد والاستدلال والتعديل، لكنها في الواقع سرعان ما تصبح معقّدة إلى حد بعيد. فهناك أعداد لا تُحصى من وثائق RFC التي تصف كل التنوّعات الممكنة، مثل التعامل مع أنواع بيانات معقّدة كالصور والصوت وما إليها. ويمكن أن يحتوي البريد الإلكتروني على الإنجليزية بترميز ASCII، أو على أي لغة ومجموعة محارف تقريبًا في الوجود. وقد استُعيرت البنية الأساسية لرسالة البريد الإلكتروني مرارًا وتكرارًا في بروتوكولات أخرى مثل NNTP وHTTP، غير أن كل واحد منها يختلف قليلًا. وقد أنتج عملنا في Mailman عدة مكتبات لمجرد التعامل مع تعقيدات هذه الصيغة (وهي كثيرة التسمية بـ"RFC822" نسبةً إلى معيار IETF التأسيسي الصادر عام 1982). أما مكتبات البريد الإلكتروني التي طُوّرت أصلًا لاستخدام GNU Mailman فقد ظهرت في المكتبة القياسية لـ Python، حيث يستمر تطويرها لتصبح أكثر امتثالًا للمعايير وأكثر متانة.
يمكن أن تعمل رسائل البريد الإلكتروني كحاويات لأنواع بيانات أخرى، كما هو محدد في معايير MIME المختلفة. ويمكّن جزء الحاوية من الرسالة (message part) من ترميز صورة أو بعض الصوت أو أي نوع من البيانات الثنائية أو النصية تقريبًا، بما في ذلك أجزاء حاوية أخرى. وفي تطبيقات قراءة البريد تُعرف هذه باسم المرفقات. ويعرض الشكل 10.1 بنية رسالة MIME معقّدة. والصناديق ذات الحدود الصلبة هي أجزاء الحاوية، والصناديق ذات الحدود المتقطعة هي بيانات ثنائية مرمّزة بـ Base64، والصندوق ذو الحدود المنقّطة هو رسالة نصية عادية.
ويمكن أيضًا أن تتداخل أجزاء الحاوية بشكل عشوائي؛ وتُسمى هذه بـالأجزاء المتعددة (multiparts)، وقد تصل عمقًا كبيرًا فعلًا. غير أن أي رسالة بريد إلكتروني، مهما كان تعقيدها، يمكن نمذجته كشجرة ذات كائن رسالة واحد في جذرها. وداخل Mailman نُشير كثيرًا إلى هذا بـ شجرة كائنات الرسائل، ونمرّر هذه الشجرة بالإشارة إلى كائن الرسالة الجذري. ويعرض الشكل 10.2 شجرة الكائنات للرسالة متعددة الأجزاء في الشكل 10.1.
الشكل 10.2: شجرة كائنات رسالة بريد MIME معقّدة
وسطعدّل Mailman الرسالة الأصلية بطريقة ما في كل الأحوال تقريبًا. أحيانًا تكون التحويلات حميدة إلى حد ما، مثل إضافة ترويسات أو إزالتها. وأحيانًا نغيّر بنية شجرة كائنات الرسالة تمامًا، كما حين يزيل مرشّح المحتوى أنواع محتوى معينة مثل HTML أو الصور أو أجزاء أخرى غير نصية. وقد يطوي Mailman حتى "multipart/alternatives" حيث تظهر الرسالة كنص عادي وكنوع نصي غني في الوقت نفسه، أو يضيف أجزاء إضافية تحتوي على معلومات عن القائمة البريدية نفسها.
يحلل Mailman تمثيل بايتات الرسالة على السلك (on the wire) مرة واحدة فقط عادةً، حين تدخل النظام أول مرة. ومن ثم لا يتعامل إلا مع شجرة كائنات الرسالة حتى يصبح جاهزًا لإرسالها من جديد إلى خادم البريد الصادر. وعند هذه النقطة يطوي Mailman الشجرة ثانيةً إلى تمثيل بالبايتات. وفي أثناء ذلك، يقوم Mailman بـ pickle) لكائنات الرسالة من أجل تخزين سريع لها في نظام الملفات وإعادة بنائها منه. وPickles (المخلِّلات) تقنية في Python لتسلسل أي كائن Python، بما في ذلك جميع كائناته الفرعية، إلى تدفق بايتات، وهي مثلى تمامًا لتحسين معالجة أشجار كائنات رسائل البريد الإلكتروني. أما Unpickling (فكّ التخليل) فهو إعادة تسلسل هذا التدفق من البايتات إلى كائن حي. وبتخزين تدفقات البايتات هذه في ملف، تكسب برامج Python ثباتًا (persistence) منخفض التكلفة.
10.2. القائمة البريدية
من الواضح أن القائمة البريدية كائن أساسي آخر في نظام Mailman، وأن معظم عمليات Mailman تتمحور حول القوائم البريدية، مثل:
- تُعرَّف العضوية بوصفها اشتراك مستخدم أو عنوان في قائمة بريدية معيّنة.
- وللقوائم البريدية عدد كبير من خيارات التهيئة المخزّنة في قاعدة البيانات، وهي تتحكّم في كل شيء من صلاحيات النشر إلى كيفية تعديل الرسائل قبل التسليم النهائي.
- وللقوائم البريدية مالكون ومشرفون لهم صلاحيات أوسع لتغيير جوانب من القائمة، أو للموافقة على المنشورات المشكوك فيها ورفضها.
- ولكل قائمة بريدية أرشيفها الخاص.
- وينشر المستخدمون الرسائل الجديدة إلى قائمة بريدية معيّنة.
وهكذا. تأخذ كل عملية تقريبًا في Mailman قائمة بريدية كوسيط—فهذا هو الأساس. وقد خضعت كائنات القوائم البريدية لإعادة تصميم جذرية في Mailman 3 لتصبح أكثر كفاءة ولتوسّع مرونتها.
كان أحد أبكر قرارات John التصميمية هو كيفية تمثيل كائن القائمة البريدية داخل النظام. ولهذا النوع المركزي من البيانات، اختار صنف Python ذا أصناف أساسية متعددة، كل منها ينفّذ جزءًا صغيرًا من مسؤوليات القائمة البريدية. وقد كانت هذه الأصناف الأساسية المتعاونة، والمسمّاة أصناف المزج (mixin classes)، طريقة ذكية لتنظيم الشيفرة بحيث يصبح إضافة خصائص جديدة كليًا أمرًا سهلًا. وبتركيب صنف أساس جديد من صنف المزج، استطاع صنف MailList الأساسي استيعاب ما هو جديد ومثير بسهولة.
على سبيل المثال، لإضافة مُجيب آلي إلى Mailman 2، أُنشئ صنف مزج يحتفظ بالبيانات الخاصة بهذه الميزة. وتُهيّأ هذه البيانات تلقائيًا عند إنشاء قائمة بريدية جديدة. كما وفّر صنف المزج الطرق اللازمة لدعم ميزة المُجيب الآلي. وقد تبيّن أن هذه البنية أكثر نفعًا حين نتناول تصميم تخزين حالة كائن MailList نفسه.
وكان قرار John التصميمي المبكر الآخر هو استخدام pickle في Python لتخزين حالة MailList.
في Mailman 2 تُخزَّن حالة كائن MailList في ملف اسمه config.pck، وهو لا يمثّل سوى التمثيل المُخَلَّل لقاموس كائن MailList. ولكل كائن Python قاموس سمات اسمه __dict__. لذا فإن حفظ كائن قائمة بريدية في ذلك الحين كان يقتصر على الأمر بتخليل __dict__ في ملف، أما تحميله فيقتضي قراءة pickle من الملف وإعادة تكوين __dict__.
وعليه، حين يُضاف صنف مزج جديد لتنفيذ وظيفة جديدة، تُخزَّل جميع سمات صنف المزج وتُستعاد تلقائيًا على النحو المناسب. ولم يكن العمل الإضافي الوحيد الذي علينا أداؤه هو صيانة رقم إصدار المخطط (schema version number) لترقية كائنات القوائم البريدية القديمة تلقائيًا عند إضافة سمات جديدة عبر صنف المزج، لأن التمثيل المُخَلَّل لكائنات MailList القديمة سيكون خاليًا من السمات الجديدة.
ومع بساطة هذه الطريقة، انهارت بنية أصناف المزج وتخزين حالة pickle في النهاية تحت ثقلهما. فكان مديرو المواقع يطلبون غالبًا وسائل للوصول إلى متغيّرات تهيئة القوائم البريدية عبر أنظمة خارجية غير مكتوبة بـ Python. لكن بروتوكول pickle خاص بـ Python بالكامل، فحصر كل تلك البيانات المفيدة داخل pickle لن ينفعهم. إضافة إلى ذلك، ولأن حالة القائمة البريدية كلها كانت محتواة في config.pck، وأن Mailman له عمليات متعددة تحتاج إلى قراءة حالة القائمة البريدية وتعديلها وكتابتها، اضطررنا إلى تنفيذ أقفال حصرية قائمة على الملفات وآمنة مع NFS لضمان اتساق البيانات. وفي كل مرة يريد فيها أي جزء من Mailman تغيير حالة قائمة بريدية، عليه أن يأخذ القفل، ويكتب التغيير، ثم يُطلق القفل. وحتى عمليات القراءة قد تتطلب إعادة تحميل ملف config.pck للقائمة، لأن عملية أخرى قد تكون غيّرته قبل عملية القراءة. وقد تبيّن أن هذا التلسلسل للعمليات على القائمة البريدية بطيء وغير كفء إلى حد مؤلم.
ولهذه الأسباب، يخزّن Mailman 3 جميع بياناته في قاعدة بيانات SQL. ويُستخدم SQLite3 افتراضيًا، غير أن تغيير ذلك سهل لأن Mailman 3 يستخدم مُرسَم الكائنات العلائقية (Object Relational Mapper) المسمّى Storm، الذي يدعم مجموعة واسعة من قواعد البيانات. وقد أُضيف دعم PostgreSQL ببضعة أسطر من الشيفرة فقط، ويمكن لمدير الموقع تفعيله بتغيير متغيّر تهيئة واحد.
وهناك مشكلة أخرى وأكبر، وهي أن كل قائمة بريدية في Mailman 2 كانت صومعة (silo) منفصلة. فكثيرًا ما تمتد العمليات عبر قوائم بريدية كثيرة، بل عبر كلّها. فمثلًا قد يريد مستخدم أن يوقف اشتراكاته مؤقتًا أثناء ذهابه في إجازة. أو قد يريد مدير موقع أن يضيف إخلاء مسؤولية ما إلى رسالة الترحيب في كل القوائم البريدية على نظامه. بل إن الأمر البسيط المتمثل في معرفة القوائم البريدية التي مشترك فيها عنوان واحد كان يتطلب فكّ تخليل حالة كل قائمة بريدية على النظام، لأن معلومات العضوية كانت محفوظة في ملف config.pck أيضًا.
كانت المشكلة أخرى أن كل ملف config.pck يقع في مجلد سُمّي باسم القائمة البريدية، لكن Mailman صُمّم أصلًا دون مراعاة النطاقات الافتراضية (virtual domains). وقد أدّى هذا إلى مشكلة مؤسفة للغاية، إذ استحال على قائمتين بريديتين أن تحملا الاسم نفسه في نطاقين مختلفين. فمثلًا، إن كنت تملك النطاقين example.com وexample.org، وأردت أن يعملا بمعزل عن بعضهما وأن يسمح كل منهما بقائمة support مختلفة، فلا يمكنك فعل ذلك في Mailman 2 دون تعديل في الشيفرة، أو خطّاف (hook) بالكاد مدعوم، أو حلول التفافية اعتيادية تفرض اسم قائمة مختلفًا في الخفاء، وهو الأسلوب الذي تستخدمه المواقع الكبرى مثل SourceForge.
حُلَّت هذه المشكلة في Mailman 3 عبر تغيير طريقة تعريف القوائم البريدية، إلى جانب نقل جميع البيانات إلى قاعدة بيانات تقليدية. فـالمفتاح الأساسي (primary key) لجدول القوائم البريدية هو اسم القائمة المؤهَّل بالكامل (fully qualified list name)، أو بما لعلّمه على الأرجح عنوان النشر. وهكذا أصبحت support@example.com وsupport@example.org الآن صفوفًا مستقلة تمامًا في جدول القوائم البريدية، ويمكن أن يتعايشا بسهولة داخل نظام Mailman واحد.
10.3. المشغِّلات
تدفّق الرسائل عبر النظام عن طريق مجموعة من العمليات المستقلة المسمّاة المشغِّلات (runners). وقد صُمِّمت في الأصل لمعالجة كل ملفات الرسائل المخزّنة في مجلد بعينه على نحو يمكن التنبؤ به، أما اليوم فهناك بضعة مشغِّلات هي مجرد عمليات مستقلة طويلة التشغيل تؤدّي مهمة محدّدة ويديرها عملية رئيسية (master process)؛ وسنعود إلى ذلك لاحقًا. وحينما يكون المشغّل يدير ملفات في مجلد، تُسمّى مشغِّلات الطوابير.
Mailman ملتزم تمامًا أحادي الخيط (single-threaded)، رغم وجود توازٍ كبير يمكن استثماره. فمثلًا يستطيع Mailman قبول الرسائل من خادم البريد في الوقت نفسه الذي يرسل فيه رسائل إلى المستلمين، أو يعالج الرسائل المرتدّة، أو يؤرشف رسالة. ويتحقق التوازي في Mailman عبر استخدام عمليات متعددة، على هيئة هذه المشغِّلات. فمثلًا هناك مشغّل طابور الوارد (incoming) مهمّته الوحيدة قبول (أو رفض) الرسائل من خادم البريد الأعلى. وهناك مشغّل طابور الصادر (outgoing) مهمّته الوحيدة التواصل مع خادم البريد الأعلى عبر SMTP لإرسال الرسائل إلى المستلمين النهائيين. وهناك مشغّل طابور الأرشفة (archiver)، ومشغّل طابور لمعالجة الرسائل المرتدّة (bounce)، ومشغّل طابور لإعادة توجيه الرسائل إلى خادم NNTP، ومشغّل لتجميع المخلِّصات (digests)، وعدة غيرها. أما المشغِّلات التي لا تدير طابورًا فتُشمل خادم Local Mail Transfer Protocol وخادم HTTP إداريًا.
كل مشغّل طابور مسؤول عن مجلد واحد، أي طابوره. ومع أن نظام Mailman النموذجي يمكنه الأداء جيدًا تمامًا بعملية واحدة لكل طابور، فإننا نستخدم خوارزمية ذكية تتيح التوازي داخل مجلد طابور واحد، دون الحاجة إلى أي نوع من التعاون أو الأقفال. والسر يكمن في الطريقة التي نسمّي بها الملفات داخل مجلد الطابور.
كما ذُكر أعلاه، ترافق كل رسالة تتدفق عبر النظام أيضًا قاموس بيانات وصفية يراكم الحالة ويسمح للمكوّنات المستقلة في Mailman بالتواصل فيما بينها. وتستطيع مكتبة pickle في Python أن تسلسِل كائنات متعددة وتعيد تسلسلها إلى ملف واحد، لذا يمكننا تخليل شجرة كائنات الرسالة وقاموس البيانات الوصفية معًا في ملف واحد.
يوجد في Mailman صنف أساسي اسمه Switchboard يوفّر واجهة لإدراج (أي كتابة) شجرة كائنات الرسالة وقاموس البيانات الوصفية في ملفات ضمن مجلد طابور محدّد، وإزالتهما منه (أي قراءتهما). ولكل مجلد طابور نسخة واحدة على الأقل من switchboard، ولكل نسخة من مشغّل الطابور switchboard واحد بالضبط.
تنتهي كل ملفات pickle باللاحقة .pck، وإن كنت قد ترى أيضًا ملفات .bak و.tmp و.psv في الطابور. وتُستخدم هذه الملفات لضمان الركائز المقدسة (sacro-sanct) الاثنين في Mailman: ألّا يضيع أي ملف أبدًا، وألّا تُسلَّم أي رسالة أكثر من مرة أبدًا. لكن الأمور تعمل بصورة سليمة عادةً، وهذه الملفات نادرة إلى حد كبير.
وكما أُشير، للمواقع المزدحمة جدًا يدعم Mailman تشغيل أكثر من عملية مشغّل لكل مجلد طابور، بشكل متوازٍ تمامًا، دون أي اتصال بينها أو الحاجة إلى أقفال لمعالجة الملفات. ويقوم ذلك بتسميات ملفات pickle بدالة تجزئة SHA1، ثم السماح لمشغّل طابور واحد بأن يدير شريحة (slice) واحدة فقط من فضاء التجزئة. فإذا أراد موقع تشغيل مشغّلين على طابور الرسائل المرتدّة (bounces)، فإن أحدهما يعالج الملفات من النصف العلوي من فضاء التجزئة، ويعالج الآخر الملفات من النصف السفلي. وتُحسب دوال التجزئة باستخدام محتوى شجرة كائنات الرسالة المُخَلَّلة، واسم القائمة البريدية الموجَّه إليها الرسالة، وختمًا زمنيًا. ودوال تجزئة SHA1 عشوائية على نحو فعلي، ومن ثم يكون لدى مجلد طابور ذي مشغّلَين في المتوسط قدر متقارب من العمل لكل عملية. ولأن فضاء التجزئة يمكن تقسيمه بصورة ساكنة، يمكن لهذه العمليات أن تعمل على مجلد الطابور نفسه دون أي تداخل أو حاجة إلى اتصال.
لهذه الخوارزمية قيد مثير للاهتمام. وبما أن خوارزمية التقسيم تخصّص بتًا واحدًا أو أكثر من البت لكل شريحة، فإن عدد المشغِّلات لكل مجلد طابور يجب أن يكون قوة للعدد 2. وهذا يعني أنه يمكن وجود 1 أو 2 أو 4 أو 8 عمليات مشغّل لكل طابور، لكن ليس 5 مثلًا. ولم يكن هذا مشكلة في الممارسة قط، إذ نادرًا ما تحتاج أي موقع إلى أكثر من 4 عمليات للتعامل مع حِمله.
وهناك أثر جانبي آخر لهذه الخوارزمية سبّب مشكلات في التصميم المبكر لهذا النظام. فعلى الرغم من أن تسليم البريد الإلكتروني غير متوقع بطبعه، فإن أفضل تجربة للمستخدم تأتي بمعالجة ملفات الطابور بترتيب أول داخل أول خارج (FIFO)، بحيث تُرسَل الردود على القائمة البريدية بترتيب زمني تقريبي. وأي إهمال في بذل الجهد الأقصى للقيام بذلك قد يربك الأعضاء. لكن استخدام دوال تجزئة SHA1 كأسماء ملفات يمحو كل الأختام الزمنية، ولأسباب الأداء ينبغي تجنّب استدعاءات stat() على ملفات الطابور، أو فكّ تخليل محتواها (مثلًا، لقراءة ختم زمني في البيانات الوصفية).
كان حل Mailman هو توسيع خوارزمية تسمية الملفات لتشمل بادئة ختم زمني، تمثل عدد الثواني منذ حقبة يونكس (مثل +.pck). تبدأ كل دورة عبر مشغّل الطابور باستدعاء os.listdir() الذي يعيد جميع الملفات في مجلد الطابور. ثم يفصل اسم كل ملف ويتجاهل أي أسماء ملفات لا يطابق فيها دالة تجزئة SHA1 شريحة مسؤوليته. ثم يرتّب المشغّل الملفات المتبقية بحسب الجزء الزمني من اسم الملف. ومن الصحيح أن وجود عدة مشغِّلات طوابير، كل منها يدير شريحة مختلفة من فضاء التجزئة، قد يؤدي إلى مشكلات في الترتيب بين المشغِّلات المتوازية، لكن في الممارسة يكفي الترتيب الزمني للحفاظ على تصور المستخدم النهائي بالتسليم التسلسلي بأقصى جهد ممكن.
في الممارسة، عمل هذا على نحو ممتاز جدًّا لمدة عقد على الأقل، مع إصلاح خلل طفيف أحيانًا أو استكمال للتعامل مع حالات الركن النادرة وأنماط الفشل. وهو أحد أكثر أجزاء Mailman استقرارًا، وقد نُقل إلى حد كبير دون تغيير من Mailman 2 إلى Mailman 3.
10.4. المشغّل الرئيسي
مع كل عمليات المشغّل هذه، احتاج Mailman إلى طريقة بسيطة لبدء العمليات وإيقافها بصورة متسقة؛ ومن هنا وُلد المراقب الرئيسي (master watcher). ويجب أن يكون قادرًا على التعامل مع مشغِّلات الطوابير ومع المشغِّلات التي لا تدير طابورًا. فمثلًا في Mailman 3 نقبل الرسائل من خادم البريد الأعلى الوارد عبر LMTP، وهو بروتوكول يشبه SMTP لكنه يعمل للتسليم المحلي فقط، ومن ثم يمكن أن يكون أبسط بكثير إذ لا يحتاج إلى التعامل مع تعقيدات تسليم البريد عبر إنترنت غير متوقع. ويكتفي مشغّل LMTP بالاستماع على منفذ، في انتظار اتصال خادم البريد الأعلى به وإرسال تدفق بايتات إليه. ثم يحلل هذا التدفق إلى شجرة كائنات رسالة، وينشئ قاموس بيانات وصفية أوليًا، ويدرج هذا في مجلد طابور للمعالجة.
ولدى Mailman أيضًا مشغّل يستمع على منفذ آخر ويعالج طلبات REST عبر HTTP. وهذه العملية لا تتعامل مع ملفات الطابور إطلاقًا.
قد يكون نظام Mailman العامل نموذجيًا فيه ثماني أو عشر عمليات، وكلها تحتاج إلى الإيقاف والبدء على نحو ملائم ومريح. وقد تنهار أحيانًا؛ على سبيل المثال، حين يسبب خلل في Mailman استثناءً غير متوقع. وحين يحدث ذلك، تُحوَّل الرسالة الجاري تسليمها إلى منطقة انتظار (shunted)، مع حفظ حالة النظام لحظة الاستثناء في بيانات الرسالة الوصفية. ويضمن هذا ألا يؤدي الاستثناء غير الملتقط إلى تسليمات متعددة للرسالة. ونظريًا، يمكن لمدير موقع Mailman أن يصلح المشكلة، ثم يُخرج الرسائل المتعثرة من الانتظار (unshunt) لإعادة تسليمها، فيستأنف من حيث توقف. وبعد تحويل الرسالة المشكلية إلى الانتظار، يعيد المشغّل الرئيسي تشغيل مشغّل الطابور المنهار، فيبدأ بمعالجة الرسائل المتبقية في طابوره.
حين يبدأ المراقب الرئيسي، يبحث في ملف تهيئة ليحدد كم مشغّلًا من أي أنواع ينبغي أن يبدأ. وبالنسبة لمشغِّلي LMTP وREST، توجد عادةً عملية واحدة. أما مشغِّلو الطوابير، فكما ذُكر أعلاه، يمكن أن يكون عدد عملياتهم المتوازية قوة للعدد 2. ويقوم المشغّل الرئيسي بـ fork() وexec() جميع عمليات المشغِّلات وفقًا لملف التهيئة، ممرّرًا لكل منها وسائط سطر الأوامر المناسبة (مثلًا، لإخبار العملية الفرعية بأي شريحة من فضاء التجزئة تنظر إليها). ثم يستقر المشغّل الرئيسي في حلقة لا نهائية، محجوبًا حتى تخرج إحدى عملياته الفرعية. وهو يتتبع معرّف العملية لكل عملية فرعية، إلى جانب عدّاد لعدد مرات إعادة تشغيل تلك العملية. ويمنع هذا العدّ خللًا كارثيًا من التسبب في سلسلة متتالية من عمليات إعادة التشغيل التي لا يمكن إيقافها. وهناك متغيّر تهيئة يحدد عدد مرات إعادة التشغيل المسموح بها، وعند تجاوزه يُسجَّل خطأ ولا يُعاد تشغيل المشغّل.
حين تخرج إحدى العمليات الفرعية فعلًا، ينظر المشغّل الرئيسي إلى كل من رمز الخروج والإشارة التي قتلت العملية الفرعية. ويثبّت كل عملية مشغّل عددًا من معالِج الإشارات (signal handlers) بالمعاني التالية:
SIGTERM: إيقاف العملية الفرعية عمدًا. لا يُعاد تشغيلها. وSIGTERMهي الإشارة التي يقتل بهاinitالعملية عند تغيير مستويات التشغيل، وهي أيضًا الإشارة التي يستخدمها Mailman نفسه لإيقاف العملية الفرعية.SIGINT: تُستخدم أيضًا لإيقاف العملية الفرعية عمدًا، وهي الإشارة التي تحدث عند استخدام control-C في الصدفة (shell). لا يُعاد تشغيل المشغّل.SIGHUP: تطلب من العملية إغلاق ملفات السجل وفتحها من جديد، مع الاستمرار في التشغيل. وتُستخدم عند تدوير ملفات السجل.SIGUSR1: توقف العملية الفرعية في البداية، لكن تسمح للمشغّل الرئيسي بإعادة تشغيل العملية. وتُستخدم في أمرrestartفي سكربتات init.
يستجيب المشغّل الرئيسي أيضًا لهذه الإشارات الأربع جميعًا، لكنه لا يفعل أكثر من مجرد إعادة توجيهها إلى جميع عملياته الفرعية. فإن أرسلت SIGTERM إلى المشغّل الرئيسي، فإن جميع العمليات الفرعية ستتلقى SIGTERM وتخرج. وسيعلم المشغّل الرئيسي أن العملية الفرعية خرجت بسبب SIGTERM، وأنه توقف مقصود، فلا يعيد تشغيل المشغّل.
لضمان عدم تشغيل أكثر من مشغّل رئيسي واحد في أي وقت، يكتسب قفلًا (lock) عمره نحو يوم ونصف. ويثبّت المشغّل الرئيسي معالج SIGALRM يوقظ المشغّل الرئيسي مرة يوميًا كي يجدّد القفل. ولأن عمر القفل أطول من فترة الاستيقاظ، فلا ينبغي أن تنتهي صلاحية القفل أو ينكسر ما دام Mailman يعمل، إلا إذا تعطّل النظام بطبيعة الحال أو قُتل المشغّل الرئيسي بإشارة لا يمكن التقاطها. وفي تلك الحالات، توفّر واجهة سطر الأوامر لعملية المشغّل الرئيسي خيارًا لتجاوز قفل عتيق (stale).
وهذا يقودنا إلى الجزء الأخير من قصة المراقب الرئيسي، وهو واجهة سطر الأوامر له. يأخذ سكربت المشغّل الرئيسي الفعلي خيارات سطر أوامر قليلة جدًا. وهو مقصود به — وكذلك سكربتات مشغِّلات الطوابير — أن يبقى بسيطًا. لم يكن الأمر كذلك في Mailman 2، حيث كان سكربت المشغّل الرئيسي معقّدًا إلى حد ما ويحاول فعل الكثير، مما جعل فهمه وتصحيح أخطائه أصعب. أما في Mailman 3، فإن واجهة سطر الأوامر الحقيقية لعملية المشغّل الرئيسي موجودة في سكربت bin/mailman، وهو نوع من السكربتات الاشتراكية (meta-script) الذي يحتوي على عدد من الأوامر الفرعية، على النمط الذي شاعه برامج مثل Subversion. وهذا يقلّل عدد البرامج التي يلزم تثبيتها على PATH الخاص بصدفتك. ويمتلك bin/mailman أوامر فرعية لبدء المشغّل الرئيسي وإيقافه وإعادة تشغيله، وكذلك جميع العمليات الفرعية، وأيضًا لإجبار جميع ملفات السجل على أن تُفتح من جديد. ويتفرّع الأمر الفرعي start عبر fork() وexec() عملية المشغّل الرئيسي، بينما يرسل الباقي ببساطة الإشارة المناسبة إلى المشغّل الرئيسي، الذي يمرّرها بدوره إلى عملياته الفرعية كما وُصف أعلاه. وقد جعل هذا الفصل الأفضل بين المسؤوليات فهم كل قطعة على حدة أسهل بكثير.
10.5. القواعد والوصلات والسلاسل
يمرّر منشور في قائمة بريدية بعدة مراحل من لحظة استلامه أول مرة حتى لحظة إرساله إلى أعضاء القائمة. في Mailman 2، كانت كل خطوة معالجة ممثّلةً بـمعالِج (handler)، وكانت سلسلة من المعالجات تُجمَع معًا في خط أنابيب (pipeline). هكذا، حين تدخل رسالة إلى النظام، كان Mailman يحدد أولًا أي خط أنابيب سيُستخدم لمعالجتها، ثم يُستدعى كل معالِج في خط الأنابيب بالتناوب. فأدى بعض المعالجات وظائف إشراف (مثل: «هل يُسمح لهذا الشخص بالنشر في القائمة البريدية؟»)، وأدى آخرون وظائف تعديل (مثل: «أي الترويسات ينبغي أن أزيلها أو أضيفها؟»)، ونسخ آخرون الرسالة إلى طوابير أخرى. وفيما يلي بعض الأمثلة على هذه الأخيرة:
- ستُنسخ الرسالة المقبولة للنشر إلى طابور
archiverفي مرحلة ما، ليضيف مشغّل هذا الطابور الرسالة إلى الأرشيف. - ينبغي أن تنتهي إحدى نسخ الرسالة في نهاية المطاف في طابور
outgoing، لتُسلَّم إلى خادم البريد الأعلى، الذي يملك المسؤولية النهائية عن التسليم إلى عضو القائمة. - ينبغي أن توضع إحدى نسخ الرسالة في مخلّص (digest) لمن يريدون حركةً دورية متفرقة فقط من القائمة، لا رسالة فردية كلما أرسل أحدهم شيئًا.
أتضح أن بنية خط أنابيب المعالجات قوية إلى حد كبير. فقد وفّرت وسيلة سهلة يستطيع الناس بها توسيع Mailman وتعديله لتنفيذ عمليات مخصصة. وكانت واجهة المعالِج مباشرة إلى حد كبير، ولم يكن الأمر سوى مسألة بسيطة لتنفيذ معالِج جديد، ما دمت تضمن إضافته إلى خط الأنابيب الصحيح في الموضع الصحيح لإنجاز العملية المخصصة.
كانت إحدى مشكلات هذا أن مزج الإشراف والتعديل في خط أنابيب واحد يصبح مشكلة. إذ كان على المعالجات أن تُرتَّب في خط الأنابيب بترتيب معين، وإلا وقعت أمور غير متوقعة أو غير مرغوبة. فمثلًا، لو جاء المعالِج الذي يضيف ترويسات List-* وفق RFC 2369 بعد المعالِج الذي ينسخ الرسالة إلى مُجمِّع المخلِّصات، لتلقى الذين يستقبلون المخلِّصات نسخًا غير صحيحة من منشورات القائمة. وفي حالات مختلفة، قد يكون من الأفضل إشراف على الرسالة قبل تعديلها أو بعده. أما في Mailman 3، فقد فُصلت عمليتا الإشراف والتعديل إلى نظامين منفصلين لتحسين التحكم في الترتيب.
كما وُصف سابقًا، يحلل مشغّل LMTP تدفق بايتات واردة إلى شجرة كائنات رسالة وينشئ قاموس بيانات وصفية أوليًا للرسالة. ثم يدرجهما في مجلد طابور أو في آخر. وقد تكون بعض الرسائل أوامر بريدية (مثل: الانضمام إلى قائمة بريدية أو مغادرتها، أو الحصول على مساعدة آلية، إلخ) تُعالَج في طابور منفصل. وأغلب الرسائل منشورات في القائمة البريدية، وتُوضع هذه في طابور الوارد. ويعالج مشغّل الطابور الوارد كل رسالة بالتسلسل عبر سلسلة (chain) تتألف من أي عدد من الوصلات (links). وهناك سلسلة مدمجة تستخدمها معظم القوائم البريدية، غير أن هذه أيضًا قابلة للتهيئة.
يوضّح الشكل 10.3 مجموعة السلاسل الافتراضية في نظام Mailman 3. ويمثَّل كل وصل في السلسلة بمستطيل ذي زوايا مستديرة. والسلسلة المدمجة هي التي تُطبَّق فيها قواعد الإشراف الأولية على الرسالة الواردة، وفي هذه السلسلة يرتبط كل وصل بـقاعدة. والقواعد ليست سوى مقاطع من الشيفرة تُمرَّر إليها الوسائط المعتادة الثلاثة: القائمة البريدية، وشجرة كائنات الرسالة، وقاموس البيانات الوصفية. والقواعد لا يُفترض بها أن تعدّل الرسالة؛ فهي تتخذ قرارًا ثنائيًا وتعيد قيمة منطقية تجيب عن السؤال: «هل طابقت القاعدة أم لا؟». ويمكن للقواعد أيضًا أن تسجّل معلومات في قاموس البيانات الوصفية.
في الشكل، تشير الأسهم الصلبة إلى تدفق الرسالة عند مطابقة القاعدة، بينما تشير الأسهم المنقّطة إلى تدفق الرسالة عند عدم مطابقتها. وتُسجَّل نتيجة كل قاعدة في قاموس البيانات الوصفية بحيث يعرف Mailman لاحقًا (ويستطيع أن يُبلّغ عنه) بدقة أي القواعد طابقت وأيها لم تطابق. أما الأسهم المتقطعة فتشير إلى انتقالات تُتَّخذ دون شرط، بغض النظر عن مطابقة القاعدة أو عدمها.
الشكل 10.3: عرض مبسّط للسلاسل الافتراضية ووصلاتها
من المهم ملاحظة أن القواعد نفسها لا توجّه بناءً على النتيجة. فالسلسلة المدمجة، يرتبط فيها كل وصل بـإجراء يُنفَّذ عند مطابقة القاعدة. فمثلًا، حين تطابق قاعدة "loop" (أي أن القائمة البريدية رأت هذه الرسالة من قبل)، تُسلَّم الرسالة فورًا إلى سلسلة "discard" التي تتخلص منها بعد إجراء بعض المحاسبات. وإذا لم تطابق قاعدة "loop"، فستعالج الوصلة التالية في السلسلة الرسالة.
في الشكل 10.3، لا يوجد للوصلات المرتبطة بقواعد "administrivia" و"max-size" و"truth" أي قرار ثنائي. وفي حالتي الأولى والثانية، السبب أن إجراءهما مؤجَّل (deferred)، لذا فإنهما يسجّلان نتيجة المطابقة فحسب وتستمر المعالجة إلى الوصلة التالية. ثم تطابق قاعدة "any" إذا طابقت أي قاعدة سابقة. وبهذه الطريقة يستطيع Mailman أن يُبلّغ عن جميع الأسباب التي تمنع نشر رسالة، لا عن السبب الأول وحده. وهناك عدة قواعد أخرى من هذا النوع لم تُوضَّح هنا للتبسيط.
قاعدة "truth" مختلفة بعض الشيء. فهي مرتبطة دائمًا بالوصلة الأخيرة في السلسلة، وهي تطابق دائمًا. ومع اقتران ذلك بقاعدة "any" التي قبل الأخيرة والتي تحيّد كل الرسائل التي طابقت سابقًا، تعرف الوصلة الأخيرة عندئذٍ أن أي رسالة تصل إلى هذه المرحلة مسموح بنشرها في القائمة البريدية، فينقلها دون شرط إلى سلسلة "accept".
هناك بضعة تفاصيل أخرى في معالجة السلاسل لم تُشرح هنا، لكن البنية مرنة وقابلة للتوسيع إلى حد بعيد بحيث يمكن تنفيذ أي نوع من معالجة الرسائل تقريبًا، ويمكن للمواقع تخصيص القواعد والوصلات والسلاسل وتوسيعها.
وماذا يحدث للرسالة حين تصل إلى سلسلة "accept"؟ الرسالة، التي صارت الآن مناسبة للقائمة البريدية، تُرسَل إلى طابور خط الأنابيب لتجري عليها بعض التعديلات قبل تسليمها إلى المستلمين النهائيين. وتُشرح هذه العملية بمزيد من التفصيل في القسم التالي.
تضع سلسلة "hold" الرسالة في دلو خاص ليضعها المشرف البشري مراجعتها. وتقوم سلسلة "moderation" بقدر بسيط من المعالجة الإضافية لتقرر ما إذا كان ينبغي قبول الرسالة، أو تعليقها بانتظار موافقة المشرف، أو التخلص منها، أو رفضها. وللتقليل من ازدحام الرسم البياني، لم تُوضَّح سلسلة "reject" المستخدمة في ارتداد الرسائل إلى المرسل الأصلي.
10.6. المعالجات وخطوط الأنابيب
ما إن تصل الرسالة عبر السلاسل والقواعد وتُقبل للنشر حتى تحتاج إلى معالجة إضافية قبل أن يتسنى تسليمها إلى المستلمين النهائيين. فمثلًا، قد تُضاف بعض الترويسات أو تُحذف، وقد تكتسب بعض الرسائل زخارف إضافية تقدّم إخلاءات مسؤولية أو معلومات مهمة، مثل كيفية مغادرة القائمة البريدية. وتُنفَّذ هذه التعديلات بواسطة خط أنابيب يحتوي على تسلسل من المعالجات. وعلى نحو يشبه السلاسل والقواعد، فإن خطوط الأنابيب والمعالجات قابلة للتوسيع، غير أن هناك عددًا من خطوط الأنابيب المدمجة للحالات الشائعة. وواجهة المعالج شبيهة بواجهة القواعد، إذ تقبل قائمة بريدية وكائن رسالة وقاموس بيانات وصفية. غير أن المعالجات، بخلاف القواعد، تستطيع وتعدّل الرسالة فعلًا. ويوضّح الشكل 10.4 خط الأنابيب الافتراضي ومجموعة المعالجات (وقد أُهملت بعض المعالجات للتبسيط).
الشكل 10.4: معالجات طابور خط الأنابيب
على سبيل المثال، تحتاج الرسالة المنشورة إلى إضافة ترويسة Precedence:، وهي تخبر بقية البرمجيات الآلية بأن هذه الرسالة أتت من قائمة بريدية. وهذه الترويسة معيار فعلي لمنع برامج الإجازات من الرد على القائمة البريدية. تتم إضافة هذه الترويسة (من بين تعديلات ترويسات أخرى) بواسطة معالج "add headers". وبخلاف القواعد، لا يهم ترتيب المعالجات عادةً، وتمر الرسائل دائمًا بجميع معالجات خط الأنابيب.
ترسل بعض المعالجات نسخًا من الرسالة إلى طوابير أخرى. وكما هو مبين في الشكل 10.4، هناك معالج يصنع نسخة من الرسالة لمن يريدون تلقي المخلِّصات. وتُرسَل النسخ أيضًا إلى طابور الأرشيف تمهيدًا لتسليمها إلى أرشيفات القائمة البريدية. وأخيرًا، تُنسخ الرسالة إلى الطابور الصادر من أجل التسليم النهائي إلى أعضاء القائمة البريدية.
10.7. VERP
تُقصد بـ VERP اختصارًا لعبارة Variable Envelope Return Path، وهي تقنية معروفة تستخدمها القوائم البريدية لتحديد عناوين المستلمين المرتدّة دون التباس. وحين يعجز عنوان في قائمة بريدية عن العمل، يرسل خادم بريد المستلم إشعارًا إلى المرسل. أما في حالة القائمة البريدية، فأنت تريد أن يعود هذا الارتداد إلى القائمة البريدية لا إلى مؤلف الرسالة الأصلي؛ فالمؤلف لا يستطيع فعل شيء حيال الارتداد، والأسوأ من ذلك أن إعادة إرسال الارتداد إلى المؤلف قد تفضي عن معلومات عن من مشترك في القائمة البريدية. أما حين تصل القائمة البريدية إلى الارتداد، فيمكنها أن تفعل شيئًا مفيدًا، مثل تعطيل العنوان المرتدّ أو إزالته من عضوية القائمة.
هناك مشكلتان عامتان في هذا. أولًا، مع وجود تنسيق قياسي لهذه الارتدادات (المسمّى إشعارات حالة التسليم)، فإن خوادم البريد المنشورة كثيرًا ما لا تلتزم به. وبدلًا من ذلك، يمكن أن يحتوي جسم رسائل الارتداد عندها على أي قدر تقريبًا من الخلط الصعب التحليل آليًا، مما يجعل التحليل الآلي صعبًا. وفي الواقع، يستخدم Mailman مكتبة تضم عشرات الاستدلالات (heuristics) حول صيغ الارتداد، وقد ظهرت جميعها في العالم الحقيقي خلال الخمسة عشر عامًا من وجود Mailman.
ثانيًا، تخيّل الحالة التي يكون فيها عضو في قائمة بريدية لديه عدة عمليات إعادة توجيه. قد تكون مشترَكة في القائمة بعنوانها anne@example.com، لكن هذا العنوان قد يعيد التوجيه إلى person@example.org، الذي قد يعيد توجيه الرسالة بدوره إلى me@example.net. وحين يستقبل خادم الوجهة النهائي في example.net الرسالة، سيكتفي عادةً بإرسال ارتداد يقول إن me@example.net لم يعد صالحًا. لكن خادم Mailman الذي أرسل الرسالة يعرف العضو باسم anne@example.com فقط، لذا فإن ارتدادًا يشير إلى me@example.net لن يحتوي عنوانًا مشتركًا به، وسيتجاهله Mailman.
هنا يأتي VERP، الذي يستغل أحد متطلبات بروتوكول SMTP الأساسي لتوفير كشف واضح لا لبس فيه للارتدادات، بإعادة إرسال رسائل الارتداد من هذا النوع إلى مرسل المظروف (envelope sender). وهذا ليس حقل From: في جسم الرسالة، بل في الواقع قيمة MAIL FROM التي تُضبط أثناء حوار SMTP. وهي تبقى محفوظة طوال مسار التسليم، والمعيارات تشترط من خادم البريد المستقبِل النهائي أن يرسل الارتدادات إلى هذا العنوان. ويستخدم Mailman هذه الحقيقة لترميز عنوان البريد الإلكتروني الأصلي للمستلم داخل قيمة MAIL FROM.
إذا كان خادم Mailman هو mylist@example.org، فإن مرسل المظروف المرمَّز بـ VERP لمنشور في قائمة بريدية أُرسل إلى anne@example.com سيكون:
mylist-bounce+anne=example.com@example.org
هنا، + هو فاصل للعناوين المحلية (local address separator)، وهو تنسيق تدعمه معظم خوادم البريد الحديثة. فحين يعود الارتداد، سيُسلَّم فعليًا إلى mylist-bounce@example.com لكن مع بقاء ترويسة To: مضبوطة على عنوان المستلم المرمَّز بـ VERP. ويستطيع Mailman عندئذٍ تحليل ترويسة To: هذه لفكّ ترميز المستلم الأصلي بوصفه anne@example.com.
رغم أن VERP أداة قوية للغاية للتخلص من العناوين السيئة في القائمة البريدية، فإن له عيبًا واحدًا محتمل الأهمية. فتستلزم استخدام VERP أن يرسل Mailman نسخة واحدة بالضبط من الرسالة لكل مستلم. وبدون VERP، يستطيع Mailman تجميع نسخ متطابقة من رسالة صادرة إلى عدة مستلمين، مما يقلّل من عرض النطاق الكلي وزمن المعالجة. لكن VERP يتطلب قيمة MAIL FROM فريدة لكل مستلم، والطريقة الوحيدة لتحقيق ذلك هي إرسال نسخة فريدة من الرسالة. وعمومًا فإن هذا مقايضة مقبولة، بل في الواقع، ما دامت هذه الرسائل المخصَّصة تُرسَل أصلًا لأغراض VERP، فهناك أشياء كثيرة مفيدة يستطيع Mailman فعلها أيضًا. فمثلًا يستطيع تضمين عنوان URL في تذييل الرسالة مخصَّصًا لكل مستلم يمنحه رابطًا مباشرًا لإلغاء اشتراكه من القائمة. بل يمكنك حتى تصور عمليات دمج البريد (mail-merge) متنوعة لتخصيص جسم الرسالة لكل مستلم على حدة.
10.8. REST
تغدو أحد أهم التغييرات المعمارية في Mailman 3 استجابة لطلب متكرر عبر السنين: السماح لـ Mailman بالتكامل بصورة أسهل مع الأنظمة الخارجية. حين عيَّنني Canonical، الراعي المؤسسي لمشروع Ubuntu، عام 2007، كانت مهمتي في الأصل إضافة قوائم بريدية إلى Launchpad، وهي منصة تعاون واستضافة لمشاريع البرمجيات. كنت أعلم أن Mailman 2 قادر على أداء المهمة، غير أن هناك شرطًا باستخدام واجهة مستخدم Launchpad على الويب بدلًا من واجهة مستخدم Mailman الافتراضية. ولأن قوائم Launchpad البريدية كانت ستكون قوائم نقاش في أغلب الأحوال، كنا نريد تنوّعًا ضئيلًا جدًا في طريقة عملها. فمديرو القوائم لن يحتاجوا إلى الكمية الكبيرة من الخيارات المتاحة في موقع Mailman المعتاد، وستُعرَض الخيارات القليلة التي يحتاجونها عبر واجهة مستخدم Launchpad على الويب.
في ذلك الحين، لم يكن Launchpad برمجية حرة (وهذا تغيّر عام 2009)، لذا اضطررنا إلى تصميم التكامل بحيث لا يمكن أن تنتقل شيفرة Mailman 2 المرخّصة بـ GPLv2 إلى Launchpad. وقد أدّى هذا إلى عدد من القرارات المعمارية أثناء تصميم ذلك التكامل كانت معقّدة نوعًا ما وغير فعّالة إلى حد ما. ولأن Launchpad أصبح الآن برمجية حرة مرخّصة بموجب AGPLv3، فلن تكون هذه الحيل ضرورية اليوم، لكن اضطرارنا إلى هذا الأسلوب قدم دروسًا بالغة القيمة حول كيفية تكامل Mailman بلا واجهة مستخدم على الويب مع الأنظمة الخارجية. والرؤية التي نتجت كانت عن محرك أساسي ينفّذ عمليات القوائم البريدية بكفاءة وموثوقية، ويمكن تديره بأي نوع من واجهات الويب الأمامية، بما فيها المكتوبة بـ Zope أو Django أو PHP، أو بلا واجهة مستخدم على الويب البتة.
كانت هناك تقنيات عديدة في ذلك الحين تتيح ذلك، وفي الواقع يستند تكامل Mailman مع Launchpad إلى XMLRPC. لكن لدى XMLRPC عدد من المشكلات التي تجعله بروتوكولًا أقل من مثالي.
تبنّى Mailman 3 نموذج نقل حالة التمثيل (Representational State Transfer) من أجل التحكم الإداري الخارجي. ويقوم REST على HTTP، والتمثيل الافتراضي لكائنات Mailman هو JSON. وهذان البروتوكولان شائعان ومدعومان جيدًا في مجموعة واسعة جدًا من لغات البرمجة والبيئات، مما يجعل دمج Mailman مع أنظمة الأطراف الثالثة سهلًا إلى حد معقول. كان REST الملاءمة المثالية لـ Mailman 3، وأصبح الآن جزء كبير من وظيفته معرَّضًا عبر REST API.
هذه نموذج قوي ينبغي أن تتبناه تطبيقات أكثر: تسلِّم محركًا أساسيًا ينفّذ وظيفته الأساسية جيدًا، معرّضًا REST API للاستعلام عنه والتحكم فيه. ويوفّر REST API طريقة أخرى أيضًا للتكامل مع Mailman، وتتمثل الطرق الأخرى في استخدام واجهة سطر الأوامر، وكتابة شيفرة Python للوصول إلى API الداخلي. وهذه البنية مرنة للغاية ويمكن استخدامها والتكامل معها بطرق تتجاوز الرؤية الأولية لمصممي النظام.
لا يتيح هذا التصميم خيارات نشر أكبر وحسب، بل سمح حتى للمكوّنات الرسمية للنظام بأن تُصمَّم وتُنفَّذ بصورة مستقلة. فعلى سبيل المثال، واجهة المستخدم الرسمية الجديدة على الويب لـ Mailman 3 هي تقنيًا مشروع منفصل له قاعدة شيفرة خاصة به، يقوده في المقام الأول مصممو واجهات ويب ذوو خبرة. هؤلاء المطوّرون المتميزون مُخوَّلون لاتخاذ القرارات وإنشاء التصاميم وتنفيذ التنفيذات دون أن يكون تطوير المحرك الأساسي اختناقًا. وعمل واجهة المستخدم على الويب يعود ليغذّي تنفيذ المحرك الأساسي بطلب وظائف إضافية معرَّضة عبر REST API، لكن ليس عليهم انتظارها، إذ يمكنهم تجهيز نموذج أولي لجهة الخادم لديهم ومتابعة التجريب وتطوير واجهة المستخدم على الويب بينما يلحق المحرك الأساسي بالركب.
نخطط لاستخدام REST API في أمور كثيرة أخرى، منها السماح بكتابة السكربتات للعمليات الشائعة والتكامل مع خوادم IMAP أو NNTP للحصول على وصول بديل إلى الأرشيفات.
10.9. التعريب
كان GNU Mailman من أوائل برامج Python التي تبنّت التعريب (internationalization). وبالطبع، بما أن Mailman لا يعدّل عادةً محتوى رسائل البريد الإلكتروني المنشورة عبره، فيمكن أن تكون تلك الرسائل بأي لغة يختارها مؤلفها الأصلي. غير أن المستخدمين، عند التفاعل مباشرةً مع Mailman، سواء عبر واجهة الويب أو عبر الأوامر البريدية، يفضّلون استخدام لغتهم الطبيعية.
كان Mailman رائدًا في كثير من تقنيات التعريب المستخدمة في عالم Python، لكنه في الواقع أعقد بكثير من معظم التطبيقات. فبيئة سطح المكتب النموذجية، تُختار اللغة الطبيعية عند تسجيل دخول المستخدم وتبقى ثابتة طوال جلسة سطح المكتب. أما Mailman فهو تطبيق خادم، لذا لا بد له من التعامل مع عشرات اللغات، بمعزل عن لغة النظام الذي يعمل عليه. وفي الواقع، لا بد أن يحدد Mailman سياق اللغة (language context) الذي ستعاد الاستجابة بمقتضاه، وأن يترجم نصها إلى تلك اللغة. وأحيانًا قد تتضمن الاستجابة لغات متعددة؛ فمثلًا، إذا كانت رسالة ارتداد من مستخدم ياباني ستُعاد توجيهها إلى مديري قوائم يتحدثون الألمانية والإيطالية والكتالونية.
وهنا أيضًا، كان Mailman رائدًا في بعض تقنيات Python الأساسية للتعامل مع سياقات اللغة المعقدة من هذا النوع. فهو يستخدم مكتبة تدير مكدّسًا من اللغات يمكن الدفع به إلى المكدّس وسحبه منه مع تغيّر السياق، حتى في أثناء معالجة رسالة واحدة. كما ينفّذ مخططًا معقّدًا لتخصيص قوالب استجاباته بناءً على تفضيلات الموقع وتفضيلات مالكي القوائم واختيار اللغة. فمثلًا، إذا أراد مالك قائمة تخصيص قالب استجابة لأحد قوائمه، ولكن للمستخدمين اليابانيين فقط، لوضع القالب المحدد في الموضع المناسب على نظام الملفات، وهذا ما يتجاوز القيم الافتراضية الأكثر عمومية.
10.10. الدروس المستفادة
بينما قدّم هذا المقال نظرة عامة على بنية Mailman 3 ورؤية لكيفية تطوّر تلك البنية عبر الخمسة عشر عامًا من وجوده (عبر ثلاث عمليات إعادة كتابة كبرى)، هناك قرارات معمارية مثيرة أخرى كثيرة في Mailman لا أستطيع تغطيتها. وتشمل هذه النظام الفرعي للتهيئة، وبنية الاختبارات، وطبقة قاعدة البيانات، والاستخدام البرمجي للواجهات الرسمية، والأرشفة، وأنماط القوائم البريدية، والأوامر البريدية وواجهة سطر الأوامر، والتكامل مع خادم البريد الصادر. تواصل معنا عبر قائمة البريد mailman-developers إذا كنت مهتمًا بمزيد من التفاصيل.
إليك بعض الدروس التي تعلّمناها أثناء إعادة كتابة جزء رائج ومستقر وراسخ من منظومة المصادر المفتوحة.
- استخدم التطوير المُوجَّه بالاختبارات (TDD). لا توجد حقًا أي طريقة أخرى! يفتقر Mailman 2 إلى مجموعة اختبارات آلية إلى حد كبير، ومع أنه صحيح أن قاعدة شيفرة Mailman 3 ليست مشمولة بالكامل بمجموعة اختباراتها، فإن معظمها مشمول بها، ويُشترط أن يرافق كل شيفرة جديدة اختبارات، سواء باستخدام
unittestsأوdoctests. وتطبيق TDD هو الطريقة الوحيدة لاكتساب الثقة بأن التغييرات التي تجريها اليوم لا تُدخل ارتدادات (regressions) في الشيفرة القائمة. ونعم، قد يستغرق TDD وقتًا أطول أحيانًا، لكن فكّر فيه بوصفه استثمارًا في جودة شيفرتك مستقبلًا. وبهذا المعنى، فإن عدم وجود مجموعة اختبارات جيدة لا يعني سوى أنك تهدر وقتك. وتذكّر العبارة: الشيفرة غير المختبَرة شيفرة معطوبة. - حسم مسألة البايتات/النصوص من البداية. في Python 3، يُرسَم تمييز حاد بين نصوص Unicode ومصفوفات البايت، وهو ما يمثل — وإن كان مؤلمًا في البداية — فائدة هائلة لكتابة شيفرة صحيحة. أما Python 2 فقد موّه هذا الخط بامتلاكه نصوص Unicode ونصوص ASCII ذات 8 بت معًا، مع بعض التحويات الآلية بينهما. وبينما بدا ذلك وسيلة مريحة مفيدة، فإن المشكلات الناتجة عن هذا الخط الضبابي هي السبب الأول للأخطاء في Mailman 2. ولا يخفف من الأمر أن البريد الإلكتروني مشهور بصعوبة تصنيفه إلى نصوص وبايتات. فتقنيًا، تمثيل البريد الإلكتروني على السلك هو تسلسل من البايتات، غير أن هذه البايتات تكاد تكون دائمًا ASCII، ويكون هناك إغراء قوي بمعالجة مكوّنات الرسالة كنص. وتصف معايير البريد الإلكتروني نفسها كيفية الترميز الآمن للنص المقروء من الإنسان وغير ASCII، لذا حتى أمور مثل البحث عن بادئة
Re:في ترويسةSubject:ستكون عمليات نصية لا عمليات بايتية. ومبدأ Mailman هو تحويل جميع البيانات الواردة من البايتات إلى Unicode في أبكر وقت ممكن، والتعامل مع النص بوصفه Unicode داخليًا، وعدم تحويله إلى بايتات إلا في طريقه إلى الخارج. ومن الضروري الوضوح التام منذ البداية حول متى تتعامل مع البايتات ومتى تتعامل مع النص، لأن إضافة هذا التحوّل النموذجي الأساسي إلى نظام قائم لاحقًا تصعب إلى حد بالغ. - تعريب تطبيقك من البداية. هل تريد أن يقتصر استخدام تطبيقك على الأقلية من العالم التي تتحدث الإنجليزية؟ فكّر كم مستخدمًا رائعًا تتجاهله! ليس من الصعب إعداد التعريب، وهناك الكثير من الأدوات الجيدة التي تجعل هذا سهلًا، عداها كثير مما ابتكره Mailman. لا تقلق بشأن الترجمات في البداية؛ فإذا كان تطبيقك متاحًا لثروة لغات العالم، فسيكون لديك متطوعون للترجمة يقفون على بابك ليساعدوك.
GNU Mailman مشروع نابض الحياة بقاعدة مستخدمين سليمة، وبه فرص كثيرة للإسهام. وفيما يلي بعض الموارد التي يمكنك استخدامها إن شعرت برغبة في مساندتنا، وهو ما أرجو أن تكون تملكه!
- الموقع الإلكتروني الرئيسي
- ويكي المشروع
- قائمة بريد المطوّرين
- قائمة بريد المستخدمين
- قناة IRC على Freenode:
#mailman
كلمة أخيرة
بينما كان هذا الفصل قيد الكتابة، علمنا بحزن عن وفاة Tokio Kikuchi (http://wiki.list.org/display/COM/TokioKikuchi)، وهو أستاذ ياباني أسهم إسهامًا كبيرًا في Mailman، وكان يتمتع بمعرفة خاصة بآليات التعريب وخصوصيات برامج البريد اليابانية. وسيُشتاق إليه كثيرًا.