ادغام سبک BPM

ادغام سبک BPM

سلام هابر!

شرکت ما در توسعه راه حل های نرم افزاری کلاس ERP تخصص دارد، که در آن سهم شیر توسط سیستم های تراکنش با حجم عظیمی از منطق تجاری و گردش کار a la EDMS اشغال شده است. نسخه‌های مدرن محصولات ما مبتنی بر فناوری‌های JavaEE هستند، اما ما نیز به طور فعال در حال آزمایش میکروسرویس‌ها هستیم. یکی از مشکل‌سازترین حوزه‌های چنین راه‌حل‌هایی، ادغام زیرسیستم‌های مختلف مرتبط با حوزه‌های مجاور است. وظایف یکپارچه سازی همیشه بدون توجه به سبک های معماری، پشته های فناوری و چارچوب هایی که استفاده می کنیم، دردسر بزرگی برای ما ایجاد کرده است، اما اخیراً پیشرفت هایی در حل چنین مشکلاتی حاصل شده است.

در مقاله ای که مورد توجه شما قرار گرفت، در مورد تجربه و تحقیقات معماری NPO Krista در منطقه تعیین شده صحبت خواهم کرد. ما همچنین نمونه ای از یک راه حل ساده برای یک مشکل یکپارچه سازی را از دیدگاه یک توسعه دهنده برنامه در نظر خواهیم گرفت و خواهیم فهمید که در پس این سادگی چه چیزی پنهان شده است.

سلب مسئولیت

راه حل های معماری و فنی شرح داده شده در مقاله توسط من بر اساس تجربه شخصی در زمینه وظایف خاص ارائه شده است. این راه حل ها ادعای جهانی بودن ندارند و ممکن است تحت شرایط استفاده دیگر بهینه نباشند.

BPM چه ربطی به آن دارد؟

برای پاسخ به این سوال، باید کمی به جزئیات مشکلات کاربردی راه حل های خود بپردازیم. بخش اصلی منطق تجاری در سیستم تراکنشی معمولی ما، وارد کردن داده ها به پایگاه داده از طریق رابط های کاربر، بررسی دستی و خودکار این داده ها، عبور آن از طریق برخی گردش کار، انتشار آن به سیستم دیگر / پایگاه داده تحلیلی / بایگانی، و تولید گزارش است. بنابراین، عملکرد کلیدی سیستم برای مشتریان، اتوماسیون فرآیندهای تجاری داخلی آنهاست.

برای راحتی، ما از اصطلاح "سند" در ارتباطات به عنوان انتزاعی از یک مجموعه داده استفاده می کنیم، که توسط یک کلید مشترک متحد شده است، که یک گردش کار خاص را می توان "ضمیمه" کرد.
اما در مورد منطق یکپارچه سازی چطور؟ از این گذشته ، وظیفه ادغام توسط معماری سیستم ایجاد می شود ، که نه به درخواست مشتری بلکه تحت تأثیر عوامل کاملاً متفاوت به قطعات "اره" می شود:

  • تحت تأثیر قانون کانوی؛
  • در نتیجه استفاده مجدد از زیرسیستم هایی که قبلاً برای محصولات دیگر توسعه یافته بودند.
  • همانطور که معمار بر اساس الزامات غیر عملکردی تصمیم گرفته است.

وسوسه بزرگی برای جدا کردن منطق یکپارچه سازی از منطق تجاری گردش کار اصلی وجود دارد تا منطق تجاری را با مصنوعات یکپارچه سازی آلوده نکنیم و توسعه دهنده برنامه را از کاوش در ویژگی های چشم انداز معماری سیستم نجات دهیم. این رویکرد دارای چندین مزیت است، اما تمرین ناکارآمدی آن را نشان می دهد:

  • حل مشکلات ادغام معمولاً به ساده‌ترین گزینه‌ها در قالب تماس‌های همزمان به دلیل محدودیت نقاط گسترش در اجرای گردش کار اصلی کاهش می‌یابد (در مورد کاستی‌های یکپارچه‌سازی همزمان در زیر)
  • در صورت نیاز به بازخورد از سیستم فرعی دیگر، مصنوعات یکپارچه سازی همچنان در منطق اصلی تجارت نفوذ می کنند.
  • توسعه‌دهنده برنامه یکپارچه‌سازی را نادیده می‌گیرد و به راحتی می‌تواند با تغییر گردش کار آن را بشکند.
  • سیستم از نظر کاربر یک کل واحد نیست، "درز" بین زیرسیستم ها قابل توجه می شود، عملیات اضافی کاربر ظاهر می شود که انتقال داده ها را از یک زیر سیستم به زیرسیستم دیگر آغاز می کند.

رویکرد دیگر این است که تعاملات یکپارچه سازی را به عنوان بخشی جدایی ناپذیر از منطق اصلی تجارت و گردش کار در نظر بگیریم. برای جلوگیری از افزایش سرسام آور نیازهای مهارتی توسعه دهندگان برنامه، ایجاد تعاملات یکپارچه سازی جدید باید به راحتی و به طور طبیعی و با حداقل گزینه ها برای انتخاب راه حل انجام شود. این دشوارتر از آن چیزی است که به نظر می رسد: ابزار باید به اندازه کافی قدرتمند باشد تا گزینه های لازم را برای استفاده از آن در اختیار کاربر قرار دهد و در عین حال به خود اجازه اصابت گلوله به پا را ندهد. سوالات زیادی وجود دارد که یک مهندس در زمینه وظایف یکپارچه سازی باید به آنها پاسخ دهد، اما یک توسعه دهنده برنامه نباید در کار روزانه خود به آنها فکر کند: مرزهای تراکنش، ثبات، اتمی، امنیت، مقیاس بندی، توزیع بار و منابع، مسیریابی، مارشال کردن، انتشار و تغییر زمینه ها، و غیره. لازم است که به توسعه دهندگان برنامه های کاربردی الگوهای تصمیم گیری نسبتاً ساده ای ارائه شود، که در آن پاسخ به همه این سؤالات از قبل پنهان شده باشد. این الگوها باید به اندازه کافی ایمن باشند: منطق تجاری اغلب تغییر می کند، که خطر ایجاد خطا را افزایش می دهد، هزینه خطاها باید در سطح نسبتاً پایینی باقی بماند.

اما با این حال، BPM چه ربطی به آن دارد؟ گزینه های زیادی برای پیاده سازی گردش کار وجود دارد ...
در واقع، اجرای دیگری از فرآیندهای کسب و کار در راه حل های ما بسیار محبوب است - از طریق تنظیم اعلامی نمودار انتقال حالت و اتصال کنترل کننده ها با منطق تجاری به انتقال. در عین حال، حالتی که موقعیت فعلی "سند" را در فرآیند تجاری تعیین می کند، ویژگی خود "سند" است.

ادغام سبک BPM
این روند در شروع پروژه به این صورت است

محبوبیت چنین پیاده سازی به دلیل سادگی و سرعت نسبی ایجاد فرآیندهای تجاری خطی است. با این حال، با پیچیده‌تر شدن سیستم‌های نرم‌افزاری، بخش خودکار فرآیند کسب‌وکار رشد می‌کند و پیچیده‌تر می‌شود. نیاز به تجزیه، استفاده مجدد از بخش هایی از فرآیندها و همچنین فرآیندهای انشعاب وجود دارد تا هر شاخه به صورت موازی اجرا شود. در چنین شرایطی، ابزار ناخوشایند می شود و نمودار انتقال وضعیت محتوای اطلاعاتی خود را از دست می دهد (تعاملات یکپارچه سازی به هیچ وجه در نمودار منعکس نمی شود).

ادغام سبک BPM
این همان چیزی است که این فرآیند پس از چندین بار شفاف سازی الزامات به نظر می رسد

راه برون رفت از این وضعیت یکپارچه سازی موتور بود jBPM به برخی از محصولات با پیچیده ترین فرآیندهای تجاری. در کوتاه‌مدت، این راه‌حل موفقیت‌هایی داشت: پیاده‌سازی فرآیندهای تجاری پیچیده با حفظ یک نمودار نسبتاً آموزنده و به‌روز در نماد ممکن شد. BPMN2.

ادغام سبک BPM
بخش کوچکی از یک فرآیند پیچیده تجاری

در درازمدت، راه‌حل انتظارات را برآورده نکرد: شدت کار بالا در ایجاد فرآیندهای تجاری از طریق ابزارهای بصری امکان دستیابی به شاخص‌های بهره‌وری قابل قبول را فراهم نمی‌کرد و خود این ابزار به یکی از محبوب‌ترین ابزارها در میان توسعه‌دهندگان تبدیل شد. همچنین شکایاتی در مورد ساختار داخلی موتور وجود داشت که منجر به ظهور بسیاری از "لکه ها" و "عصاها" شد.

جنبه مثبت اصلی استفاده از jBPM، درک مزایا و مضرات داشتن حالت پایدار خود برای یک نمونه فرآیند تجاری بود. ما همچنین امکان استفاده از یک رویکرد فرآیندی را برای پیاده سازی پروتکل های یکپارچه سازی پیچیده بین برنامه های مختلف با استفاده از تعاملات ناهمزمان از طریق سیگنال ها و پیام ها مشاهده کردیم. حضور یک دولت پایدار در این امر نقش اساسی دارد.

با توجه به مطالب فوق می توان نتیجه گرفت: رویکرد فرآیند در سبک BPM به ما اجازه می‌دهد تا طیف گسترده‌ای از وظایف را برای خودکارسازی فرآیندهای تجاری پیچیده‌تر حل کنیم، فعالیت‌های یکپارچه‌سازی را به طور هماهنگ در این فرآیندها قرار دهیم و توانایی نمایش بصری فرآیند اجرا شده را در یک نماد مناسب حفظ کنیم.

معایب تماس های همزمان به عنوان یک الگوی یکپارچه سازی

ادغام همزمان به ساده ترین تماس مسدود کننده اشاره دارد. یک زیر سیستم به عنوان سمت سرور عمل می کند و API را با روش مورد نظر در معرض نمایش قرار می دهد. زیرسیستم دیگری به عنوان سمت مشتری عمل می کند و در زمان مناسب با انتظار نتیجه تماس برقرار می کند. بسته به معماری سیستم، سمت سرویس گیرنده و سرور می تواند در یک برنامه و فرآیند یکسان یا در برنامه های مختلف میزبانی شود. در حالت دوم، باید مقداری پیاده سازی RPC را اعمال کنید و پارامترها و نتیجه فراخوانی را مارشال کنید.

ادغام سبک BPM

چنین الگوی ادغامی دارای مجموعه نسبتاً بزرگی از اشکالات است، اما به دلیل سادگی در عمل بسیار مورد استفاده قرار می گیرد. سرعت اجرا را مجذوب خود می کند و باعث می شود که در شرایط "سوزاندن" ضرب الاجل، بارها و بارها آن را اعمال کنید و راه حل را در بدهی فنی بنویسید. اما همچنین اتفاق می افتد که توسعه دهندگان بی تجربه به طور ناخودآگاه از آن استفاده می کنند و به سادگی متوجه عواقب منفی آن نیستند.

علاوه بر آشکارترین افزایش در اتصال زیرسیستم ها، مشکلات کمتر آشکاری در مورد "گسترش" و "کشش" تراکنش ها وجود دارد. در واقع، اگر منطق کسب‌وکار تغییراتی ایجاد کند، تراکنش‌ها ضروری هستند و تراکنش‌ها به نوبه خود، منابع برنامه خاصی را که تحت تأثیر این تغییرات قرار گرفته‌اند قفل می‌کنند. به این معنا که تا زمانی که یک زیرسیستم منتظر پاسخ از سوی زیرسیستم دیگری نباشد، نمی‌تواند تراکنش را کامل کند و قفل‌ها را آزاد کند. این به طور قابل توجهی خطر ابتلا به انواع اثرات را افزایش می دهد:

  • پاسخگویی سیستم از بین می رود، کاربران مدت زیادی منتظر پاسخ به درخواست ها هستند.
  • معمولاً سرور به دلیل وجود یک مخزن موضوعی سرریز از پاسخگویی به درخواست‌های کاربر متوقف می‌شود: بیشتر رشته‌ها روی قفل منبع اشغال شده توسط تراکنش قرار دارند.
  • بن بست ها ظاهر می شوند: احتمال وقوع آنها به شدت به مدت تراکنش ها، میزان منطق تجاری و قفل های موجود در معامله بستگی دارد.
  • خطاهای انقضای مهلت معامله ظاهر می شوند.
  • سرور بر روی OutOfMemory می افتد اگر کار به پردازش و تغییر مقادیر زیادی داده نیاز داشته باشد و وجود ادغام های همزمان تقسیم پردازش به تراکنش های "سبک تر" را بسیار دشوار می کند.

از نقطه نظر معماری، استفاده از مسدود کردن تماس ها در طول یکپارچه سازی منجر به از دست دادن کنترل کیفیت زیرسیستم های فردی می شود: دستیابی به اهداف کیفیت برای یک زیرسیستم جدا از شاخص های کیفیت برای زیرسیستم دیگر غیرممکن است. اگر زیرسیستم ها توسط تیم های مختلف توسعه داده شوند، این یک مشکل بزرگ است.

اگر زیرسیستم های در حال ادغام در برنامه های مختلف باشند و نیاز به تغییرات همزمان در هر دو طرف باشد، همه چیز جالب تر می شود. چگونه می توان این تغییرات را تراکنشی کرد؟

اگر تغییراتی در تراکنش‌های جداگانه انجام شود، باید مدیریت استثنایی و جبران خسارت قوی ارائه شود و این مزیت اصلی ادغام‌های همزمان - سادگی را کاملاً از بین می‌برد.

تراکنش های توزیع شده نیز به ذهن می رسند، اما ما از آنها در راه حل های خود استفاده نمی کنیم: اطمینان از قابلیت اطمینان دشوار است.

"حماسه" به عنوان راه حلی برای مشکل معاملات

با افزایش محبوبیت میکروسرویس ها، تقاضای فزاینده ای برای آنها وجود دارد الگوی حماسه.

این الگو به خوبی مشکلات فوق در مورد تراکنش های طولانی را حل می کند و همچنین امکانات مدیریت وضعیت سیستم را از ناحیه منطق تجاری گسترش می دهد: جبران خسارت پس از یک تراکنش ناموفق ممکن است سیستم را به حالت اولیه خود برنگرداند، اما یک جایگزین ارائه می دهد. مسیر پردازش داده ها همچنین به شما این امکان را می‌دهد که مراحل پردازش داده‌ها را که با موفقیت انجام شده‌اند، تکرار نکنید، زمانی که می‌خواهید فرآیند را به پایان «خوب» برسانید.

جالب اینجاست که در سیستم‌های یکپارچه، این الگو در مورد ادغام زیرسیستم‌های جفت شده آزاد نیز مرتبط است و اثرات منفی ناشی از تراکنش‌های طولانی و قفل‌های منابع مربوطه وجود دارد.

با توجه به فرآیندهای کسب و کار ما در سبک BPM، به نظر می رسد که اجرای Sagas بسیار آسان است: مراحل فردی Sagas را می توان به عنوان فعالیت هایی در فرآیند کسب و کار تنظیم کرد، و وضعیت پایدار فرآیند کسب و کار را تعیین می کند. چیزهای دیگر، وضعیت داخلی حماسه. یعنی ما به هیچ مکانیزم هماهنگی اضافی نیاز نداریم. تنها چیزی که نیاز دارید یک کارگزار پیام با پشتیبانی از "حداقل یک بار" تضمین به عنوان حمل و نقل است.

اما چنین راه حلی نیز "قیمت" خود را دارد:

  • منطق کسب و کار پیچیده تر می شود: شما باید جبران خسارت کنید.
  • لازم است سازگاری کامل را کنار بگذاریم که می تواند به ویژه برای سیستم های یکپارچه حساس باشد.
  • معماری کمی پیچیده تر می شود، نیاز اضافی به یک کارگزار پیام وجود دارد.
  • ابزار نظارت و مدیریت اضافی مورد نیاز خواهد بود (اگرچه به طور کلی این حتی خوب است: کیفیت خدمات سیستم افزایش خواهد یافت).

برای سیستم های یکپارچه، توجیه استفاده از "Sags" چندان واضح نیست. برای میکروسرویس ها و سایر SOA ها، که به احتمال زیاد، در حال حاضر یک کارگزار وجود دارد، و سازگاری کامل در شروع پروژه قربانی شده است، مزایای استفاده از این الگو می تواند به میزان قابل توجهی از معایب آن بیشتر باشد، به خصوص اگر یک API مناسب در آن وجود داشته باشد. سطح منطق کسب و کار

کپسوله سازی منطق کسب و کار در میکروسرویس ها

زمانی که ما شروع به آزمایش با میکروسرویس ها کردیم، یک سوال منطقی مطرح شد: منطق تجاری دامنه را در رابطه با سرویسی که تداوم داده دامنه را فراهم می کند، کجا قرار دهیم؟

هنگامی که به معماری BPMS های مختلف نگاه می کنیم، ممکن است منطقی به نظر برسد که منطق کسب و کار را از پایداری جدا کنیم: ایجاد یک لایه از پلت فرم و میکروسرویس های مستقل از دامنه که محیط و محفظه ای را برای اجرای منطق تجاری دامنه تشکیل می دهد، و تداوم داده های دامنه را به عنوان یک مجزا مرتب می کند. لایه ای از میکروسرویس های بسیار ساده و سبک. فرآیندهای تجاری در این مورد خدمات لایه پایداری را هماهنگ می‌کنند.

ادغام سبک BPM

این رویکرد یک مزیت بسیار بزرگ دارد: شما می توانید عملکرد پلت فرم را تا جایی که دوست دارید افزایش دهید و فقط لایه مربوطه از میکروسرویس های پلت فرم از این طریق "چاق می شود". فرآیندهای تجاری از هر دامنه بلافاصله فرصت استفاده از قابلیت های جدید پلت فرم را به محض به روز رسانی آن به دست می آورند.

مطالعه دقیق تر کاستی های قابل توجهی را در این رویکرد نشان داد:

  • یک سرویس پلت فرم که منطق تجاری بسیاری از دامنه ها را به طور همزمان اجرا می کند، به عنوان یک نقطه شکست، خطرات زیادی را به همراه دارد. تغییرات مکرر در منطق کسب و کار، خطر اشکالات منجر به شکست در سراسر سیستم را افزایش می دهد.
  • مسائل مربوط به عملکرد: منطق کسب و کار با داده های خود از طریق یک رابط باریک و کند کار می کند:
    • داده ها یک بار دیگر مارشال شده و از طریق پشته شبکه پمپ می شوند.
    • به دلیل ناکافی بودن قابلیت پارامترسازی پرس و جو در سطح API خارجی سرویس، سرویس دامنه اغلب داده های بیشتری را نسبت به منطق تجاری که برای پردازش نیاز دارد، برمی گرداند.
    • چندین بخش مستقل از منطق تجاری می توانند بارها و بارها همان داده ها را برای پردازش درخواست کنند (شما می توانید این مشکل را با اضافه کردن Session beans که داده ها را در حافظه پنهان می کند، کاهش دهید، اما این امر معماری را پیچیده تر می کند و مشکلاتی در تازگی داده ها و عدم اعتبار کش ایجاد می کند).
  • مسائل معاملاتی:
    • فرآیندهای تجاری با وضعیت پایدار ذخیره شده توسط سرویس پلت فرم با داده های دامنه ناسازگار هستند و هیچ راه آسانی برای حل این مشکل وجود ندارد.
    • انتقال قفل داده های دامنه از تراکنش: اگر منطق کسب و کار دامنه نیاز به ایجاد تغییرات داشته باشد، پس از بررسی صحت داده های واقعی، لازم است امکان تغییر رقابتی در داده های پردازش شده حذف شود. مسدود کردن خارجی داده ها می تواند به حل مشکل کمک کند، اما چنین راه حلی خطرات بیشتری را به همراه دارد و قابلیت اطمینان کلی سیستم را کاهش می دهد.
  • عوارض اضافی هنگام به روز رسانی: در برخی موارد، شما باید سرویس پایداری و منطق تجاری را به طور همزمان یا به ترتیب دقیق به روز کنید.

در پایان، مجبور شدم به اصول اولیه بازگردم: داده های دامنه و منطق تجاری دامنه را در یک میکروسرویس محصور کنید. این رویکرد درک میکروسرویس را به عنوان یک جزء جدایی ناپذیر در سیستم ساده می کند و مشکلات فوق را ایجاد نمی کند. این نیز رایگان نیست:

  • استانداردسازی API برای تعامل با منطق تجاری (به ویژه برای ارائه فعالیت های کاربر به عنوان بخشی از فرآیندهای تجاری) و خدمات پلت فرم API مورد نیاز است. توجه دقیق تر به تغییرات API، سازگاری رو به جلو و عقب مورد نیاز است.
  • برای اطمینان از عملکرد منطق تجاری به عنوان بخشی از هر میکروسرویس، لازم است کتابخانه‌های زمان اجرا اضافی اضافه شود، و این باعث ایجاد الزامات جدیدی برای چنین کتابخانه‌هایی می‌شود: سبکی و حداقل وابستگی‌های گذرا.
  • توسعه دهندگان منطق کسب و کار باید نسخه های کتابخانه را پیگیری کنند: اگر یک میکروسرویس برای مدت طولانی نهایی نشده باشد، به احتمال زیاد دارای نسخه قدیمی کتابخانه ها خواهد بود. این می تواند یک مانع غیرمنتظره برای افزودن یک ویژگی جدید باشد و ممکن است نیاز داشته باشد که منطق تجاری قدیمی چنین سرویسی به نسخه های جدید کتابخانه ها منتقل شود، اگر تغییرات ناسازگاری بین نسخه ها وجود داشته باشد.

ادغام سبک BPM

لایه‌ای از خدمات پلتفرم نیز در چنین معماری وجود دارد، اما این لایه دیگر محفظه‌ای برای اجرای منطق کسب‌وکار دامنه تشکیل نمی‌دهد، بلکه فقط محیط آن را تشکیل می‌دهد که توابع «پلتفرم» کمکی را ارائه می‌کند. چنین لایه ای نه تنها برای حفظ سبکی ریزسرویس های دامنه، بلکه برای متمرکز کردن مدیریت نیز مورد نیاز است.

به عنوان مثال، فعالیت های کاربر در فرآیندهای تجاری، وظایفی را ایجاد می کند. با این حال، هنگام کار با وظایف، کاربر باید وظایف همه دامنه‌ها را در فهرست کلی ببیند، به این معنی که باید یک سرویس پلت فرم ثبت وظیفه مناسب، پاک از منطق تجاری دامنه وجود داشته باشد. حفظ کپسوله کردن منطق تجاری در این زمینه کاملاً مشکل ساز است و این یکی دیگر از مصالحه این معماری است.

یکپارچه سازی فرآیندهای کسب و کار از دید یک توسعه دهنده برنامه

همانطور که در بالا ذکر شد، توسعه‌دهنده برنامه باید از ویژگی‌های فنی و مهندسی اجرای تعامل چندین برنامه انتزاعی باشد تا بتواند روی بهره‌وری توسعه خوب حساب کند.

بیایید سعی کنیم یک مشکل ادغام نسبتاً دشوار را که مخصوصاً برای مقاله اختراع شده است حل کنیم. این یک کار "بازی" خواهد بود که شامل سه برنامه است، که در آن هر یک از آنها نام دامنه ای را تعریف می کند: "app1"، "app2"، "app3".

در داخل هر برنامه، فرآیندهای تجاری راه اندازی می شوند که از طریق گذرگاه یکپارچه سازی شروع به "توپ بازی" می کنند. پیام هایی با نام "Ball" به عنوان توپ عمل می کنند.

قوانین بازی:

  • اولین بازیکن آغازگر است. او بازیکنان دیگر را به بازی دعوت می کند، بازی را شروع می کند و می تواند آن را در هر زمان به پایان برساند.
  • سایر بازیکنان شرکت خود را در بازی اعلام می کنند، با یکدیگر و اولین بازیکن "آشنایی" می یابند.
  • پس از دریافت توپ، بازیکن بازیکن دیگر شرکت کننده را انتخاب کرده و توپ را به او پاس می دهد. تعداد کل پاس ها شمرده می شود.
  • هر بازیکن دارای "انرژی" است که با هر پاس توپ توسط آن بازیکن کاهش می یابد. وقتی انرژی تمام می شود، بازیکن از بازی حذف می شود و بازنشستگی خود را اعلام می کند.
  • اگر بازیکن تنها بماند، فوراً جدایی خود را اعلام می کند.
  • وقتی همه بازیکنان حذف شوند، اولین بازیکن پایان بازی را اعلام می کند. اگر او بازی را زودتر ترک کرد، پس باید بازی را دنبال کرد تا آن را کامل کند.

برای حل این مشکل، من از 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، بازیکن را با رفتار کمی متفاوت می سازیم: به جای انتخاب تصادفی بازیکن بعدی، طبق الگوریتم دورگرد عمل می کند:

کلاس 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;
  • گردانندگان پیام‌ها و رویدادهای دریافتی نیز باید با تراکنش تغییر وضعیت فرآیند کسب‌وکار کار کنند: اگر چنین معامله‌ای برگشت داده شود، دریافت پیام نیز باید لغو شود.
  • پیام هایی که به دلیل خطا نمی توانند تحویل داده شوند باید در یک فروشگاه جداگانه ذخیره شوند D.L.Q. (صف نامه مرده). برای انجام این کار، ما یک میکروسرویس پلتفرم جداگانه ایجاد کردیم که چنین پیام‌هایی را در فضای ذخیره‌سازی خود ذخیره می‌کند، آن‌ها را با ویژگی‌ها فهرست‌بندی می‌کند (برای گروه‌بندی و جستجوی سریع)، و API را برای مشاهده، ارسال مجدد به آدرس مقصد و حذف پیام‌ها در معرض دید قرار می‌دهد. مدیران سیستم می توانند از طریق رابط وب خود با این سرویس کار کنند.
  • در تنظیمات کارگزار، برای کاهش احتمال ورود پیام‌ها به DLQ، باید تعداد دفعات تحویل مجدد و تأخیر بین تحویل‌ها را تنظیم کنید (محاسبه پارامترهای بهینه تقریبا غیرممکن است، اما می‌توانید به صورت تجربی عمل کنید و آنها را در طول زمان تنظیم کنید. عمل)؛
  • فروشگاه DLQ باید به طور مداوم نظارت شود و سیستم نظارت باید مدیران سیستم را مطلع کند تا در صورت بروز پیام‌های تحویل‌نشده، بتوانند در سریع‌ترین زمان ممکن پاسخ دهند. این "منطقه آسیب" شکست یا خطای منطق تجاری را کاهش می دهد.
  • گذرگاه یکپارچه سازی باید نسبت به عدم حضور موقت برنامه ها حساس نباشد: اشتراک موضوع باید بادوام باشد و نام دامنه برنامه باید منحصر به فرد باشد تا شخص دیگری در طول غیبت برنامه سعی نکند پیام آن را از صف پردازش کند.

اطمینان از ایمنی رشته منطق تجاری

یک نمونه از یک فرآیند تجاری می تواند چندین پیام و رویداد را به طور همزمان دریافت کند که پردازش آنها به صورت موازی آغاز می شود. در عین حال، برای یک توسعه دهنده برنامه، همه چیز باید ساده و ایمن باشد.

منطق کسب و کار فرآیند هر رویداد خارجی را که بر این فرآیند تجاری تأثیر می گذارد به صورت جداگانه پردازش می کند. این رویدادها می توانند عبارتند از:

  • راه اندازی یک نمونه فرآیند کسب و کار؛
  • یک اقدام کاربر مربوط به یک فعالیت در یک فرآیند تجاری؛
  • دریافت پیام یا سیگنالی که یک نمونه فرآیند تجاری در آن ثبت شده است.
  • انقضای تایمر تنظیم شده توسط نمونه فرآیند کسب و کار؛
  • کنترل کنش از طریق API (مثلاً توقف فرآیند).

هر یک از این رویدادها می‌تواند وضعیت یک نمونه فرآیند تجاری را تغییر دهد: برخی از فعالیت‌ها می‌توانند پایان یابند و برخی دیگر شروع می‌شوند، مقادیر ویژگی‌های پایدار می‌توانند تغییر کنند. بستن هر فعالیت ممکن است منجر به فعال شدن یک یا چند مورد از فعالیت های زیر شود. آن‌ها به نوبه خود می‌توانند منتظر رویدادهای دیگر نباشند، یا اگر به داده‌های اضافی نیاز ندارند، می‌توانند در همان تراکنش کامل شوند. قبل از بستن تراکنش، وضعیت جدید فرآیند تجاری در پایگاه داده ذخیره می شود، جایی که منتظر رویداد خارجی بعدی می ماند.

داده های دائمی فرآیند کسب و کار ذخیره شده در یک پایگاه داده رابطه ای یک نقطه همگام سازی پردازش بسیار راحت هنگام استفاده از SELECT FOR UPDATE است. اگر یک تراکنش بتواند وضعیت فرآیند کسب و کار را از پایگاه داده دریافت کند تا آن را تغییر دهد، هیچ تراکنش دیگری به طور موازی قادر به تغییر وضعیت مشابه برای تغییر دیگری نخواهد بود و پس از اتمام اولین تراکنش، تراکنش دوم تضمین شده است که وضعیت قبلاً تغییر یافته را دریافت می کند.

با استفاده از قفل های بدبینانه در سمت DBMS، ما تمام الزامات لازم را برآورده می کنیم ACID، و همچنین توانایی مقیاس سازی برنامه با منطق تجاری را با افزایش تعداد نمونه های در حال اجرا حفظ می کند.

با این حال، قفل‌های بدبینانه ما را با بن‌بست تهدید می‌کنند، به این معنی که SELECT FOR UPDATE همچنان باید به مدت زمان معقولی در صورت بن‌بست در برخی موارد فاحش در منطق تجاری محدود شود.

مشکل دیگر همگام سازی شروع فرآیند کسب و کار است. در حالی که هیچ نمونه فرآیند تجاری وجود ندارد، هیچ حالتی در پایگاه داده نیز وجود ندارد، بنابراین روش توصیف شده کار نخواهد کرد. اگر می‌خواهید از منحصربه‌فرد بودن یک نمونه فرآیند تجاری در یک محدوده خاص اطمینان حاصل کنید، به نوعی شی همگام‌سازی مرتبط با کلاس فرآیند و محدوده مربوطه نیاز دارید. برای حل این مشکل، از مکانیزم قفل متفاوتی استفاده می کنیم که به ما امکان می دهد از طریق یک سرویس خارجی، یک منبع دلخواه را که توسط یک کلید در قالب URI مشخص شده است، قفل کنیم.

در مثال های ما، فرآیند کسب و کار InitialPlayer حاوی یک اعلان است

uniqueConstraint = UniqueConstraints.singleton

بنابراین، گزارش حاوی پیام هایی در مورد گرفتن و رها کردن قفل کلید مربوطه است. چنین پیام هایی برای سایر فرآیندهای تجاری وجود ندارد: منحصر به فرد تنظیم نشده است.

مشکلات فرآیند کسب و کار با وضعیت پایدار

گاهی اوقات داشتن حالت پایدار نه تنها کمک می کند، بلکه واقعاً مانع از پیشرفت می شود.
مشکلات زمانی شروع می شوند که شما نیاز به ایجاد تغییراتی در منطق کسب و کار و/یا مدل فرآیند کسب و کار دارید. هیچ چنین تغییری با وضعیت قدیمی فرآیندهای تجاری سازگار نیست. اگر نمونه‌های "زنده" زیادی در پایگاه داده وجود داشته باشد، ایجاد تغییرات ناسازگار می‌تواند باعث ایجاد مشکلات زیادی شود که اغلب هنگام استفاده از jBPM با آن مواجه می‌شویم.

بسته به عمق تغییر، می توانید به دو روش عمل کنید:

  1. یک نوع فرآیند کسب و کار جدید ایجاد کنید تا تغییرات ناسازگاری با نوع قبلی ایجاد نکنید و هنگام شروع نمونه های جدید از آن به جای نوع قبلی استفاده کنید. نمونه های قدیمی "روش قدیمی" به کار خود ادامه خواهند داد.
  2. هنگام به روز رسانی منطق کسب و کار، وضعیت پایدار فرآیندهای تجاری را تغییر دهید.

راه اول ساده تر است، اما محدودیت ها و معایب خود را دارد، به عنوان مثال:

  • تکرار منطق کسب و کار در بسیاری از مدل های فرآیند کسب و کار، افزایش حجم منطق کسب و کار؛
  • اغلب یک انتقال فوری به یک منطق تجاری جدید مورد نیاز است (تقریبا همیشه از نظر وظایف یکپارچه سازی).
  • توسعه دهنده نمی داند در چه مرحله ای می توان مدل های منسوخ را حذف کرد.

در عمل، ما از هر دو رویکرد استفاده می کنیم، اما تعدادی تصمیم برای ساده کردن زندگی خود گرفته ایم:

  • در پایگاه داده، وضعیت پایدار فرآیند کسب و کار به شکلی به راحتی قابل خواندن و پردازش آسان ذخیره می شود: در یک رشته با فرمت JSON. این به شما این امکان را می دهد که هم در داخل برنامه و هم در خارج از آن مهاجرت انجام دهید. در موارد شدید، شما همچنین می توانید آن را با دسته ها تغییر دهید (به ویژه در توسعه در هنگام اشکال زدایی مفید است).
  • منطق کسب‌وکار یکپارچه‌سازی از نام‌های فرآیندهای تجاری استفاده نمی‌کند، به طوری که در هر زمان می‌توان اجرای یکی از فرآیندهای شرکت‌کننده را با یک نام جدید جایگزین کرد (به عنوان مثال، "InitialPlayerV2"). اتصال از طریق نام پیام ها و سیگنال ها اتفاق می افتد.
  • مدل فرآیند یک شماره نسخه دارد که در صورت ایجاد تغییرات ناسازگار در این مدل، آن را افزایش می‌دهیم و این عدد همراه با وضعیت نمونه فرآیند ذخیره می‌شود.
  • حالت پایدار فرآیند ابتدا از پایه به یک مدل شی مناسب خوانده می شود که اگر شماره نسخه مدل تغییر کرده باشد، رویه مهاجرت می تواند با آن کار کند.
  • رویه مهاجرت در کنار منطق تجاری قرار می گیرد و برای هر نمونه از فرآیند تجاری در زمان بازیابی آن از پایگاه داده "تنبل" نامیده می شود.
  • اگر نیاز دارید که وضعیت همه نمونه های فرآیند را به سرعت و همزمان منتقل کنید، راه حل های کلاسیک تری برای انتقال پایگاه داده استفاده می شود، اما باید در آنجا با JSON کار کنید.

آیا به چارچوب دیگری برای فرآیندهای تجاری نیاز دارم؟

راه حل های شرح داده شده در مقاله به ما این امکان را می دهد که زندگی خود را به طور قابل توجهی ساده کنیم، دامنه مسائل حل شده در سطح توسعه برنامه را گسترش دهیم و ایده جداسازی منطق تجاری به میکروسرویس ها را جذاب تر کنیم. برای این کار، کارهای زیادی انجام شده است، یک چارچوب بسیار سبک وزن برای فرآیندهای تجاری و همچنین اجزای خدماتی برای حل مشکلات شناسایی شده در زمینه طیف گسترده ای از وظایف کاربردی ایجاد شده است. ما تمایل داریم این نتایج را به اشتراک بگذاریم تا توسعه اجزای مشترک را تحت یک مجوز رایگان به دسترسی آزاد تبدیل کنیم. این امر مستلزم کمی تلاش و زمان است. درک تقاضا برای چنین راه حل هایی می تواند یک انگیزه اضافی برای ما باشد. در مقاله پیشنهادی به قابلیت های خود فریم ورک بسیار کم توجه شده است، اما برخی از آنها از نمونه های ارائه شده قابل مشاهده است. اگر با این وجود چارچوب خود را منتشر کنیم، مقاله جداگانه ای به آن اختصاص داده خواهد شد. در ضمن، اگر با پاسخ دادن به این سوال، بازخورد کوچکی را در اختیار ما بگذارید، سپاسگزار خواهیم بود:

فقط کاربران ثبت نام شده می توانند در نظرسنجی شرکت کنند. ورود، لطفا.

آیا به چارچوب دیگری برای فرآیندهای تجاری نیاز دارم؟

  • ٪۱۰۰بله من خیلی وقته دنبال همچین چیزی بودم.

  • ٪۱۰۰جالب است که در مورد پیاده سازی خود بیشتر بدانید، ممکن است مفید باشد2

  • ٪۱۰۰ما از یکی از فریمورک های موجود استفاده می کنیم، اما به فکر جایگزینی آن هستیم

  • ٪۱۰۰ما از یکی از چارچوب های موجود استفاده می کنیم، همه چیز مناسب است

  • ٪۱۰۰کنار آمدن بدون چارچوب3

  • ٪۱۰۰خودت بنویس 4

16 کاربر رای دادند. 7 کاربر رای ممتنع دادند.

منبع: www.habr.com

اضافه کردن نظر