Інтэграцыя ў стылі BPM

Інтэграцыя ў стылі BPM

Прывітанне, хабр!

Наша кампанія спецыялізуецца на распрацоўцы праграмных рашэнняў класа ERP, у складзе якіх ільвіную долю займаюць транзакцыйныя сістэмы з вялізным аб'ёмам бізнес-логікі і дакументазваротам а-ля СЭД. Сучасныя версіі нашых прадуктаў грунтуюцца на тэхналогіях JavaEE, але мы таксама актыўна эксперыментуем з мікрасэрвісамі. Адно з самых праблемных месцаў такіх рашэнняў - інтэграцыя розных падсістэм, якія адносяцца да сумежных даменаў. Задачы інтэграцыі заўсёды дастаўлялі нам велізарны галаўны боль, незалежна ад ужывальных намі архітэктурных стыляў, тэхналагічных стэкаў і фрэймворкаў, аднак у апошні час у рашэнні такіх задач азначыўся прагрэс.

У прапанаваным вашай увазе артыкуле я раскажу пра наяўныя ў НВА «Крышта» досвед і архітэктурныя пошукі ў пазначанай вобласці. Таксама мы разгледзім прыклад простага рашэння інтэграцыйнай задачы з пункту гледжання прыкладнога распрацоўніка і высветлім, што хаваецца за гэтай прастатой.

Адмова ад адказнасці

Апісаныя ў артыкуле архітэктурныя і тэхнічныя рашэнні прапануюцца мной на аснове асабістага досведу ў кантэксце пэўных задач. Гэтыя рашэнні не прэтэндуюць на ўніверсальнасць і могуць апынуцца не аптымальнымі пры іншых умовах выкарыстання.

Пры чым тут BPM?

Для адказу на гэтае пытанне трэба крыху паглыбіцца ў спецыфіку прыкладных задач нашых рашэнняў. Асноўная частка бізнес-логікі ў нашай тыповай транзакцыйнай сістэме - гэта ўвод дадзеных у БД праз карыстацкія інтэрфейсы, ручная і аўтаматызаваная праверка гэтых дадзеных, правядзенне іх па некаторым workflow, публікацыя ў іншую сістэму / аналітычную базу / архіў, фарміраванне справаздач. Такім чынам, ключавой функцыяй сістэмы для заказчыкаў з'яўляецца аўтаматызацыя іх унутраных бізнес-працэсаў.

Для зручнасці мы выкарыстоўваем у зносінах тэрмін "дакумент" як некаторую абстракцыю набору дадзеных, аб'яднаных агульным ключом, да якога можна "прывязаць" пэўны workflow.
Але што рабіць з інтэграцыйнай логікай? Бо задача інтэграцыі спараджаецца архітэктурай сістэмы, якая "распілавана" на часткі НЕ па патрабаванні заказчыка, а пад уплывам зусім іншых фактараў:

  • пад дзеяннем закона Канвея;
  • у выніку паўторнага выкарыстання падсістэм, раней распрацаваных для іншых прадуктаў;
  • па рашэнні архітэктара, зыходзячы з нефункцыянальных патрабаванняў.

Існуе вялікая спакуса аддзяліць інтэграцыйную логіку ад бізнэс-логікі асноўнага workflow, каб не забруджваць бізнэс-логіку інтэграцыйнымі артэфактамі і пазбавіць прыкладнага распрацоўніка ад неабходнасці ўнікаць у асаблівасці архітэктурнага ландшафту сістэмы. У такога падыходу ёсць шэраг пераваг, аднак практыка паказвае яго неэфектыўнасць:

  • рашэнне інтэграцыйных задач звычайна скочваецца да самых простых варыянтаў у выглядзе сінхронных выклікаў з-за абмежаванасці кропак пашырэння ў рэалізацыі асноўнага workflow (аб недахопах сінхроннай інтэграцыі – крыху ніжэй);
  • інтэграцыйныя артэфакты ўсё роўна пранікаюць у асноўную бізнэс-логіку, калі патрабуецца зваротная сувязь з іншай падсістэмы;
  • прыкладны распрацоўшчык ігнаруе інтэграцыю і можа лёгка яе зламаць, змяніўшы workflow;
  • сістэма перастае быць адзіным цэлым з пункта гледжання карыстача, становяцца прыкметныя "швы" паміж падсістэмамі, з'яўляюцца залішнія карыстацкія аперацыі, якія ініцыююць перадачу дадзеных з адной падсістэмы ў іншую.

Іншы падыход - разгляд інтэграцыйных узаемадзеянняў як неад'емнай часткі асноўнай бізнес-логікі і workflow. Каб патрабаванні да кваліфікацыі прыкладных распрацоўшчыкаў не ўзляцелі да нябёсаў, стварэнне новых інтэграцыйных узаемадзеянняў павінна выконвацца лёгка і нязмушана, з мінімальнымі магчымасцямі для выбару спосабу рашэння. Гэта зрабіць складаней, чым здаецца: прылада павінна быць досыць магутным, каб забяспечыць карыстачу неабходнае мноства варыянтаў яго ўжывання і пры гэтым не дазволіць "стрэліць сабе ў нагу". Існуе мноства пытанняў, на якія павінен адказаць інжынер у кантэксце інтэграцыйных задач, але пра якія не павінен задумвацца прыкладны распрацоўшчык у сваёй паўсядзённай працы: межы транзакцый, кансістэнтнасць, атамарнасць, бяспека, маштабаванне, размеркаванне нагрузак і рэсурсаў, роўтынг, маршалінг, распаўсюджванне і пераключэнне кантэкстаў і т. п. Трэба прапанаваць прыкладным распрацоўнікам досыць простыя шаблоны рашэнняў, у якіх ужо схаваныя адказы на ўсе падобныя пытанні. Гэтыя шаблоны павінны быць дастаткова бяспечныя: бізнес-логіка мяняецца вельмі часта, што павышае рызыкі ўнясення памылак, цана памылак павінна заставацца на дастаткова нізкім узроўні.

Але ўсё ж такі пры чым тут BPM? Ёсць жа мноства варыянтаў рэалізацыі workflow…
Сапраўды, у нашых рашэннях вельмі папулярная іншая рэалізацыя бізнес-працэсаў - праз дэкларатыўнае заданне дыяграмы пераходаў станаў і падключэнне апрацоўшчыкаў з бізнес-логікай на пераходы. Пры гэтым стан, якое вызначае бягучае становішча "дакумента" ў бізнес-працэсе, з'яўляецца атрыбутам самога "дакумента".

Інтэграцыя ў стылі BPM
Так выглядае працэс на старце праекту

Папулярнасць такой рэалізацыі абумоўлена адноснай прастатой і хуткасцю стварэння лінейных бізнес-працэсаў. Аднак па меры сталага ўскладнення праграмных сістэм аўтаматызаваная частка бізнэс-працэсу разрастаецца і ўскладняецца. З'яўляецца неабходнасць у дэкампазіцыі, паўторным выкарыстанні частак працэсаў, а таксама ў разгалінаванні працэсаў, каб кожная галінка выконвалася раўналежна. У такіх умовах прылада становіцца няёмкім, а дыяграма пераходаў станаў губляе інфарматыўнасць (інтэграцыйныя ўзаемадзеянні наогул ніяк не адлюстроўваюцца на дыяграме).

Інтэграцыя ў стылі BPM
Так выглядае працэс праз некалькі ітэрацый удакладнення патрабаванняў.

Выйсцем з гэтай сітуацыі стала інтэграцыя рухавічка jBPM у некаторыя прадукты з найбольш складанымі бізнес-працэсамі. У кароткатэрміновай перспектыве гэтае рашэнне мела пэўны поспех: з'явілася магчымасць рэалізацыі складаных бізнес-працэсаў з захаваннем дастаткова інфарматыўнай і актуальнай дыяграмы ў натацыі. BPMN2.

Інтэграцыя ў стылі BPM
Невялікая частка складанага бізнес-працэсу

У доўгатэрміновай перспектыве рашэнне не апраўдала чаканняў: высокая працаёмкасць стварэння бізнес-працэсаў праз візуальныя інструменты не дазволіла дасягнуць прымальных паказчыкаў прадуктыўнасці, а сам інструмент стаў адным з самых нялюбых сярод распрацоўшчыкаў. Да ўнутранай прылады рухавічка таксама былі прэтэнзіі, якія прывялі да з'яўлення мноства "латак" і "мыліц".

Галоўным дадатным момантам ужывання jBPM стала ўсведамленне карысці і шкоды ад наяўнасці ўласнага персістэнтнага стану ў асобніка бізнэс-працэсу. Таксама мы ўбачылі магчымасць ужывання працэснага падыходу для рэалізацыі складаных пратаколаў інтэграцыі паміж рознымі прыкладаннямі з ужываннем асінхронных узаемадзеянняў праз сігналы і паведамленні. Наяўнасць персістэнтнага стану гуляе ў гэтым найважную ролю.

На падставе сказанага можна зрабіць выснову: працэсны падыход у стылі BPM дазваляе нам вырашаць шырокі спектр задач па аўтаматызацыі пастаянна ўскладняюцца бізнес-працэсаў, гарманічна ўпісваць у гэтыя працэсы інтэграцыйныя актыўнасці і захоўваць магчымасць візуальнага адлюстравання рэалізаванага працэсу ў прыдатнай для гэтага натацыі.

Недахопы сінхронных выклікаў як інтэграцыйнага патэрна

Пад сінхроннай інтэграцыяй разумеецца найпросты блакуючы выклік. Адна падсістэма выступае серверным бокам і выстаўляе API з патрэбным метадам. Іншая падсістэма выступае кліенцкім бокам і ў патрэбны момант выконвае выклік з чаканнем выніку. У залежнасці ад архітэктуры сістэмы кліенцкая і серверная бакі могуць размяшчацца або ў адным дадатку і працэсе, або ў розных. У другім выпадку патрабуецца прымяніць некаторую рэалізацыю RPC і забяспечыць маршалінг параметраў і выніку выкліку.

Інтэграцыя ў стылі BPM

Такі інтэграцыйны патэрн валодае дастаткова вялікім наборам недахопаў, але ён вельмі шырока выкарыстоўваецца на практыцы ў сілу сваёй прастаты. Хуткасць рэалізацыі падкупляе і прымушае прымяняць яго зноў і зноў ва ўмовах "палаючых" тэрмінаў, запісваючы рашэнне ў тэхнічны доўг. Але бывае і так, што неспрактыкаваныя распрацоўшчыкі ўжываюць яго неўсвядомлена, проста не здагадваючыся аб негатыўных наступствах.

Апроч найболей відавочнага падвышэння складнасці падсістэм, ёсць і меней відавочныя праблемы з «растарошчваннем» і «расцягам» транзакцый. Сапраўды, калі бізнес-логіка ўносіць нейкія змены, тады не абысціся без транзакцый, а транзакцыі, у сваю чаргу, блакуюць пэўныя рэсурсы дадатку, якія закранаюцца гэтымі зменамі. Гэта значыць, пакуль адна падсістэма не дачакаецца адказу ад іншай, яна не зможа завяршыць транзакцыю і зняць блакіроўкі. Гэта істотна павялічвае рызыку ўзнікнення разнастайных эфектаў:

  • губляецца спагадлівасць сістэмы, карыстачы падоўгу чакаюць адказаў на запыты;
  • сервер наогул перастае адказваць на запыты карыстальнікаў з-за перапоўненага пула патокаў: большасць патокаў "усталі" на блакіроўцы рэсурсу, занятага транзакцыяй;
  • пачынаюць з'яўляцца дэдлакі: верагоднасць іх з'яўлення моцна залежыць ад працягласці транзакцый, колькасці ўцягнутай у транзакцыю бізнес-логікі і блакіровак;
  • з'яўляюцца памылкі заканчэння таймаўту транзакцыі;
  • сервер "падае" па OutOfMemory, калі задача патрабуе апрацоўкі і змены вялікіх аб'ёмаў дадзеных, а наяўнасць сінхронных інтэграцый моцна абцяжарвае драбненне апрацоўкі на лягчэйшыя" транзакцыі.

З архітэктурнага пункта гледжання ўжыванне блакавальных выклікаў пры інтэграцыі прыводзіць да страты кіравання якасцю асобных падсістэм: немагчыма забяспечыць мэтавыя паказчыкі якасці адной падсістэмы ў адрыве ад паказчыкаў якасці іншай падсістэмы. Калі падсістэмы распрацоўваюцца рознымі камандамі, гэта вялікая праблема.

Усё становіцца яшчэ цікавей, калі інтэграваныя падсістэмы знаходзяцца ў розных дадатках і трэба ўнесці сінхронныя змены з двух бакоў. Як забяспечыць транзакцыйнасць гэтых змен?

Калі змены ўносяцца паасобнымі транзакцыямі, тады запатрабуецца забяспечыць надзейную апрацоўку выключэнняў і кампенсаванні, а гэта цалкам нівелюе асноўную перавагу сінхронных інтэграцый - прастату.

На розум таксама прыходзяць размеркаваныя транзакцыі, але мы іх не выкарыстоўваем у сваіх рашэннях: складана забяспечыць надзейнасць.

"Сага" як вырашэнне праблемы транзакцый

З ростам папулярнасці мікрасэрвісаў усё большую запатрабаванасць набывае Saga Pattern.

Дадзены шаблон выдатна вырашае пазначаныя вышэй праблемы доўгіх транзакцый, а таксама пашырае магчымасці кіравання станам сістэмы са боку бізнэс-логікі: кампенсаванне пасля няўдалай транзакцыі можа не адкочваць сістэму ў зыходны стан, а забяспечваць альтэрнатыўны маршрут апрацоўкі дадзеных. Гэта таксама дазваляе не паўтараць паспяхова завершаныя крокі апрацоўкі дадзеных пры паўторных спробах давесці працэс да "добрага" фіналу.

Што цікава, у маналітных сістэмах гэты шаблон таксама актуальны, калі гаворка ідзе аб інтэграцыі слаба звязаных падсістэм і назіраюцца негатыўныя эфекты, выкліканыя працяглымі транзакцыямі і адпаведнымі блакіроўкамі рэсурсаў.

У дачыненні да нашых бізнес-працэсаў у стылі BPM імплементаваць "Сагі" аказваецца вельмі лёгка: асобныя крокі "Сагі" могуць быць зададзены ў выглядзе актыўнасцяў ўнутры бізнес-працэсу, а персістэнтнае стан бізнес-працэсу вызначае ў тым ліку ўнутраны стан "Сагі". Гэта значыць, нам не патрабуецца ніякага дадатковага каардынацыйнага механізму. Спатрэбіцца толькі брокер паведамленняў з падтрымкай "at least once" гарантый у якасці транспарту.

Але і ў такога рашэння ёсць свая «кошт»:

  • бізнес-логіка становіцца больш складанай: трэба адпрацоўваць кампенсацыі;
  • запатрабуецца адмовіцца ад full consistency, што можа быць асоба адчувальным для маналітных сістэм;
  • трохі ўскладняецца архітэктура, з'яўляецца дадатковае запатрабаванне ў брокеры паведамленняў;
  • спатрэбяцца дадатковыя сродкі маніторынгу і адміністравання (хоць у цэлым гэта нават добра: якасць абслугоўвання сістэмы павысіцца).

Для маналітных сістэм апраўданасць выкарыстання "Саг" не так відавочная. Для мікрасэрвісаў і іншых SOA, дзе, хутчэй за ўсё, ужо ёсць брокер, а full consistency прынесена ў ахвяру яшчэ на старце праекту, карысць ад выкарыстання гэтага шаблону можа значна пераважыць недахопы, асабліва пры наяўнасці зручнай API на ўзроўні бізнэс-логікі.

Інкапсуляцыя бізнес-логікі ў мікрасэрвісах

Калі мы пачалі эксперыментаваць з мікрасэрвісамі, узнікла слушнае пытанне: куды змяшчаць даменную бізнес-логіку адносна сэрвісу, які забяспечвае персістэнцыю даменных дадзеных?

Пры поглядзе на архітэктуру розных BPMS можа здацца разумным аддзяліць бізнес-логіку ад персістэнцыі: стварыць пласт платформенных і даменна-незалежных мікрасэрвісаў, якія фарміруюць асяроддзе і кантэйнер для выканання даменнай бізнес-логікі, а персістэнцыю даменных дадзеных аформіць асобным пластом з вельмі простых і легкаважных мікрасэрвісаў. Бізнес-працэсы ў такім разе выконваюць аркестроўку сэрвісаў пласта персістэнцыі.

Інтэграцыя ў стылі BPM

У такога падыходу ёсць вельмі вялікі плюс: можна колькі заўгодна нарошчваць функцыянальнасць платформы, і "таўсцець" ад гэтага будзе толькі які адпавядае пласт платформенных мікрасэрвісаў. Бізнэс-працэсы з любога дамена адразу атрымліваюць магчымасць выкарыстоўваць новую функцыянальнасць платформы, як толькі яна будзе абноўлена.

Больш дэталёвая прапрацоўка выявіла істотныя недахопы такога падыходу:

  • платформавы сэрвіс, які выконвае бізнэс-логіку адразу шматлікіх даменаў, нясе ў сабе вялікія рызыкі як адзіная кропка адмовы. Частыя змены бізнес-логікі павышаюць рызыку ўзнікнення памылак, якія прыводзяць да збояў, якія распаўсюджваюцца на ўсю сістэму;
  • праблемы прадукцыйнасці: бізнес-логіка працуе са сваімі дадзенымі праз вузкі і павольны інтэрфейс:
    • дадзеныя будуць лішні раз маршаліцца і прапампоўвацца праз сеткавы стэк;
    • даменны сэрвіс часцяком будзе аддаваць больш дадзеных, чым патрабуецца бізнэс-логіцы для апрацоўкі, з-за недастатковых магчымасцяў параметрызацыі запытаў на ўзроўні вонкавай API сэрвісу;
    • некалькі незалежных частак бізнес-логікі могуць паўторна перазапытваць адны і тыя ж дадзеныя для апрацоўкі (можна змякчыць гэтую праблему даданнем сесійных кампанентаў, якія кэшуюць дадзеныя, але гэта дадаткова ўскладняе архітэктуру і стварае праблемы актуальнасці дадзеных і інвалідацыі кэша);
  • праблемы транзакцыйнасці:
    • бізнес-працэсы з персістэнтным станам, захоўваннем якога займаецца платформавы сэрвіс, разузгадняюцца з даменнымі дадзенымі, і простых шляхоў рашэння гэтай праблемы не прадбачыцца;
    • вынясенне блакіроўкі даменных дадзеных за межы транзакцыі: калі даменнай бізнес-логіцы патрабуецца ўнесці змены, папярэдне выканаўшы праверку карэктнасці актуальных дадзеных, патрабуецца выключыць магчымасць канкурэнтнай змены апрацоўваных дадзеных. Вонкавае блакаванне дадзеных можа дапамагчы вырашыць задачу, але такое рашэнне нясе ў сабе дадатковыя рызыкі і змяншае агульную надзейнасць сістэмы;
  • дадатковыя складанасці пры абнаўленні: у шэрагу выпадкаў абнаўляць сэрвіс персістэнцыі і бізнэс-логіку трэба сінхронна ці ў строгай паслядоўнасці.

У канчатковым выніку прыйшлося вярнуцца да вытокаў: інкапсуляваць даменныя дадзеныя і даменную бізнэс-логіку ў адзін мікрасэрвіс. Такі падыход спрашчае ўспрыманне мікрасэрвісу як цэласнага кампанента ў складзе сістэмы і не спараджае вышэйпералічаныя праблемы. Гэта таксама даецца не бясплатна:

  • патрабуецца стандартызацыя API для ўзаемадзеяння з бізнес-логікай (у прыватнасці, для забеспячэння карыстацкіх актыўнасцей у складзе бізнес-працэсаў) і API-платформавых сэрвісаў; патрабуецца больш уважлівае стаўленне да змены API, прамой і зваротнай сумяшчальнасці;
  • патрабуецца даданне дадатковых runtime-бібліятэк для забеспячэння функцыянавання бізнес-логікі ў складзе кожнага такога мікрасэрвісу, і гэта спараджае новыя патрабаванні да такіх бібліятэк: легкаважнасць і мінімум транзітыўных залежнасцяў;
  • распрацоўшчыкам бізнес-логікі неабходна сачыць за версіямі бібліятэк: калі нейкі мікрасэрвіс даўно не дапрацоўвалі, то ў ім, хутчэй за ўсё, апынецца састарэлая версія бібліятэк. Гэта можа стаць нечаканай перашкодай для дадання новай фічы і можа запатрабаваць міграцыі старой бізнэс-логікі такога сэрвісу на новыя версіі бібліятэк, калі паміж версіямі былі несумяшчальныя змены.

Інтэграцыя ў стылі BPM

Пласт платформенных сэрвісаў у такой архітэктуры таксама прысутнічае, але гэты пласт фармуе ўжо не кантэйнер для выканання даменнай бізнес-логікі, а ўсяго толькі яе асяроддзе, падаючы дапаможныя "платформенныя" функцыі. Такі пласт патрэбен не толькі для захавання легкаважнасці даменных мікрасэрвісаў, але і для цэнтралізацыі кіравання.

Напрыклад, карыстацкія актыўнасці ў бізнес-працэсах спараджаюць задачы. Аднак, працуючы з задачамі, карыстач павінен бачыць задачы з усіх даменаў у агульным спісе, а значыць, павінен быць які адпавядае платформавы сэрвіс рэгістрацыі задач, вычышчаны ад даменнай бізнэс-логікі. Захаваць інкапсуляцыю бізнес-логікі ў такім кантэксце дастаткова праблематычна, і гэта яшчэ адзін кампраміс дадзенай архітэктуры.

Інтэграцыя бізнес-працэсаў вачыма прыкладнога распрацоўшчыка

Як ужо сказана вышэй, прыкладны распрацоўшчык павінен быць абстрагаваны ад тэхнічных і інжынерных асаблівасцяў рэалізацыі ўзаемадзеяння некалькіх прыкладанняў, каб можна было разлічваць на добрую прадуктыўнасць распрацоўкі.

Паспрабуем вырашыць дастаткова няпростую інтэграцыйную задачу, спецыяльна прыдуманую для артыкула. Гэта будзе "гульнявая" задача з удзелам трох прыкладанняў, дзе кожнае з іх вызначае некаторае даменнае імя: "app1", "app2", "app3".

Унутры кожнага прыкладання запускаюцца бізнес-працэсы, якія пачынаюць "гуляць у мяч" праз інтэграцыйную шыну. У ролі мяча будуць выступаць паведамленні з імем Ball.

Правілы гульні:

  • першы гулец - ініцыятар. Ён запрашае іншых гульцоў у гульню, пачынае гульню і можа яе ў любы момант скончыць;
  • іншыя гульцы заяўляюць аб сваім удзеле ў гульні, "знаёмяцца" адзін з адным і першым гульцом;
  • прыняўшы мяч, гулец выбірае іншага які ўдзельнічае гульца і перадае яму мяч. Вядзецца падлік агульнай колькасці перадач;
  • у кожнага гульца ёсць "энергія", якая памяншаецца з кожнай перадачай мяча гэтым гульцом. Па заканчэнні энергіі гулец выбывае з гульні, заяўляючы аб сваім сыходзе;
  • калі гулец застаўся адзін, ён адразу заяўляе аб сыходзе;
  • калі ўсе гульцы выбываюць, першы гулец заяўляе аб завяршэнні гульні. Калі ён выбыў з гульні раней, то застаецца сачыць за гульнёй, каб завяршыць яе.

Для рашэння гэтай задачы я скарыстаюся нашым DSL для бізнэс-працэсаў, якія дазваляюць апісаць логіку на Kotlin кампактна, з мінімумам бойлерплейта.

У дадатку app1 будзе працаваць бізнэс-працэс першага гульца (ён жа ініцыятар гульні):

class 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 будзе ўключаць бізнэс-працэс іншага гульца:

class RandomPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

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

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

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

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

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

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

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

Дыяграма:

Інтэграцыя ў стылі BPM

У дадатку app3 зробім гульца крыху з іншымі паводзінамі: замест выпадковага выбару наступнага гульца, ён будзе дзейнічаць па алгарытме round-robin:

class 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;
}

Запускаем тэст, глядзім лог:

console output

Взята блокировка ключа 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!

З усяго гэтага можна зрабіць некалькі важных высноў:

  • пры наяўнасці неабходных інструментаў прыкладныя распрацоўшчыкі могуць ствараць інтэграцыйныя ўзаемадзеянні паміж праграмамі без адрыву ад бізнес-логікі;
  • складанасць (complexity) інтэграцыйнай задачы, якая патрабуе інжынерных кампетэнцый, можна схаваць усярэдзіне фреймворка, калі першапачаткова закласці гэта ў архітэктуру фреймворка. Цяжкасць задачы (difficulty) схаваць не атрымаецца, таму рашэнне цяжкай задачы ў кодзе будзе выглядаць адпаведна;
  • пры распрацоўцы інтэграцыйнай логікі абавязкова трэба ўлічваць eventually consistency і адсутнасць лінеарызуемага змены стану ўсіх удзельнікаў інтэграцыі. Гэта змушае ўскладняць логіку, каб зрабіць яе неадчувальнай да парадку ўзнікнення вонкавых падзей. У нашым прыкладзе гулец змушаны прымаць удзел у гульні ўжо пасля таго, як ён заявіць аб выхадзе з гульні: іншыя гульцы будуць працягваць перадаваць яму мяч, пакуль інфармацыя аб яго выхадзе не дойдзе і не апрацуецца ўсімі ўдзельнікамі. Гэтая логіка не выцякае з правіл гульні і з'яўляецца кампрамісным рашэннем у рамках абранай архітэктуры.

Далей пагаворым аб розных тонкасцях нашага рашэння, кампрамісах і іншых момантах.

Усе паведамленні - у адной чарзе

Усе інтэгравальныя прыкладанні працуюць з адной інтэграцыйнай шынай, якая прадстаўлена ў выглядзе вонкавага брокера, адной чаргі BPMQueue - для паведамленняў і аднаго топіка BPMTopic - для сігналаў (падзей). Прапускаць усе паведамленні праз адну чаргу само па сабе з'яўляецца кампрамісам. На ўзроўні бізнес-логікі зараз можна ўводзіць колькі заўгодна новых тыпаў паведамленняў, не ўносячы змен у структуру сістэмы. Гэта значнае спрашчэнне, але яно нясе ў сабе пэўныя рызыкі, якія ў кантэксце нашых тыпавых задач падаліся нам не такімі ўжо значнымі.

Інтэграцыя ў стылі BPM

Аднак тут ёсць адна тонкасць: кожнае прыкладанне адфільтроўвае "свае" паведамленні з чаргі яшчэ на ўваходзе, па імі свайго дамена. Таксама дамен можа быць паказаны і ў сігналах, калі трэба абмежаваць "вобласць бачнасці" сігналу адным адзіным дадаткам. Гэта павінна павялічыць прапускную здольнасць шыны, але бізнэс-логіка зараз павінна апераваць імёнамі даменаў: для адрасавання паведамленняў - абавязкова, для сігналаў - пажадана.

Забеспячэнне надзейнасці інтэграцыйнай шыны

Надзейнасць складаецца з некалькіх момантаў:

  • абраны брокер паведамленняў - крытычна важны кампанент архітэктуры і адзіная кропка адмовы: ён павінен быць дастаткова адмоваўстойлівасцю. Варта выкарыстоўваць толькі правераныя часам рэалізацыі, з добрай падтрымкай і вялікім кам'юніці;
  • неабходна забяспечыць высокую даступнасць брокера паведамленняў, для чаго ён павінен быць фізічна аддзелены ад інтэграваных прыкладанняў (высокую даступнасць прыкладанняў з прыкладной бізнес-логікай забяспечыць значна складаней і даражэй);
  • брокер абавязаны забяспечыць "at least once" гарантыі дастаўкі. Гэта абавязковае патрабаванне для надзейнай працы інтэграцыйнай шыны. У гарантыях узроўню "exactly once" няма неабходнасці: бізнес-працэсы, як правіла, не адчувальныя да паўторнага паступлення паведамленняў або падзей, а ў асаблівых задачах, дзе гэта важна, прасцей дадаць дадатковую праверку ў бізнес-логіку, чым пастаянна выкарыстоўваць дастаткова "дарагія" » гарантыі;
  • адпраўку паведамленняў і сігналаў неабходна залучаць у агульную транзакцыю са зменай стану бізнэс-працэсаў і даменных дадзеных. Пераважным варыянтам будзе выкарыстанне патэрна Transactional Outbox, але яно запатрабуе наяўнасці дадатковай табліцы ў базе і рэтранслятара. У JEE-прыкладаннях можна спрасціць гэты момант з выкарыстаннем лакальнага JTA-мэнэджара, але падлучэнне да абранага брокера павінна ўмець працаваць у рэжыме XA;
  • апрацоўшчыкі ўваходных паведамленняў і падзей таксама павінны працаваць з транзакцыяй змены стану бізнэс-працэсу: калі такая транзакцыя адкочваецца, то і прыём паведамлення павінен быць адменены;
  • паведамленні, якія не ўдалося даставіць з-за памылак, трэба складаць у асобнае сховішча DLQ (Dead Letter Queue). Мы для гэтага стварылі асобны платформавы мікрасэрвіс, які захоўвае такія паведамленні ў сваім сховішчы, індэксуе іх па атрыбутах (для хуткай групоўкі і пошуку), і выстаўляе API для прагляду, паўторнай адпраўкі па адрасе прызначэння, выдаленні паведамленняў. Адміністратары сістэмы могуць працаваць з гэтым сэрвісам праз свой вэб-інтэрфейс;
  • у наладах брокера трэба падбудаваць колькасць паўторных спроб дастаўкі і затрымкі паміж дастаўкамі, каб паменшыць верагоднасць траплення паведамленняў у DLQ (вылічыць аптымальныя параметры практычна нерэальна, але можна дзейнічаць па-эмпірычнаму і падладжваць іх па ходзе эксплуатацыі);
  • сховішча DLQ павінна бесперапынна маніторыцца, і сістэма маніторынгу павінна апавяшчаць адміністратараў сістэмы, каб пры з'яўленні недастаўленых паведамленняў рэагаваць як мага хутчэй. Гэта дазволіць паменшыць «зону паражэння» збою або памылкі бізнес-логікі;
  • інтэграцыйная шына павінна быць неадчувальная да часовай адсутнасці прыкладанняў: падпіскі на топік павінны быць durable, а даменнае імя прыкладання павінна быць унікальна, каб за час адсутнасці прыкладання яго паведамлення з чаргі не паспрабаваў апрацаваць хтосьці іншы.

Забеспячэнне патокабяспекі бізнес-логікі

Аднаму і таму ж асобніку бізнес-працэсу можа паступіць адразу некалькі паведамленняў і падзей, апрацоўка якіх запусціцца паралельна. У той жа час для прыкладнога распрацоўніка ўсё павінна быць проста і струменебяспечна.

Бізнес-логіка працэсу апрацоўвае кожную знешнюю падзею, якая ўплывае на гэты бізнес-працэс, па асобнасці. Такімі падзеямі могуць быць:

  • запуск экзэмпляра бізнес-працэсу;
  • дзеянне карыстальніка, якое адносіцца да актыўнасці ўнутры бізнес-працэсу;
  • паступленне паведамлення або сігналу, на якое падпісаны экзэмпляр бізнес-працэсу;
  • спрацоўванне таймера, устаноўленага экзэмплярам бізнес-працэсу;
  • кіравальнае ўздзеянне праз API (напрыклад, аварыйнае перапыненне працэсу).

Кожная такая падзея можа змяніць стан асобніка бізнес-працэсу: могуць завяршыцца адны актыўнасці і пачацца іншыя, могуць змяніцца значэнні персістэнтных уласцівасцяў. Закрыццё любой актыўнасці можа прывесці да актывацыі адной ці некалькіх наступных актыўнасцяў. Тыя, у сваю чаргу, могуць спыніцца на чаканні іншых падзей або, калі ім не патрэбны ніякія дадатковыя дадзеныя, могуць завяршыцца ў той жа транзакцыі. Перад закрыццём транзакцыі новы стан бізнес-працэсу захоўваецца ў БД, дзе будзе чакаць наступлення наступнай знешняй падзеі.

Персістэнтныя дадзеныя бізнэс-працэсу, захаваныя ў рэляцыйную БД, з'яўляюцца вельмі зручнай кропкай сінхранізацыі апрацоўкі, калі выкарыстоўваць SELECT FOR UPDATE. Калі адной транзакцыі ўдалося атрымаць стан бізнес-працэсу з базы для яго змянення, то ніякая іншая транзакцыя паралельна не зможа атрымаць гэты ж самы стан для іншай змены, а пасля завяршэння першай транзакцыі другая гарантавана атрымае ўжо зменены стан.

Выкарыстоўваючы песімістычныя блакіроўкі на баку СКБД, мы выконваем усе неабходныя патрабаванні ACID, а таксама захоўваем магчымасць маштабавання прыкладання з бізнес-логікай шляхам павелічэння колькасці запушчаных экзэмпляраў.

Аднак песімістычныя блакаванні пагражаюць нам дэдлакамі, а значыць, SELECT FOR UPDATE усёткі варта абмежаваць некаторым разумным таймаўтам на выпадак узнікнення дэдлакаў на якіх-небудзь абуральных кейсах у бізнэс-логіцы.

Яшчэ адна праблема - сінхранізацыя старту бізнес-працэсу. Пакуль няма асобніка бізнэс-працэсу, няма і яго стану ў базе, таму апісаны метад не падыдзе. Калі трэба забяспечыць унікальнасць асобніка бізнэс-працэсу ў вызначанай скоупе, тады запатрабуецца некаторы аб'ект сінхранізацыі, асацыяваны з класам працэсу і якая адпавядае скоупам. Для вырашэння гэтай праблемы мы выкарыстоўваем іншы механізм блакіровак, які дазваляе ўзяць блакіроўку адвольнага рэсурсу, зададзенага ключом у фармаце URI, праз вонкавы сэрвіс.

У нашых прыкладах бізнес-працэс InitialPlayer змяшчае аб'яву

uniqueConstraint = UniqueConstraints.singleton

Таму ў логу прысутнічаюць паведамленні аб узяцці і вызваленні блакавання адпаведнага ключа. Па іншых бізнэс-працэсам такіх паведамленняў няма: uniqueConstraint не зададзены.

Праблемы бізнес-працэсаў з персістэнтным станам

Часам наяўнасць персістэнтнага стану не толькі дапамагае, але і вельмі мяшае ў распрацоўцы.
Праблемы пачынаюцца, калі трэба занесці змены ў бізнэс-логіку і/ці мадэль бізнэс-працэсу. Не любая такая змена апыняецца сумяшчальным са старым станам бізнэс-працэсаў. Калі ў базе дадзеных ёсць шмат "жывых" асобнікаў, тады занясенне несумяшчальных змен можа даставіць масу непрыемнасцяў, з якімі мы часта сутыкаліся пры выкарыстанні jBPM.

У залежнасці ад глыбіні змен можна дзейнічаць двума шляхамі:

  1. стварыць новы тып бізнес-працэсу, каб не ўносіць несумяшчальных змен у стары, і выкарыстоўваць яго замест старога пры запуску новых асобнікаў. Старыя экзэмпляры будуць працягваць працаваць "па-старому";
  2. міграваць персістэнтнае стан бізнес-працэсаў пры абнаўленні бізнес-логікі.

Першы шлях прасцейшы, але мае свае абмежаванні і недахопы, напрыклад:

  • дубліраванне бізнес-логікі ў многіх мадэлях бізнес-працэсаў, павелічэнне аб'ёму бізнес-логікі;
  • часта патрабуецца маментальны пераход на новую бізнес-логіку (у частцы інтэграцыйных задач - амаль заўсёды);
  • распрацоўшчык не ведае, у які момант можна выдаляць састарэлыя мадэлі.

На практыцы мы выкарыстоўваем абодва падыходы, але прынялі шэраг рашэнняў, каб спрасціць сабе жыццё:

  • у базе дадзеных персістэнтны стан бізнэс-працэсу захоўваецца ў лёгка чытэльным і лёгка апрацоўваным выглядзе: у радку фармату JSON. Гэта дазваляе выконваць міграцыі як усярэдзіне прыкладання, так і звонку. У крайнім выпадку можна і ручкамі падправіць (асабліва карысна ў распрацоўцы падчас адладкі);
  • інтэграцыйная бізнэс-логіка не выкарыстоўвае імёны бізнэс-працэсаў, каб у любы момант можна было замяніць рэалізацыю аднаго з якія ўдзельнічаюць працэсаў на новую, з новым імем (напрыклад, «InitialPlayerV2»). Звязванне адбываецца праз імёны паведамленняў і сігналаў;
  • мадэль працэсу мае нумар версіі, які мы павялічваем, калі ўносім у гэтую мадэль несумяшчальныя змены, і гэты нумар захоўваецца разам са станам асобніка працэсу;
  • персістэнтны стан працэсу вычытваецца з базы спачатку ў зручную аб'ектную мадэль, з якой можа працаваць працэдура міграцыі, калі змяніўся нумар версіі мадэлі;
  • працэдура міграцыі размяшчаецца побач з бізнес-логікай і выклікаецца "гультаявата" для кожнага экзэмпляра бізнес-працэсу ў момант яго аднаўлення з базы;
  • калі трэба міграваць стан усіх асобнікаў працэсу аператыўна і сінхронна, ужываюцца больш класічныя рашэнні па міграцыі БД, але тамака прыходзіцца працаваць з JSON.

Ці патрэбен яшчэ адзін фрэймворк для бізнэс-працэсаў?

Апісаныя ў артыкуле рашэнні дазволілі нам прыкметна спрасціць сабе жыццё, пашырыць круг пытанняў, развязальных на ўзроўні прыкладнай распрацоўкі, зрабіць больш прывабнымі ідэі вылучэння бізнэс-логікі ў мікрасэрвісы. Для гэтага было праведзена шмат працы, створаны вельмі «лёгаважны» фрэймворк для бізнэс-працэсаў, а таксама службовыя кампаненты для рашэння пазначаных праблем у кантэксце шырокага круга прыкладных задач. У нас ёсць жаданне падзяліцца гэтымі вынікамі, вынесці распрацоўку агульных кампанентаў у адкрыты доступ пад свабоднай ліцэнзіяй. Гэта запатрабуе пэўных намаганняў і часу. Разуменне запатрабаванасці такіх рашэнняў магло б стаць для нас дадатковым стымулам. У прапанаваным артыкуле вельмі мала ўвагі нададзена магчымасцям самога фрэймворка, але некаторыя з іх бачныя з прадстаўленых прыкладаў. Калі мы ўсё ж такі апублікуем свой фрэймворк, яму будзе прысвечаны асобны артыкул. А пакуль будзем удзячныя, калі пакінеце невялікі фідбэк, адказаўшы на пытанне:

Толькі зарэгістраваныя карыстачы могуць удзельнічаць у апытанні. Увайдзіце, Калі ласка.

ці патрэбен яшчэ адзін фрэймворк для бізнэс-працэсаў?

  • 18,8%так, даўно шукаем нешта падобнае3

  • 12,5%цікава даведацца пра вашу рэалізацыю пабольш, можа спатрэбіцца2

  • 6,2%выкарыстоўваем адзін з існуючых фрэймворкаў, але падумваем аб замене1

  • 18,8%выкарыстоўваем адзін з існуючых фрэймворкаў, усё задавальняе3

  • 18,8%спраўляемся без фрэймворка3

  • 25,0%пішам свой4

Прагаласавалі 16 карыстальнікаў. Устрымаліся 7 карыстальнікаў.

Крыніца: habr.com

Дадаць каментар