إدارة المهام التابعة

عند النظر إلى الصفحات السابقة، يتم تكرار المظهر مرارًا وتكرارًا، فإدارة الرمز الخاص بك سهلة وبسيطة إلى حدٍّ ما، ولكن إدارة الاعتماديات عليها أكثر صعوبة. هناك جميع أنواع الاعتماديات: في بعض الأحيان، يعتمد ذلك على مهمة معيّنة (مثل "إرسال المستندات قبل وضع علامة على إصدار كمكتمل") واعتمادًا أحيانًا على أحد العناصر (مثل "أحتاج إلى الحصول على أحدث إصدار من مكتبة الرؤية الحاسوبية لإنشاء الرمز الخاص بي"). وفي بعض الأحيان، يكون لديك اعتمادية داخلية على جزء آخر من قاعدة الرمز الخاص بك أو يكون لديك فريق آخر تابع إلى نظامك الأساسي أو إلى قاعدة بيانات أخرى أو إلى قاعدة خارجية، أو كان لديك اعتماد داخلي على جزء من مجموعة الرموز الخاصة بك أو إلى قاعدة بيانات أخرى أو إلى شبكة أخرى. ولكن على أي حال، إنّ فكرة "احتجت إلى ذلك قبل أن أتمكن من الحصول على هذا" هو أمر يتكرّر في تصميم أنظمة التصميم، وقد تكون إدارة الاعتماديات هي المهمة الأساسية في نظام التصميم.

التعامل مع الوحدات والاعتماديات

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

استخدام وحدات دقيقة الحبل والقاعدة 1:1

والسؤال الأول الذي يتم التوصّل إليه عند إنشاء بنية مستندة إلى عناصر فنية هو تحديد مقدار الوظائف التي يجب أن تتضمنها الوحدة الفردية. في Bazel، يتم تمثيل وحدة باستهداف يحدّد وحدة قابلة للإصدار، مثل java_library أو go_binary. في المرحلة القصوى، يمكن تضمين المشروع بالكامل في وحدة واحدة من خلال وضع ملف BUILD واحد في الجذر ودمج جميع ملفات المصدر الخاصة بهذا المشروع بشكل متكرر. ومن ناحية أخرى، يمكن إنشاء كل ملف مصدر تقريبًا في وحدته الخاصة، ما يتطلب من كل ملف إدراجه في ملف BUILD كل ملف آخر يعتمد عليه.

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

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

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

تتوفّر بعض هذه الأدوات، مثل buildifier وbuildozer، مع خدمة Bazel في دليل buildtools.

تصغير مستوى رؤية الوحدة

وتتيح Bazel وأنظمة الإنشاء الأخرى لكل هدف تحديد مستوى الرؤية، وهو موقع يحدّد الاستهدافات الأخرى التي يمكن أن تعتمد عليها. لا يمكن الإشارة إلى هدف خاص إلا داخل ملف BUILD الخاص به. وقد يمنح الاستهداف مستوى رؤية أوسع لاستهدافات قائمة محدّدة صراحةً من ملفات BUILD، أو في حال مستوى الرؤية العلني، لكل هدف في مساحة العمل.

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

إدارة العناصر التابعة

يجب أن تتمكّن الوحدات من الرجوع إلى بعضها. ويتمثل الجانب السلبي في تقسيم قاعدة الرمز إلى وحدات دقيقة بالغة الدقة في أنه عليك إدارة المهام التابعة ضمن هذه الوحدات (على الرغم من أن الأدوات يمكنها المساعدة في تنفيذ ذلك تلقائيًا). ويؤدي عادةً التعبير عن هذه العناصر التابعة إلى أن يكون الجزء الأكبر من المحتوى في ملف BUILD.

المهام التابعة الداخلية

في مشروع كبير مُقسَّم إلى وحدات دقيقة جدًا، من المرجّح أن تكون معظم التبعيات داخلية، أي في هدف آخر محدّد ومصمّم في مستودع المصدر نفسه. تختلف العناصر التابعة الداخلية عن العناصر التابعة الخارجية في أنّها مبنية من مصدر بدلاً من تنزيلها كعناصر تم إنشاؤها مسبقًا أثناء تشغيل الإصدار. ويعني هذا أيضًا أنه لا يتوفّر مفهوم "الإصدار" للاعتماديات الداخلية، فالهدف دائمًا يتم إنشاء جميع التبعيات الداخلية له بالتزامن/المراجعة نفسها في المستودع. إحدى المشاكل التي يجب التعامل معها بعناية فيما يتعلق بالاعتماديات الداخلية هي كيفية التعامل مع العناصر التابعة الانتقالية (الشكل 1). لنفترض أن الهدف "أ" يعتمد على الهدف "ب"، الذي يعتمد على هدف المكتبة المشترك "ج". هل يجب أن يكون الاستهداف "أ" قادرًا على استخدام الفئات المحددة في الهدف "ج"؟

المهام التابعة الانتقالية

الشكل 1. المهام التابعة الانتقالية

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

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

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

المهام التابعة الخارجية

إذا لم تكن الاعتمادية داخلية، يجب أن تكون خارجية. المهام التابعة الخارجية هي العناصر التي يتم إنشاؤها وتخزينها خارج نظام الإصدار. يتم استيراد الاعتمادية مباشرةً من مستودع العناصر (الذي يتم الوصول إليه عادةً على الإنترنت) ويتم استخدامه كما هو بدلاً من إنشاؤه من المصدر. من أكبر الاختلافات بين تبعيات المستخدم الداخلية والخارجية أن تبعيات خارجية تؤدي إلى نُسخ، وتظهر هذه النُسخ بشكل مستقل عن رمز مصدر المشروع.

إدارة الاعتمادية التلقائية مقابل الإدارة اليدوية

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

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

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

قاعدة الإصدار الواحد

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

أكبر مشكلة في السماح بإصدارات متعددة هي مشكلة اعتمادية معيّنة. لنفترض أن الهدف "أ" يعتمد على الهدف "ب" وعلى الإصدار 1 من مكتبة خارجية. إذا تمت إعادة استخدام target B لاحقًا لإضافة اعتمادية على الإصدار 2 من المكتبة الخارجية نفسها، سيتعطّل الهدف A لأنّه يعتمد الآن بشكل ضمني على إصدارَين مختلفَين من المكتبة نفسها. في الواقع، ليس من الآمن إضافة اعتمادية جديدة من هدف إلى أي مكتبة تابعة لجهة خارجية لها إصدارات متعدّدة، لأنّ أيًّا من مستخدمي هذا الاستهداف قد يعتمد على إصدار مختلف. إنّ اتّباع قاعدة الإصدار الواحد يجعل هذا التضارب مستحيلاً، فإذا أضاف هدفٌ اعتمادًا على مكتبة تابعة لجهة خارجية، ستكون أي تبعيات حالية موجودة على تلك النسخة نفسها، وبالتالي يمكنها العمل معًا بسعادة.

المهام التابعة الخارجية الانتقالية

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

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

ولهذا السبب، لا يُنزِّل تطبيق Bazel المهام الملحقة بشكل تلقائي. للأسف، لا تتوفّر تعداد نقطي من المستوى الفضّي، بل أنّ خيار Bazel هو طلب ملف عالمي يدرج كل اعتمادية خارجية للمستودع ونسخة صريحة منه تُستخدَم للحصول على هذا الاعتمادية في جميع مستودعه. ولحسن الحظ، توفّر Bazel أدوات يمكنها تلقائيًا إنشاء هذا الملف الذي يحتوي على المهام التابعة الانتقالية لمجموعة من عناصر Maven. يمكن تشغيل هذه الأداة مرة واحدة لإنشاء ملف WORKSPACE الأوليّ لمشروع، ويمكن بعد ذلك تعديل هذا الملف يدويًا لضبط إصدارات كل اعتمادية.

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

تخزين نتائج الإنشاء مؤقتًا باستخدام المهام التابعة الخارجية

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

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

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

أمان المهام التابعة الخارجية وموثوقيتها

واستنادًا إلى العناصر الواردة من مصادر تابعة لجهات خارجية، يمثّل ذلك خطرًا بطبيعته. هناك خطر من مدى التوفّر إذا توقف مصدر الجهة الخارجية (مثل مستودع العناصر)، لأن الإصدار بالكامل قد يؤدي إلى توقّف إذا تعذّر عليه تنزيل اعتمادية خارجية. هناك أيضًا خطر أمني: إذا اخترق أحد المهاجمين نظام الجهة الخارجية، يمكن أن يستبدل العنصر المشار إليه بتصميم خاص به، ما يسمح له بإدخال رمز عشوائي في الإصدار الذي تستخدمه. ويمكن الحدّ من هاتين المشكلتين من خلال نسخ أي قطع أثرية تعتمد عليها على خوادم تتحكّم فيها وتحظر نظام الإصدار الخاص بك من الوصول إلى مستودعات العناصر الخارجية مثل Maven Central. وتتمثّل المفاضلة في أنّ هذه المرايا تستغرق جهودًا وموارد للحفاظ عليها، لذا يعتمد اختيار استخدامها في كثير من الأحيان على حجم المشروع. ويمكن أيضًا منع المشكلة الأمنية تمامًا مع بذل قدر قليل من الجهد من خلال طلب تجزئة التجزئة لكل عنصر من العناصر الخارجية في مستودع المصدر، ما يؤدي إلى تعذُّر الإصدار إذا تم التلاعب بالعنصر. وهناك حل بديل آخر يتم من خلاله حلّ المشكلة تمامًا، وهو مورّد العناصر التابعة لمشروعك. عندما يتحقّق أحد المشاريع من العناصر التابعة له، يتم فحصها في عنصر التحكّم في المصدر إلى جانب رمز مصدر المشروع، سواءً كان مصدرًا أو برنامجًا ثنائيًا. وهذا يعني أن جميع المهام التابعة للمشروع يتم تحويلها إلى المهام التابعة الداخلية. تستخدم Google هذه الطريقة داخليًا، مع التحقّق من كل مكتبة تابعة لجهات خارجية مُشار إليها في جميع أنحاء Google في دليل third_party في جذر شجرة البيانات على Google. ولا تعمل هذه الطريقة في Google إلا لأنّ نظام التحكّم في المصدر في Google مخصّص للتعامل مع أحادي المستوى كبير جدًا، لذلك قد لا يكون تقديم الاختيار خيارًا لجميع المؤسسات.