تكامل أسلوب BPM

تكامل أسلوب BPM

مرحبا، هبر!

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

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

تنصل

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

ما علاقة BPM به؟

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

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

  • تحت تأثير قانون كونواي ؛
  • نتيجة لإعادة استخدام الأنظمة الفرعية التي سبق تطويرها لمنتجات أخرى ؛
  • وفقًا لما يقرره المهندس المعماري ، بناءً على المتطلبات غير الوظيفية.

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

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

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

ولكن مع ذلك ، ما علاقة BPM به؟ هناك العديد من الخيارات لتنفيذ سير العمل ...
في الواقع ، هناك تطبيق آخر للعمليات التجارية شائع جدًا في حلولنا - من خلال الإعداد التعريفي لمخطط انتقال الحالة وربط المعالجات بمنطق الأعمال بالتحولات. في الوقت نفسه ، فإن الحالة التي تحدد الوضع الحالي لـ "المستند" في عملية الأعمال هي سمة من سمات "المستند" نفسه.

تكامل أسلوب BPM
هكذا تبدو العملية في بداية المشروع

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

تكامل أسلوب BPM
هذا ما تبدو عليه العملية بعد عدة تكرارات لتوضيح المتطلبات

كان المخرج من هذا الموقف هو دمج المحرك jBPM في بعض المنتجات ذات العمليات التجارية الأكثر تعقيدًا. على المدى القصير ، حقق هذا الحل بعض النجاح: أصبح من الممكن تنفيذ عمليات تجارية معقدة مع الحفاظ على مخطط إعلامي وحديث إلى حد ما في التدوين BPMN2.

تكامل أسلوب BPM
جزء صغير من عملية تجارية معقدة

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

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

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

عيوب المكالمات المتزامنة كنمط تكامل

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

تكامل أسلوب BPM

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

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

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

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

تصبح الأمور أكثر إثارة للاهتمام إذا كانت الأنظمة الفرعية التي يتم دمجها في تطبيقات مختلفة ويجب إجراء تغييرات متزامنة على كلا الجانبين. كيف تجعل هذه التغييرات المعاملات؟

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

تتبادر إلى الذهن أيضًا المعاملات الموزعة ، لكننا لا نستخدمها في حلولنا: من الصعب ضمان الموثوقية.

"الملحمة" كحل لمشكلة المعاملات

مع تزايد شعبية الخدمات المصغرة ، هناك طلب متزايد على نمط الملحمة.

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

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

فيما يتعلق بعمليات الأعمال الخاصة بنا في أسلوب BPM ، فقد اتضح أنه من السهل جدًا تنفيذ Sagas: يمكن تعيين الخطوات الفردية لـ Sagas كأنشطة ضمن عملية الأعمال ، وتحدد الحالة المستمرة لعملية الأعمال ، من بين أشياء أخرى ، الحالة الداخلية للملاحم. أي أننا لا نحتاج إلى أي آلية تنسيق إضافية. كل ما تحتاجه هو وسيط رسائل يدعم ضمانات "مرة واحدة على الأقل" كوسيلة نقل.

لكن هذا الحل له أيضًا "ثمنه" الخاص:

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

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

تغليف منطق الأعمال في الخدمات المصغرة

عندما بدأنا في تجربة الخدمات المصغرة ، نشأ سؤال معقول: أين نضع منطق أعمال المجال فيما يتعلق بالخدمة التي توفر استمرارية بيانات المجال؟

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

تكامل أسلوب BPM

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

كشفت دراسة أكثر تفصيلاً عن أوجه قصور كبيرة في هذا النهج:

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

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

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

تكامل أسلوب BPM

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

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

تكامل العمليات التجارية من خلال عيون مطور التطبيق

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

دعنا نحاول حل مشكلة تكامل صعبة نوعًا ما ، تم اختراعها خصيصًا للمقال. ستكون هذه مهمة "لعبة" تتضمن ثلاثة تطبيقات ، حيث يحدد كل منها بعض اسم المجال: "app1" ، "app2" ، "app3".

داخل كل تطبيق ، يتم إطلاق العمليات التجارية التي تبدأ "بلعب الكرة" من خلال ناقل التكامل. الرسائل المسماة "الكرة" ستكون بمثابة الكرة.

قواعد اللعبة:

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

لحل هذه المشكلة ، سأستخدم DSL للعمليات التجارية ، والذي يسمح لك بوصف المنطق في Kotlin بشكل مضغوط ، مع حد أدنى من النموذج المعياري.

في تطبيق app1 ، ستعمل العملية التجارية للاعب الأول (وهو أيضًا بادئ اللعبة):

فئة InitialPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.constraint.UniqueConstraints
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.dsl.taskOperation
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList : ArrayList<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    startFrom(sendNewGameSignal)
            .fork("mainFork") {
                next(startTask)
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut)
                        .branch("checkPlayers") {
                            ifTrue { players.isEmpty() }
                                    .next(sendStopGameSignal)
                                    .terminate()
                            ifElse().next(waitPlayerOut)
                        }
            }
    startTask.fork("afterStart") {
        next(throwStartBall)
                .branch("mainLoop") {
                    ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
                    ifElse().next(waitBall).next(throwBall).loop()
                }
        next(stopTask).next(sendStopGameSignal)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<InitialPlayer, Int>.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

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

تكامل أسلوب BPM

سيتضمن app2 العملية التجارية للاعب آخر:

فئة RandomPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RandomPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val randomPlayerModel = processModel<RandomPlayer>(name = "RandomPlayer", 
                                                   version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("PlayerJoin") {
        eventCondition = { signal ->
            signal.sender.processInstanceId != process.id 
                && !process.players.any { signal.sender.processInstanceId == it.id}
        }
        handler = { signal ->
            players.add(PlayerInfo(
                    signal.data!!,
                    signal.sender.domain,
                    signal.sender.processInstanceId))
        }
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RandomPlayer, Int>.selectNextPlayer() {
    val player = if (process.players.isNotEmpty()) 
        process.players.random() 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

رسم بياني:

تكامل أسلوب BPM

في تطبيق app3 ، سنجعل للاعب سلوكًا مختلفًا بعض الشيء: فبدلاً من اختيار اللاعب التالي عشوائيًا ، سيتصرف وفقًا لخوارزمية round-robin:

فئة RoundRobinPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RoundRobinPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var nextPlayerIndex: Int by persistent(-1)
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val roundRobinPlayerModel = processModel<RoundRobinPlayer>(
        name = "RoundRobinPlayer", 
        version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("PlayerJoin") {
        eventCondition = { signal ->
            signal.sender.processInstanceId != process.id 
                && !process.players.any { signal.sender.processInstanceId == it.id}
        }
        handler = { signal ->
            players.add(PlayerInfo(
                    signal.data!!, 
                    signal.sender.domain, 
                    signal.sender.processInstanceId))
        }
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!, 
                signal.sender.domain, 
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RoundRobinPlayer, Int>.selectNextPlayer() {
    var idx = process.nextPlayerIndex + 1
    if (idx >= process.players.size) {
        idx = 0
    }
    process.nextPlayerIndex = idx
    val player = if (process.players.isNotEmpty()) 
        process.players[idx] 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

خلاف ذلك ، لا يختلف سلوك اللاعب عن السلوك السابق ، لذلك لا يتغير الرسم التخطيطي.

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

testGame ()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

private Map<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

قم بإجراء الاختبار ، انظر إلى السجل:

إخراج وحدة التحكم

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

يمكن استخلاص عدة استنتاجات مهمة من كل هذا:

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

بعد ذلك ، لنتحدث عن التفاصيل الدقيقة للحل والتنازلات والنقاط الأخرى.

جميع الرسائل في قائمة انتظار واحدة

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

تكامل أسلوب BPM

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

ضمان موثوقية ناقل التكامل

تتكون الموثوقية من عدة أشياء:

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

ضمان سلامة الخيط لمنطق الأعمال

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

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

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

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

تعد بيانات عمليات الأعمال المستمرة المخزنة في قاعدة بيانات علائقية نقطة تزامن معالجة ملائمة للغاية عند استخدام SELECT FOR UPDATE. إذا تمكنت إحدى المعاملات من الحصول على حالة العملية التجارية من قاعدة البيانات لتغييرها ، فلن تتمكن أي معاملة أخرى على التوازي من الحصول على نفس الحالة لتغيير آخر ، وبعد اكتمال المعاملة الأولى ، تكون المعاملة الثانية مضمون لاستلام الحالة المتغيرة بالفعل.

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

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

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

في أمثلةنا ، تحتوي عملية الأعمال InitialPlayer على إقرار

uniqueConstraint = UniqueConstraints.singleton

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

مشاكل إجراءات العمل ذات الحالة المستمرة

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

اعتمادًا على عمق التغيير ، يمكنك التصرف بطريقتين:

  1. قم بإنشاء نوع عملية تجارية جديد حتى لا يتم إجراء تغييرات غير متوافقة على النوع القديم ، واستخدمه بدلاً من النوع القديم عند بدء مثيلات جديدة. ستستمر الأمثلة القديمة في العمل "بالطريقة القديمة" ؛
  2. ترحيل الحالة المستمرة للعمليات التجارية عند تحديث منطق الأعمال.

الطريقة الأولى أبسط ، لكن لها حدودها وعيوبها ، على سبيل المثال:

  • ازدواجية منطق الأعمال في العديد من نماذج العمليات التجارية ، وزيادة حجم منطق الأعمال ؛
  • غالبًا ما يكون الانتقال الفوري إلى منطق عمل جديد مطلوبًا (دائمًا تقريبًا من حيث مهام التكامل) ؛
  • لا يعرف المطور في أي نقطة يمكن حذف النماذج المتقادمة.

في الممارسة العملية ، نستخدم كلا النهجين ، لكننا اتخذنا عددًا من القرارات لتبسيط حياتنا:

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

هل أحتاج إلى إطار عمل آخر لعمليات الأعمال؟

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

يمكن للمستخدمين المسجلين فقط المشاركة في الاستطلاع. تسجيل الدخول، من فضلك.

هل أحتاج إلى إطار عمل آخر لعمليات الأعمال؟

  • 18,8%نعم ، لقد كنت أبحث عن شيء مثل هذا لفترة طويلة.

  • 12,5%من المثير للاهتمام معرفة المزيد عن التنفيذ الخاص بك ، فقد يكون مفيدًا 2

  • 6,2%نستخدم أحد الأطر الحالية ، لكننا نفكر في استبداله 1

  • 18,8%نستخدم أحد الأطر الموجودة ، كل شيء يناسب 3

  • 18,8%التعامل بدون إطار 3

  • 25,0%اكتب ما تريد 4

صوت 16 مستخدمين. امتنع 7 مستخدما عن التصويت.

المصدر: www.habr.com

إضافة تعليق