BPM стилінің интеграциясы

BPM стилінің интеграциясы

Сәлеметсіз бе, Хабар!

Біздің компания ERP класындағы бағдарламалық шешімдерді әзірлеуге маманданған, олардың басым бөлігін бизнес логикасы мен құжат айналымы a la EDMS бар транзакциялық жүйелер алады. Біздің өнімдеріміздің ағымдағы нұсқалары JavaEE технологияларына негізделген, бірақ біз микросервистермен де белсенді түрде тәжірибе жасап жатырмыз. Мұндай шешімдердің ең проблемалық бағыттарының бірі көршілес домендерге жататын әртүрлі ішкі жүйелерді біріктіру болып табылады. Біз қолданатын архитектуралық стильдерге, технологиялық стектерге және рамкаларға қарамастан, интеграциялық мәселелер бізге әрқашан үлкен бас ауруын тудырды, бірақ соңғы уақытта мұндай мәселелерді шешуде ілгерілеушілік байқалды.

Мен сіздердің назарларыңызға ұсынатын мақалада мен Криста НПО-ның тағайындалған аймақтағы тәжірибесі мен сәулеттік зерттеулері туралы айтатын боламын. Сондай-ақ, біз қолданбаларды әзірлеуші ​​тұрғысынан интеграциялық мәселені шешудің қарапайым мысалын қарастырамыз және осы қарапайымдылықтың артында не жасырылғанын анықтаймыз.

Ескерту

Мақалада сипатталған архитектуралық және техникалық шешімдерді мен нақты тапсырмалар контекстіндегі жеке тәжірибеге сүйене отырып ұсынамын. Бұл шешімдер әмбебап болып саналмайды және басқа пайдалану шарттарында оңтайлы болмауы мүмкін.

Оған BPM не қатысы бар?

Бұл сұраққа жауап беру үшін біз шешімдеріміздің қолданбалы мәселелерінің ерекшеліктеріне біршама тереңірек енуіміз керек. Біздің типтік транзакциялық жүйеміздегі бизнес логикасының негізгі бөлігі пайдаланушы интерфейстері арқылы деректер базасына деректерді енгізу, бұл деректерді қолмен және автоматтандырылған тексеру, оны қандай да бір жұмыс процесі арқылы жүзеге асыру, оны басқа жүйеге / аналитикалық дерекқорға / мұрағатқа жариялау, есептерді жасау. . Осылайша, тұтынушылар үшін жүйенің негізгі қызметі олардың ішкі бизнес-процестерін автоматтандыру болып табылады.

Ыңғайлы болу үшін біз байланыста «құжат» терминін белгілі бір жұмыс процесін «байланыстыруға» болатын ортақ кілтпен біріктірілген деректер жиынтығының кейбір абстракциясы ретінде қолданамыз.
Бірақ интеграциялық логика туралы не деуге болады? Ақыр соңында, интеграциялық тапсырма жүйенің архитектурасы арқылы жасалады, ол тапсырыс берушінің өтініші бойынша ЕМЕС, бірақ мүлдем басқа факторлардың әсерінен бөліктерге «кесілген»:

  • Конуэй заңына бағынады;
  • басқа өнімдер үшін бұрын әзірленген ішкі жүйелерді қайта пайдалану нәтижесінде;
  • функционалды емес талаптарға негізделген сәулетшінің қалауы бойынша.

Іскерлік логиканы интеграциялық артефактілермен ластамау және қолданбаны әзірлеуші ​​​​жүйенің сәулеттік ландшафтының ерекшеліктерін зерттеу қажеттілігінен құтқару үшін интеграциялық логиканы негізгі жұмыс процесінің іскерлік логикасынан бөлуге үлкен азғыру бар. Бұл тәсілдің бірқатар артықшылықтары бар, бірақ тәжірибе оның тиімсіздігін көрсетеді:

  • интеграциялық мәселелерді шешу, әдетте, негізгі жұмыс процесін жүзеге асыруда кеңейту нүктелерінің шектеулі болуына байланысты синхронды шақырулар түріндегі ең қарапайым опцияларға қайта оралады (синхронды интеграцияның кемшіліктері төменде талқыланады);
  • басқа ішкі жүйеден кері байланыс қажет болғанда интеграциялық артефактілер әлі де негізгі бизнес логикасына енеді;
  • қолданбаны әзірлеуші ​​интеграцияны елемейді және жұмыс процесін өзгерту арқылы оны оңай бұза алады;
  • пайдаланушы көзқарасы бойынша жүйе біртұтас болуды тоқтатады, ішкі жүйелер арасындағы «тігістер» байқалады және деректерді бір ішкі жүйеден екіншісіне тасымалдауды бастайтын артық пайдаланушы операциялары пайда болады.

Басқа тәсіл – интеграциялық өзара әрекеттесулерді негізгі бизнес логикасы мен жұмыс үрдісінің ажырамас бөлігі ретінде қарастыру. Қолданбаларды әзірлеуші ​​біліктілігінің күрт өсуіне жол бермеу үшін жаңа интеграциялық өзара әрекеттесулерді жасау шешімді таңдаудың ең аз мүмкіндігімен оңай және қиындықсыз болуы керек. Мұны істеу көрінгеннен қиынырақ: құрал пайдаланушыға «аяққа атуға» мүмкіндік бермей, оны қолданудың қажетті әртүрлі нұсқаларын қамтамасыз ету үшін жеткілікті күшті болуы керек. Инженер интеграциялық тапсырмалар контекстінде жауап беруі керек көптеген сұрақтар бар, бірақ қолданбаларды әзірлеуші ​​өзінің күнделікті жұмысында бұл туралы ойламауы керек: транзакция шекаралары, жүйелілік, атомдық, қауіпсіздік, масштабтау, жүктеме және ресурстарды бөлу, маршруттау, маршалдау, тарату және ауысу контексттері және т.б.. Қолданбаларды әзірлеушілерге барлық осындай сұрақтарға жауаптар жасырылған жеткілікті қарапайым шешім үлгілерін ұсыну қажет. Бұл үлгілер өте қауіпсіз болуы керек: бизнес логикасы өте жиі өзгереді, бұл қателерді енгізу қаупін арттырады, қателердің құны айтарлықтай төмен деңгейде қалуы керек.

Бірақ оған BPM не қатысы бар? Жұмыс процесін жүзеге асырудың көптеген нұсқалары бар...
Шынында да, бизнес-процестердің тағы бір іске асырылуы біздің шешімдерімізде өте танымал - күйдің ауысу диаграммасының декларативті анықтамасы және өңдеушілердің ауысулардың бизнес логикасымен байланысы арқылы. Бұл жағдайда бизнес-процестегі «құжаттың» ағымдағы орнын анықтайтын күй «құжаттың» атрибуты болып табылады.

BPM стилінің интеграциясы
Процесс жобаның басында осылай көрінеді

Бұл іске асырудың танымалдығы сызықтық бизнес-процестерді құрудың салыстырмалы қарапайымдылығы мен жылдамдығына байланысты. Дегенмен, бағдарламалық қамтамасыз ету жүйелері үнемі күрделене түскен сайын, бизнес-процестің автоматтандырылған бөлігі өсіп, күрделене түседі. Әр тармақ параллель орындалатындай ыдырау, процестердің бөліктерін қайта пайдалану, сонымен қатар тармақталу процестері қажет. Мұндай жағдайларда құрал ыңғайсыз болып, күйдің ауысу диаграммасы өзінің ақпараттық мазмұнын жоғалтады (интеграциялық өзара әрекеттесу диаграммада мүлде көрсетілмейді).

BPM стилінің интеграциясы
Талаптарды нақтылаудың бірнеше итерациясынан кейін процесс осылай көрінеді.

Бұл жағдайдан шығудың жолы қозғалтқышты біріктіру болды jBPM ең күрделі бизнес процестері бар кейбір өнімдерге. Қысқа мерзімді перспективада бұл шешім біршама жетістікке жетті: белгілерде жеткілікті ақпаратты және сәйкес диаграмманы сақтай отырып, күрделі бизнес-процестерді жүзеге асыру мүмкін болды. BPMN2.

BPM стилінің интеграциясы
Күрделі бизнес-процестің шағын бөлігі

Ұзақ мерзімді перспективада шешім үмітті ақтамады: көрнекі құралдар арқылы бизнес-процестерді құрудың жоғары еңбек сыйымдылығы қолайлы өнімділік көрсеткіштеріне қол жеткізуге мүмкіндік бермеді, ал құралдың өзі әзірлеушілер арасында ең ұнамсыз нәрселердің біріне айналды. Сондай-ақ қозғалтқыштың ішкі құрылымына қатысты шағымдар болды, бұл көптеген «патчтар» мен «балдақтардың» пайда болуына әкелді.

JBPM пайдаланудың негізгі оң аспектісі бизнес-процесс данасы жеке тұрақты күйінің болуының пайдасы мен зиянын білу болды. Біз сондай-ақ сигналдар мен хабарламалар арқылы асинхронды өзара әрекеттесулерді пайдалана отырып, әртүрлі қолданбалар арасында күрделі интеграциялық хаттамаларды жүзеге асыру үшін процестік тәсілді пайдалану мүмкіндігін көрдік. Бұл ретте тұрақты мемлекеттің болуы шешуші рөл атқарады.

Жоғарыда айтылғандарға сүйене отырып, қорытынды жасауға болады: BPM стиліндегі процесстік тәсіл бізге барған сайын күрделене түсетін бизнес-процестерді автоматтандыруға, интеграциялық әрекеттерді осы процестерге үйлесімді түрде сәйкестендіруге және іске асырылған процесті қолайлы белгілерде көрнекі түрде көрсету мүмкіндігін сақтауға мүмкіндік береді.

Интеграция үлгісі ретінде синхронды шақырулардың кемшіліктері

Синхронды біріктіру ең қарапайым блоктаушы қоңырауды білдіреді. Бір ішкі жүйе сервер жағы ретінде әрекет етеді және API интерфейсін қажетті әдіспен көрсетеді. Басқа ішкі жүйе клиенттік тарап ретінде әрекет етеді және қажетті уақытта қоңырау шалып, нәтижені күтеді. Жүйенің архитектурасына байланысты клиент және сервер жақтары бір қолданбада және процесте немесе әртүрлі болуы мүмкін. Екінші жағдайда, кейбір RPC іске асыруын қолдану керек және параметрлерді және қоңырау нәтижесін біріктіруді қамтамасыз ету керек.

BPM стилінің интеграциясы

Бұл интеграциялық үлгіде кемшіліктердің жеткілікті үлкен жиынтығы бар, бірақ ол өзінің қарапайымдылығына байланысты практикада өте кең қолданылады. Жүзеге асыру жылдамдығы сізді баурап алады және шешімді техникалық қарыз ретінде жазып, соңғы мерзімдер жағдайында оны қайта-қайта пайдалануға мәжбүр етеді. Бірақ тәжірибесіз әзірлеушілер оны жағымсыз салдарын түсінбей, бейсаналық түрде пайдаланады.

Ішкі жүйе қосылымының ең айқын ұлғаюымен қатар, транзакциялардың «өсу» және «созылу» проблемалары да азырақ. Шынында да, егер бизнес логикасы кейбір өзгерістер енгізсе, онда транзакцияларды болдырмау мүмкін емес, ал транзакциялар, өз кезегінде, осы өзгерістер әсер еткен кейбір қолданба ресурстарын блоктайды. Яғни, бір ішкі жүйе екіншісінен жауап күтпейінше, ол транзакцияны аяқтап, құлыптарды алып тастай алмайды. Бұл әртүрлі әсерлердің қаупін айтарлықтай арттырады:

  • Жүйенің жауап беру қабілеті жоғалады, пайдаланушылар сұрауларға жауаптарды ұзақ күтеді;
  • сервер толып кеткен ағындар пулына байланысты пайдаланушы сұрауларына әдетте жауап беруді тоқтатады: ағындардың көпшілігі транзакциямен қамтылған ресурста құлыпталған;
  • Тығырықтаулар пайда бола бастайды: олардың пайда болу ықтималдығы транзакциялардың ұзақтығына, іскерлік логика мен транзакцияға тартылған құлыптардың көлеміне қатты байланысты;
  • транзакцияның күту уақыты қателері пайда болады;
  • егер тапсырма деректердің үлкен көлемін өңдеуді және өзгертуді қажет етсе, сервер OutOfMemory-мен «сәтсіз болады» және синхронды интеграциялардың болуы өңдеуді «жеңілірек» транзакцияларға бөлуді өте қиындатады.

Архитектуралық тұрғыдан алғанда, интеграциялау кезінде блоктау шақыруларын пайдалану жеке ішкі жүйелердің сапасын бақылауды жоғалтуға әкеледі: бір ішкі жүйенің мақсатты сапа көрсеткіштерін басқа ішкі жүйенің сапа көрсеткіштерінен оқшаулаумен қамтамасыз ету мүмкін емес. Егер ішкі жүйелерді әртүрлі командалар әзірлесе, бұл үлкен мәселе.

Біріктірілетін ішкі жүйелер әртүрлі қолданбаларда болса және екі жағынан синхронды өзгертулер енгізу қажет болса, жағдай одан да қызықты болады. Бұл өзгерістердің транзакциялық болуын қалай қамтамасыз етуге болады?

Егер өзгертулер бөлек транзакцияларда жасалса, онда сіз сенімді ерекшелік өңдеуді және өтеуді қамтамасыз етуіңіз керек және бұл синхронды интеграциялардың негізгі артықшылығын - қарапайымдылықты толығымен жояды.

Бөлінген транзакциялар да ойға келеді, бірақ біз оларды шешімдерімізде қолданбаймыз: сенімділікті қамтамасыз ету қиын.

«Сага» транзакция мәселесінің шешімі ретінде

Микросервистердің танымалдылығының артуына байланысты сұраныс Сага үлгісі.

Бұл үлгі ұзақ транзакциялардың жоғарыда аталған мәселелерін тамаша шешеді, сонымен қатар бизнес логикасы жағынан жүйенің күйін басқару мүмкіндіктерін кеңейтеді: сәтсіз транзакциядан кейінгі өтемақы жүйені бастапқы күйіне қайтармауы мүмкін, бірақ деректерді өңдеудің балама жолы. Бұл сонымен қатар процесті «жақсы» аяқтауға тырысқанда сәтті аяқталған деректерді өңдеу қадамдарын қайталамауға мүмкіндік береді.

Бір қызығы, монолитті жүйелерде бұл үлгі еркін байланысқан ішкі жүйелерді біріктіру кезінде де өзекті болып табылады және ұзаққа созылған транзакциялардан туындаған жағымсыз әсерлер және сәйкес ресурс құлыптары байқалады.

BPM стиліндегі біздің бизнес-процестерге қатысты «Сагаларды» жүзеге асыру өте оңай болып шықты: «Саганың» жеке қадамдарын бизнес-процестегі әрекеттер ретінде көрсетуге болады, сонымен қатар бизнес-процестің тұрақты күйін де көрсетуге болады. «Сағаның» ішкі күйін анықтайды. Яғни, бізге ешқандай қосымша үйлестіру механизмі қажет емес. Сізге тек көлік ретінде «кем дегенде бір рет» кепілдіктерін қолдайтын хабар брокері қажет.

Бірақ бұл шешімнің де өзіндік «бағасы» бар:

  • іскерлік логика күрделене түседі: өтемақы пысықталуы керек;
  • монолитті жүйелер үшін әсіресе сезімтал болуы мүмкін толық консистенциядан бас тарту қажет болады;
  • Архитектура біршама күрделене түседі және хабарлама брокеріне қосымша қажеттілік пайда болады;
  • қосымша бақылау және басқару құралдары қажет болады (бірақ бұл жалпы алғанда жақсы: жүйелік қызмет көрсету сапасы артады).

Монолитті жүйелер үшін «Sag» пайдаланудың негіздемесі соншалықты айқын емес. Микросервистер және басқа SOA үшін, ең алдымен, брокер бар және жобаның басында толық үйлесімділік құрбан болады, бұл үлгіні пайдаланудың артықшылықтары кемшіліктерден айтарлықтай асып кетуі мүмкін, әсіресе бизнес логикасында ыңғайлы API болса. деңгейі.

Микросервистерде бизнес логикасын инкапсуляциялау

Микросервистермен тәжірибе жасай бастағанда, орынды сұрақ туындады: домендік деректердің тұрақтылығын қамтамасыз ететін қызметке қатысты домен бизнес логикасын қайда орналастыру керек?

Әртүрлі BPMS архитектурасын қарастырған кезде, бизнес логикасын тұрақтылықтан бөлу орынды болып көрінуі мүмкін: домен бизнес логикасын орындау үшін орта мен контейнерді құрайтын платформа мен доменге тәуелсіз микросервистердің қабатын жасаңыз және домен деректерінің тұрақтылығын келесідей жобалаңыз. өте қарапайым және жеңіл микросервистердің жеке қабаты. Бизнес-процестер бұл жағдайда тұрақтылық деңгейінің қызметтерін басқаруды орындайды.

BPM стилінің интеграциясы

Бұл тәсілдің өте үлкен артықшылығы бар: сіз платформаның функционалдығын қалағаныңызша арттыра аласыз және платформа микросервистерінің сәйкес қабаты ғана «май» болады. Кез келген домендегі бизнес-процестер платформаның жаңа функционалдығын ол жаңартылған бойда бірден пайдалана алады.

Егжей-тегжейлі зерттеу бұл тәсілдің елеулі кемшіліктерін анықтады:

  • көптеген домендердің іскерлік логикасын бірден орындайтын платформа қызметі бір сәтсіздік нүктесі ретінде үлкен тәуекелдерді тудырады. Іскерлік логиканың жиі өзгеруі жүйелік ақауларға әкелетін қателер қаупін арттырады;
  • өнімділік мәселелері: бизнес логикасы тар және баяу интерфейс арқылы деректермен жұмыс істейді:
    • деректер қайтадан желілік стек арқылы жинақталады және айдалады;
    • домен қызметі көбінесе қызметтің сыртқы API деңгейінде сұрауларды параметрлеу мүмкіндіктерінің жеткіліксіздігіне байланысты өңдеу үшін бизнес логикасы үшін талап етілетіннен көбірек деректерді береді;
    • бизнес логикасының бірнеше тәуелсіз бөліктері бір деректерді өңдеуге қайта-қайта қайта сұрауы мүмкін (бұл мәселені деректерді кэштейтін сеанс құрамдастарын қосу арқылы азайтуға болады, бірақ бұл архитектураны одан әрі қиындатады және деректердің өзектілігі мен кэшті жарамсыз ету мәселелерін тудырады);
  • транзакция мәселелері:
    • платформа қызметі сақтайтын тұрақты күйі бар бизнес-процестер домен деректеріне сәйкес келмейді және бұл мәселені шешудің оңай жолдары жоқ;
    • домен деректерін блоктауды транзакциядан тыс орналастыру: егер домендік бизнес логикасы бірінші рет ағымдағы деректердің дұрыстығын тексергеннен кейін өзгертулер енгізу қажет болса, өңделген деректердің бәсекелестік өзгеруі мүмкіндігін болдырмау қажет. Сыртқы деректерді блоктау мәселені шешуге көмектесуі мүмкін, бірақ мұндай шешім қосымша тәуекелдерді тудырады және жүйенің жалпы сенімділігін төмендетеді;
  • жаңарту кезіндегі қосымша қиындықтар: кейбір жағдайларда тұрақтылық қызметі мен бизнес логикасын синхронды түрде немесе қатаң ретпен жаңарту қажет.

Сайып келгенде, бізге негіздерге оралу керек болды: домен деректерін және домен бизнес логикасын бір микросервиске инкапсуляциялау. Бұл тәсіл микросервисті жүйенің ажырамас құрамдас бөлігі ретінде қабылдауды жеңілдетеді және жоғарыда аталған проблемаларды тудырмайды. Бұл да тегін берілмейді:

  • API стандарттауы бизнес логикасымен (атап айтқанда, бизнес-процестердің бөлігі ретінде пайдаланушы әрекеттерін қамтамасыз ету үшін) және API платформасының қызметтерімен өзара әрекеттесу үшін қажет; API өзгерістеріне, алға және кері үйлесімділікке мұқият назар аударуды талап етеді;
  • әрбір осындай микросервистің бөлігі ретінде бизнес-логиканың жұмыс істеуін қамтамасыз ету үшін қосымша жұмыс уақытының кітапханаларын қосу қажет және бұл мұндай кітапханаларға жаңа талаптарды тудырады: жеңілдігі және транзиттік тәуелділіктердің минималдылығы;
  • бизнес логикасын әзірлеушілер кітапхана нұсқаларын бақылауы керек: егер микросервис ұзақ уақыт бойы аяқталмаса, онда кітапханалардың ескірген нұсқасы болуы мүмкін. Бұл жаңа мүмкіндікті қосуға күтпеген кедергі болуы мүмкін және нұсқалар арасында үйлесімсіз өзгерістер болған жағдайда, мұндай қызметтің ескі бизнес логикасын кітапханалардың жаңа нұсқаларына көшіруді қажет етуі мүмкін.

BPM стилінің интеграциясы

Мұндай архитектурада платформа қызметтерінің қабаты да бар, бірақ бұл қабат енді домен бизнес логикасын орындауға арналған контейнер емес, тек қосалқы «платформа» функцияларын қамтамасыз ететін оның ортасын құрайды. Мұндай қабат домендік микросервистердің жеңіл салмағын сақтау үшін ғана емес, сонымен қатар басқаруды орталықтандыру үшін де қажет.

Мысалы, бизнес процестеріндегі пайдаланушы әрекеттері тапсырмаларды тудырады. Дегенмен, тапсырмалармен жұмыс істеу кезінде пайдаланушы жалпы тізімдегі барлық домендердің тапсырмаларын көруі керек, бұл домен бизнес логикасынан тазартылған сәйкес платформа тапсырмасын тіркеу қызметі болуы керек дегенді білдіреді. Мұндай контексте іскерлік логиканың инкапсуляциясын сақтау өте қиын және бұл архитектураның тағы бір компромиссі.

Бағдарлама әзірлеушісінің көзімен бизнес-процестерді біріктіру

Жоғарыда айтылғандай, бағдарлама әзірлеушісі жақсы даму өнімділігіне сену үшін бірнеше қолданбалардың өзара әрекеттесуін жүзеге асырудың техникалық және инженерлік ерекшеліктерінен абстракциялауы керек.

Мақала үшін арнайы ойлап табылған күрделі интеграциялық мәселені шешуге тырысайық. Бұл үш қолданбаны қамтитын «ойын» тапсырмасы болады, олардың әрқайсысы белгілі бір домен атауын анықтайды: «app1», «app2», «app3».

Әрбір қосымшаның ішінде интеграция шинасы арқылы «доп ойнай» бастайтын бизнес-процестер іске қосылады. «Шар» атауы бар хабарламалар доп ретінде әрекет етеді.

Ойын ережесі:

  • бірінші ойыншы – бастамашы. Ол ойынға басқа ойыншыларды шақырады, ойынды бастайды және оны кез келген уақытта аяқтай алады;
  • басқа ойыншылар ойынға қатысатындарын мәлімдейді, бір-бірін және бірінші ойыншыны «танысады»;
  • допты алғаннан кейін ойыншы басқа қатысушы ойыншыны таңдап, оған допты береді. Берілгендердің жалпы саны есептеледі;
  • Әрбір ойыншының «қуаты» бар, ол сол ойыншының допты әр берген сайын азаяды. Энергия таусылғанда, ойыншы ойыннан шығып кеткенін хабарлайды;
  • егер ойыншы жалғыз қалса, ол бірден өзінің кеткенін хабарлайды;
  • Барлық ойыншылар жойылған кезде, бірінші ойыншы ойынның аяқталғанын жариялайды. Егер ол ойыннан ерте шықса, оны аяқтау үшін ойынды жалғастыру керек.

Бұл мәселені шешу үшін мен бизнес-процестер үшін DSL-ді қолданамын, бұл бізге Котлиндегі логиканы ықшам, ең аз кестемен сипаттауға мүмкіндік береді.

Бірінші ойыншының (ойынның бастамашысы) бизнес процесі 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 арқылы әрекетті бақылау (мысалы, процесті үзу).

Әрбір осындай оқиға бизнес-процесс данасы күйін өзгерте алады: кейбір әрекеттер аяқталып, басқалары басталуы мүмкін және тұрақты сипаттардың мәндері өзгеруі мүмкін. Кез келген әрекетті жабу келесі әрекеттердің біреуін немесе бірнешеуін белсендіруге әкелуі мүмкін. Олар, өз кезегінде, басқа оқиғаларды күтуді тоқтата алады немесе қосымша деректер қажет болмаса, сол транзакцияны аяқтай алады. Транзакцияны жабу алдында бизнес-процестің жаңа күйі дерекқорда сақталады, онда ол келесі сыртқы оқиғаның орын алуын күтеді.

Реляциялық дерекқорда сақталған тұрақты бизнес-процес деректері ТАҢДАУ ҮШІН ЖАҢАРТУ функциясын пайдалансаңыз, өңдеуді синхрондау үшін өте ыңғайлы нүкте болып табылады. Егер бір транзакция бизнес-процестің күйін оны өзгерту үшін базадан ала алса, онда параллельді басқа ешбір транзакция басқа өзгеріс үшін бірдей күйді ала алмайды, ал бірінші транзакция аяқталғаннан кейін екіншісі өзгертілген күйді алуға кепілдік беріледі.

ДҚБЖ жағындағы пессимистік құлыптарды қолдана отырып, біз барлық қажетті талаптарды орындаймыз ACID, сонымен қатар іске қосылған даналардың санын көбейту арқылы қолданбаны іскери логикамен масштабтау мүмкіндігін сақтаңыз.

Дегенмен, пессимистік құлыптар бізді тығырыққа тіреумен қорқытады, бұл бизнес логикасында кейбір өрескел жағдайларда тығырықтар орын алған жағдайда ЖАҢАРТУ ҮШІН ТАҢДАУ әлі де кейбір ақылға қонымды күту уақытымен шектелуі керек дегенді білдіреді.

Тағы бір мәселе - бизнес-процестің басталуын синхрондау. Бизнес процесінің данасы болмаса да, дерекқорда күй жоқ, сондықтан сипатталған әдіс жұмыс істемейді. Белгілі бір аумақта бизнес-процес данасы бірегейлігін қамтамасыз ету қажет болса, онда сізге процесс сыныбымен және сәйкес аумақпен байланысты синхрондау нысанының қандай да бір түрі қажет болады. Бұл мәселені шешу үшін біз сыртқы қызмет арқылы 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

пікір қалдыру