Наша кампанія спецыялізуецца на распрацоўцы праграмных рашэнняў класа ERP, у складзе якіх ільвіную долю займаюць транзакцыйныя сістэмы з вялізным аб'ёмам бізнес-логікі і дакументазваротам а-ля СЭД. Сучасныя версіі нашых прадуктаў грунтуюцца на тэхналогіях JavaEE, але мы таксама актыўна эксперыментуем з мікрасэрвісамі. Адно з самых праблемных месцаў такіх рашэнняў - інтэграцыя розных падсістэм, якія адносяцца да сумежных даменаў. Задачы інтэграцыі заўсёды дастаўлялі нам велізарны галаўны боль, незалежна ад ужывальных намі архітэктурных стыляў, тэхналагічных стэкаў і фрэймворкаў, аднак у апошні час у рашэнні такіх задач азначыўся прагрэс.
У прапанаваным вашай увазе артыкуле я раскажу пра наяўныя ў НВА «Крышта» досвед і архітэктурныя пошукі ў пазначанай вобласці. Таксама мы разгледзім прыклад простага рашэння інтэграцыйнай задачы з пункту гледжання прыкладнога распрацоўніка і высветлім, што хаваецца за гэтай прастатой.
Адмова ад адказнасці
Апісаныя ў артыкуле архітэктурныя і тэхнічныя рашэнні прапануюцца мной на аснове асабістага досведу ў кантэксце пэўных задач. Гэтыя рашэнні не прэтэндуюць на ўніверсальнасць і могуць апынуцца не аптымальнымі пры іншых умовах выкарыстання.
Пры чым тут BPM?
Для адказу на гэтае пытанне трэба крыху паглыбіцца ў спецыфіку прыкладных задач нашых рашэнняў. Асноўная частка бізнес-логікі ў нашай тыповай транзакцыйнай сістэме - гэта ўвод дадзеных у БД праз карыстацкія інтэрфейсы, ручная і аўтаматызаваная праверка гэтых дадзеных, правядзенне іх па некаторым workflow, публікацыя ў іншую сістэму / аналітычную базу / архіў, фарміраванне справаздач. Такім чынам, ключавой функцыяй сістэмы для заказчыкаў з'яўляецца аўтаматызацыя іх унутраных бізнес-працэсаў.
Для зручнасці мы выкарыстоўваем у зносінах тэрмін "дакумент" як некаторую абстракцыю набору дадзеных, аб'яднаных агульным ключом, да якога можна "прывязаць" пэўны workflow.
Але што рабіць з інтэграцыйнай логікай? Бо задача інтэграцыі спараджаецца архітэктурай сістэмы, якая "распілавана" на часткі НЕ па патрабаванні заказчыка, а пад уплывам зусім іншых фактараў:
пад дзеяннем закона Канвея;
у выніку паўторнага выкарыстання падсістэм, раней распрацаваных для іншых прадуктаў;
па рашэнні архітэктара, зыходзячы з нефункцыянальных патрабаванняў.
Існуе вялікая спакуса аддзяліць інтэграцыйную логіку ад бізнэс-логікі асноўнага workflow, каб не забруджваць бізнэс-логіку інтэграцыйнымі артэфактамі і пазбавіць прыкладнага распрацоўніка ад неабходнасці ўнікаць у асаблівасці архітэктурнага ландшафту сістэмы. У такога падыходу ёсць шэраг пераваг, аднак практыка паказвае яго неэфектыўнасць:
рашэнне інтэграцыйных задач звычайна скочваецца да самых простых варыянтаў у выглядзе сінхронных выклікаў з-за абмежаванасці кропак пашырэння ў рэалізацыі асноўнага workflow (аб недахопах сінхроннай інтэграцыі – крыху ніжэй);
інтэграцыйныя артэфакты ўсё роўна пранікаюць у асноўную бізнэс-логіку, калі патрабуецца зваротная сувязь з іншай падсістэмы;
прыкладны распрацоўшчык ігнаруе інтэграцыю і можа лёгка яе зламаць, змяніўшы workflow;
сістэма перастае быць адзіным цэлым з пункта гледжання карыстача, становяцца прыкметныя "швы" паміж падсістэмамі, з'яўляюцца залішнія карыстацкія аперацыі, якія ініцыююць перадачу дадзеных з адной падсістэмы ў іншую.
Іншы падыход - разгляд інтэграцыйных узаемадзеянняў як неад'емнай часткі асноўнай бізнес-логікі і workflow. Каб патрабаванні да кваліфікацыі прыкладных распрацоўшчыкаў не ўзляцелі да нябёсаў, стварэнне новых інтэграцыйных узаемадзеянняў павінна выконвацца лёгка і нязмушана, з мінімальнымі магчымасцямі для выбару спосабу рашэння. Гэта зрабіць складаней, чым здаецца: прылада павінна быць досыць магутным, каб забяспечыць карыстачу неабходнае мноства варыянтаў яго ўжывання і пры гэтым не дазволіць "стрэліць сабе ў нагу". Існуе мноства пытанняў, на якія павінен адказаць інжынер у кантэксце інтэграцыйных задач, але пра якія не павінен задумвацца прыкладны распрацоўшчык у сваёй паўсядзённай працы: межы транзакцый, кансістэнтнасць, атамарнасць, бяспека, маштабаванне, размеркаванне нагрузак і рэсурсаў, роўтынг, маршалінг, распаўсюджванне і пераключэнне кантэкстаў і т. п. Трэба прапанаваць прыкладным распрацоўнікам досыць простыя шаблоны рашэнняў, у якіх ужо схаваныя адказы на ўсе падобныя пытанні. Гэтыя шаблоны павінны быць дастаткова бяспечныя: бізнес-логіка мяняецца вельмі часта, што павышае рызыкі ўнясення памылак, цана памылак павінна заставацца на дастаткова нізкім узроўні.
Але ўсё ж такі пры чым тут BPM? Ёсць жа мноства варыянтаў рэалізацыі workflow…
Сапраўды, у нашых рашэннях вельмі папулярная іншая рэалізацыя бізнес-працэсаў - праз дэкларатыўнае заданне дыяграмы пераходаў станаў і падключэнне апрацоўшчыкаў з бізнес-логікай на пераходы. Пры гэтым стан, якое вызначае бягучае становішча "дакумента" ў бізнес-працэсе, з'яўляецца атрыбутам самога "дакумента".
Так выглядае працэс на старце праекту
Папулярнасць такой рэалізацыі абумоўлена адноснай прастатой і хуткасцю стварэння лінейных бізнес-працэсаў. Аднак па меры сталага ўскладнення праграмных сістэм аўтаматызаваная частка бізнэс-працэсу разрастаецца і ўскладняецца. З'яўляецца неабходнасць у дэкампазіцыі, паўторным выкарыстанні частак працэсаў, а таксама ў разгалінаванні працэсаў, каб кожная галінка выконвалася раўналежна. У такіх умовах прылада становіцца няёмкім, а дыяграма пераходаў станаў губляе інфарматыўнасць (інтэграцыйныя ўзаемадзеянні наогул ніяк не адлюстроўваюцца на дыяграме).
Так выглядае працэс праз некалькі ітэрацый удакладнення патрабаванняў.
Выйсцем з гэтай сітуацыі стала інтэграцыя рухавічка jBPM у некаторыя прадукты з найбольш складанымі бізнес-працэсамі. У кароткатэрміновай перспектыве гэтае рашэнне мела пэўны поспех: з'явілася магчымасць рэалізацыі складаных бізнес-працэсаў з захаваннем дастаткова інфарматыўнай і актуальнай дыяграмы ў натацыі. BPMN2.
Невялікая частка складанага бізнес-працэсу
У доўгатэрміновай перспектыве рашэнне не апраўдала чаканняў: высокая працаёмкасць стварэння бізнес-працэсаў праз візуальныя інструменты не дазволіла дасягнуць прымальных паказчыкаў прадуктыўнасці, а сам інструмент стаў адным з самых нялюбых сярод распрацоўшчыкаў. Да ўнутранай прылады рухавічка таксама былі прэтэнзіі, якія прывялі да з'яўлення мноства "латак" і "мыліц".
Галоўным дадатным момантам ужывання jBPM стала ўсведамленне карысці і шкоды ад наяўнасці ўласнага персістэнтнага стану ў асобніка бізнэс-працэсу. Таксама мы ўбачылі магчымасць ужывання працэснага падыходу для рэалізацыі складаных пратаколаў інтэграцыі паміж рознымі прыкладаннямі з ужываннем асінхронных узаемадзеянняў праз сігналы і паведамленні. Наяўнасць персістэнтнага стану гуляе ў гэтым найважную ролю.
На падставе сказанага можна зрабіць выснову: працэсны падыход у стылі BPM дазваляе нам вырашаць шырокі спектр задач па аўтаматызацыі пастаянна ўскладняюцца бізнес-працэсаў, гарманічна ўпісваць у гэтыя працэсы інтэграцыйныя актыўнасці і захоўваць магчымасць візуальнага адлюстравання рэалізаванага працэсу ў прыдатнай для гэтага натацыі.
Недахопы сінхронных выклікаў як інтэграцыйнага патэрна
Пад сінхроннай інтэграцыяй разумеецца найпросты блакуючы выклік. Адна падсістэма выступае серверным бокам і выстаўляе API з патрэбным метадам. Іншая падсістэма выступае кліенцкім бокам і ў патрэбны момант выконвае выклік з чаканнем выніку. У залежнасці ад архітэктуры сістэмы кліенцкая і серверная бакі могуць размяшчацца або ў адным дадатку і працэсе, або ў розных. У другім выпадку патрабуецца прымяніць некаторую рэалізацыю RPC і забяспечыць маршалінг параметраў і выніку выкліку.
Такі інтэграцыйны патэрн валодае дастаткова вялікім наборам недахопаў, але ён вельмі шырока выкарыстоўваецца на практыцы ў сілу сваёй прастаты. Хуткасць рэалізацыі падкупляе і прымушае прымяняць яго зноў і зноў ва ўмовах "палаючых" тэрмінаў, запісваючы рашэнне ў тэхнічны доўг. Але бывае і так, што неспрактыкаваныя распрацоўшчыкі ўжываюць яго неўсвядомлена, проста не здагадваючыся аб негатыўных наступствах.
Апроч найболей відавочнага падвышэння складнасці падсістэм, ёсць і меней відавочныя праблемы з «растарошчваннем» і «расцягам» транзакцый. Сапраўды, калі бізнес-логіка ўносіць нейкія змены, тады не абысціся без транзакцый, а транзакцыі, у сваю чаргу, блакуюць пэўныя рэсурсы дадатку, якія закранаюцца гэтымі зменамі. Гэта значыць, пакуль адна падсістэма не дачакаецца адказу ад іншай, яна не зможа завяршыць транзакцыю і зняць блакіроўкі. Гэта істотна павялічвае рызыку ўзнікнення разнастайных эфектаў:
губляецца спагадлівасць сістэмы, карыстачы падоўгу чакаюць адказаў на запыты;
сервер наогул перастае адказваць на запыты карыстальнікаў з-за перапоўненага пула патокаў: большасць патокаў "усталі" на блакіроўцы рэсурсу, занятага транзакцыяй;
пачынаюць з'яўляцца дэдлакі: верагоднасць іх з'яўлення моцна залежыць ад працягласці транзакцый, колькасці ўцягнутай у транзакцыю бізнес-логікі і блакіровак;
з'яўляюцца памылкі заканчэння таймаўту транзакцыі;
сервер "падае" па OutOfMemory, калі задача патрабуе апрацоўкі і змены вялікіх аб'ёмаў дадзеных, а наяўнасць сінхронных інтэграцый моцна абцяжарвае драбненне апрацоўкі на лягчэйшыя" транзакцыі.
З архітэктурнага пункта гледжання ўжыванне блакавальных выклікаў пры інтэграцыі прыводзіць да страты кіравання якасцю асобных падсістэм: немагчыма забяспечыць мэтавыя паказчыкі якасці адной падсістэмы ў адрыве ад паказчыкаў якасці іншай падсістэмы. Калі падсістэмы распрацоўваюцца рознымі камандамі, гэта вялікая праблема.
Усё становіцца яшчэ цікавей, калі інтэграваныя падсістэмы знаходзяцца ў розных дадатках і трэба ўнесці сінхронныя змены з двух бакоў. Як забяспечыць транзакцыйнасць гэтых змен?
Калі змены ўносяцца паасобнымі транзакцыямі, тады запатрабуецца забяспечыць надзейную апрацоўку выключэнняў і кампенсаванні, а гэта цалкам нівелюе асноўную перавагу сінхронных інтэграцый - прастату.
На розум таксама прыходзяць размеркаваныя транзакцыі, але мы іх не выкарыстоўваем у сваіх рашэннях: складана забяспечыць надзейнасць.
"Сага" як вырашэнне праблемы транзакцый
З ростам папулярнасці мікрасэрвісаў усё большую запатрабаванасць набывае Saga Pattern.
Дадзены шаблон выдатна вырашае пазначаныя вышэй праблемы доўгіх транзакцый, а таксама пашырае магчымасці кіравання станам сістэмы са боку бізнэс-логікі: кампенсаванне пасля няўдалай транзакцыі можа не адкочваць сістэму ў зыходны стан, а забяспечваць альтэрнатыўны маршрут апрацоўкі дадзеных. Гэта таксама дазваляе не паўтараць паспяхова завершаныя крокі апрацоўкі дадзеных пры паўторных спробах давесці працэс да "добрага" фіналу.
Што цікава, у маналітных сістэмах гэты шаблон таксама актуальны, калі гаворка ідзе аб інтэграцыі слаба звязаных падсістэм і назіраюцца негатыўныя эфекты, выкліканыя працяглымі транзакцыямі і адпаведнымі блакіроўкамі рэсурсаў.
У дачыненні да нашых бізнес-працэсаў у стылі BPM імплементаваць "Сагі" аказваецца вельмі лёгка: асобныя крокі "Сагі" могуць быць зададзены ў выглядзе актыўнасцяў ўнутры бізнес-працэсу, а персістэнтнае стан бізнес-працэсу вызначае ў тым ліку ўнутраны стан "Сагі". Гэта значыць, нам не патрабуецца ніякага дадатковага каардынацыйнага механізму. Спатрэбіцца толькі брокер паведамленняў з падтрымкай "at least once" гарантый у якасці транспарту.
Але і ў такога рашэння ёсць свая «кошт»:
бізнес-логіка становіцца больш складанай: трэба адпрацоўваць кампенсацыі;
запатрабуецца адмовіцца ад full consistency, што можа быць асоба адчувальным для маналітных сістэм;
трохі ўскладняецца архітэктура, з'яўляецца дадатковае запатрабаванне ў брокеры паведамленняў;
спатрэбяцца дадатковыя сродкі маніторынгу і адміністравання (хоць у цэлым гэта нават добра: якасць абслугоўвання сістэмы павысіцца).
Для маналітных сістэм апраўданасць выкарыстання "Саг" не так відавочная. Для мікрасэрвісаў і іншых SOA, дзе, хутчэй за ўсё, ужо ёсць брокер, а full consistency прынесена ў ахвяру яшчэ на старце праекту, карысць ад выкарыстання гэтага шаблону можа значна пераважыць недахопы, асабліва пры наяўнасці зручнай API на ўзроўні бізнэс-логікі.
Інкапсуляцыя бізнес-логікі ў мікрасэрвісах
Калі мы пачалі эксперыментаваць з мікрасэрвісамі, узнікла слушнае пытанне: куды змяшчаць даменную бізнес-логіку адносна сэрвісу, які забяспечвае персістэнцыю даменных дадзеных?
Пры поглядзе на архітэктуру розных BPMS можа здацца разумным аддзяліць бізнес-логіку ад персістэнцыі: стварыць пласт платформенных і даменна-незалежных мікрасэрвісаў, якія фарміруюць асяроддзе і кантэйнер для выканання даменнай бізнес-логікі, а персістэнцыю даменных дадзеных аформіць асобным пластом з вельмі простых і легкаважных мікрасэрвісаў. Бізнес-працэсы ў такім разе выконваюць аркестроўку сэрвісаў пласта персістэнцыі.
У такога падыходу ёсць вельмі вялікі плюс: можна колькі заўгодна нарошчваць функцыянальнасць платформы, і "таўсцець" ад гэтага будзе толькі які адпавядае пласт платформенных мікрасэрвісаў. Бізнэс-працэсы з любога дамена адразу атрымліваюць магчымасць выкарыстоўваць новую функцыянальнасць платформы, як толькі яна будзе абноўлена.
Больш дэталёвая прапрацоўка выявіла істотныя недахопы такога падыходу:
платформавы сэрвіс, які выконвае бізнэс-логіку адразу шматлікіх даменаў, нясе ў сабе вялікія рызыкі як адзіная кропка адмовы. Частыя змены бізнес-логікі павышаюць рызыку ўзнікнення памылак, якія прыводзяць да збояў, якія распаўсюджваюцца на ўсю сістэму;
праблемы прадукцыйнасці: бізнес-логіка працуе са сваімі дадзенымі праз вузкі і павольны інтэрфейс:
дадзеныя будуць лішні раз маршаліцца і прапампоўвацца праз сеткавы стэк;
даменны сэрвіс часцяком будзе аддаваць больш дадзеных, чым патрабуецца бізнэс-логіцы для апрацоўкі, з-за недастатковых магчымасцяў параметрызацыі запытаў на ўзроўні вонкавай API сэрвісу;
некалькі незалежных частак бізнес-логікі могуць паўторна перазапытваць адны і тыя ж дадзеныя для апрацоўкі (можна змякчыць гэтую праблему даданнем сесійных кампанентаў, якія кэшуюць дадзеныя, але гэта дадаткова ўскладняе архітэктуру і стварае праблемы актуальнасці дадзеных і інвалідацыі кэша);
праблемы транзакцыйнасці:
бізнес-працэсы з персістэнтным станам, захоўваннем якога займаецца платформавы сэрвіс, разузгадняюцца з даменнымі дадзенымі, і простых шляхоў рашэння гэтай праблемы не прадбачыцца;
вынясенне блакіроўкі даменных дадзеных за межы транзакцыі: калі даменнай бізнес-логіцы патрабуецца ўнесці змены, папярэдне выканаўшы праверку карэктнасці актуальных дадзеных, патрабуецца выключыць магчымасць канкурэнтнай змены апрацоўваных дадзеных. Вонкавае блакаванне дадзеных можа дапамагчы вырашыць задачу, але такое рашэнне нясе ў сабе дадатковыя рызыкі і змяншае агульную надзейнасць сістэмы;
дадатковыя складанасці пры абнаўленні: у шэрагу выпадкаў абнаўляць сэрвіс персістэнцыі і бізнэс-логіку трэба сінхронна ці ў строгай паслядоўнасці.
У канчатковым выніку прыйшлося вярнуцца да вытокаў: інкапсуляваць даменныя дадзеныя і даменную бізнэс-логіку ў адзін мікрасэрвіс. Такі падыход спрашчае ўспрыманне мікрасэрвісу як цэласнага кампанента ў складзе сістэмы і не спараджае вышэйпералічаныя праблемы. Гэта таксама даецца не бясплатна:
патрабуецца стандартызацыя API для ўзаемадзеяння з бізнес-логікай (у прыватнасці, для забеспячэння карыстацкіх актыўнасцей у складзе бізнес-працэсаў) і API-платформавых сэрвісаў; патрабуецца больш уважлівае стаўленне да змены API, прамой і зваротнай сумяшчальнасці;
патрабуецца даданне дадатковых runtime-бібліятэк для забеспячэння функцыянавання бізнес-логікі ў складзе кожнага такога мікрасэрвісу, і гэта спараджае новыя патрабаванні да такіх бібліятэк: легкаважнасць і мінімум транзітыўных залежнасцяў;
распрацоўшчыкам бізнес-логікі неабходна сачыць за версіямі бібліятэк: калі нейкі мікрасэрвіс даўно не дапрацоўвалі, то ў ім, хутчэй за ўсё, апынецца састарэлая версія бібліятэк. Гэта можа стаць нечаканай перашкодай для дадання новай фічы і можа запатрабаваць міграцыі старой бізнэс-логікі такога сэрвісу на новыя версіі бібліятэк, калі паміж версіямі былі несумяшчальныя змены.
Пласт платформенных сэрвісаў у такой архітэктуры таксама прысутнічае, але гэты пласт фармуе ўжо не кантэйнер для выканання даменнай бізнес-логікі, а ўсяго толькі яе асяроддзе, падаючы дапаможныя "платформенныя" функцыі. Такі пласт патрэбен не толькі для захавання легкаважнасці даменных мікрасэрвісаў, але і для цэнтралізацыі кіравання.
Напрыклад, карыстацкія актыўнасці ў бізнес-працэсах спараджаюць задачы. Аднак, працуючы з задачамі, карыстач павінен бачыць задачы з усіх даменаў у агульным спісе, а значыць, павінен быць які адпавядае платформавы сэрвіс рэгістрацыі задач, вычышчаны ад даменнай бізнэс-логікі. Захаваць інкапсуляцыю бізнес-логікі ў такім кантэксце дастаткова праблематычна, і гэта яшчэ адзін кампраміс дадзенай архітэктуры.
Як ужо сказана вышэй, прыкладны распрацоўшчык павінен быць абстрагаваны ад тэхнічных і інжынерных асаблівасцяў рэалізацыі ўзаемадзеяння некалькіх прыкладанняў, каб можна было разлічваць на добрую прадуктыўнасць распрацоўкі.
Паспрабуем вырашыць дастаткова няпростую інтэграцыйную задачу, спецыяльна прыдуманую для артыкула. Гэта будзе "гульнявая" задача з удзелам трох прыкладанняў, дзе кожнае з іх вызначае некаторае даменнае імя: "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 натацыю ў частцы выкарыстання гейтаў, каб палепшыць узгодненасць дыяграмы з прыведзеным кодам):
Прыкладанне 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}")
}
Дыяграма:
У дадатку 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;
}
З усяго гэтага можна зрабіць некалькі важных высноў:
пры наяўнасці неабходных інструментаў прыкладныя распрацоўшчыкі могуць ствараць інтэграцыйныя ўзаемадзеянні паміж праграмамі без адрыву ад бізнес-логікі;
складанасць (complexity) інтэграцыйнай задачы, якая патрабуе інжынерных кампетэнцый, можна схаваць усярэдзіне фреймворка, калі першапачаткова закласці гэта ў архітэктуру фреймворка. Цяжкасць задачы (difficulty) схаваць не атрымаецца, таму рашэнне цяжкай задачы ў кодзе будзе выглядаць адпаведна;
пры распрацоўцы інтэграцыйнай логікі абавязкова трэба ўлічваць eventually consistency і адсутнасць лінеарызуемага змены стану ўсіх удзельнікаў інтэграцыі. Гэта змушае ўскладняць логіку, каб зрабіць яе неадчувальнай да парадку ўзнікнення вонкавых падзей. У нашым прыкладзе гулец змушаны прымаць удзел у гульні ўжо пасля таго, як ён заявіць аб выхадзе з гульні: іншыя гульцы будуць працягваць перадаваць яму мяч, пакуль інфармацыя аб яго выхадзе не дойдзе і не апрацуецца ўсімі ўдзельнікамі. Гэтая логіка не выцякае з правіл гульні і з'яўляецца кампрамісным рашэннем у рамках абранай архітэктуры.
Далей пагаворым аб розных тонкасцях нашага рашэння, кампрамісах і іншых момантах.
Усе паведамленні - у адной чарзе
Усе інтэгравальныя прыкладанні працуюць з адной інтэграцыйнай шынай, якая прадстаўлена ў выглядзе вонкавага брокера, адной чаргі BPMQueue - для паведамленняў і аднаго топіка BPMTopic - для сігналаў (падзей). Прапускаць усе паведамленні праз адну чаргу само па сабе з'яўляецца кампрамісам. На ўзроўні бізнес-логікі зараз можна ўводзіць колькі заўгодна новых тыпаў паведамленняў, не ўносячы змен у структуру сістэмы. Гэта значнае спрашчэнне, але яно нясе ў сабе пэўныя рызыкі, якія ў кантэксце нашых тыпавых задач падаліся нам не такімі ўжо значнымі.
Аднак тут ёсць адна тонкасць: кожнае прыкладанне адфільтроўвае "свае" паведамленні з чаргі яшчэ на ўваходзе, па імі свайго дамена. Таксама дамен можа быць паказаны і ў сігналах, калі трэба абмежаваць "вобласць бачнасці" сігналу адным адзіным дадаткам. Гэта павінна павялічыць прапускную здольнасць шыны, але бізнэс-логіка зараз павінна апераваць імёнамі даменаў: для адрасавання паведамленняў - абавязкова, для сігналаў - пажадана.
Забеспячэнне надзейнасці інтэграцыйнай шыны
Надзейнасць складаецца з некалькіх момантаў:
абраны брокер паведамленняў - крытычна важны кампанент архітэктуры і адзіная кропка адмовы: ён павінен быць дастаткова адмоваўстойлівасцю. Варта выкарыстоўваць толькі правераныя часам рэалізацыі, з добрай падтрымкай і вялікім кам'юніці;
неабходна забяспечыць высокую даступнасць брокера паведамленняў, для чаго ён павінен быць фізічна аддзелены ад інтэграваных прыкладанняў (высокую даступнасць прыкладанняў з прыкладной бізнес-логікай забяспечыць значна складаней і даражэй);
брокер абавязаны забяспечыць "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.
У залежнасці ад глыбіні змен можна дзейнічаць двума шляхамі:
стварыць новы тып бізнес-працэсу, каб не ўносіць несумяшчальных змен у стары, і выкарыстоўваць яго замест старога пры запуску новых асобнікаў. Старыя экзэмпляры будуць працягваць працаваць "па-старому";
міграваць персістэнтнае стан бізнес-працэсаў пры абнаўленні бізнес-логікі.
Першы шлях прасцейшы, але мае свае абмежаванні і недахопы, напрыклад:
дубліраванне бізнес-логікі ў многіх мадэлях бізнес-працэсаў, павелічэнне аб'ёму бізнес-логікі;
часта патрабуецца маментальны пераход на новую бізнес-логіку (у частцы інтэграцыйных задач - амаль заўсёды);
распрацоўшчык не ведае, у які момант можна выдаляць састарэлыя мадэлі.
На практыцы мы выкарыстоўваем абодва падыходы, але прынялі шэраг рашэнняў, каб спрасціць сабе жыццё:
у базе дадзеных персістэнтны стан бізнэс-працэсу захоўваецца ў лёгка чытэльным і лёгка апрацоўваным выглядзе: у радку фармату JSON. Гэта дазваляе выконваць міграцыі як усярэдзіне прыкладання, так і звонку. У крайнім выпадку можна і ручкамі падправіць (асабліва карысна ў распрацоўцы падчас адладкі);
інтэграцыйная бізнэс-логіка не выкарыстоўвае імёны бізнэс-працэсаў, каб у любы момант можна было замяніць рэалізацыю аднаго з якія ўдзельнічаюць працэсаў на новую, з новым імем (напрыклад, «InitialPlayerV2»). Звязванне адбываецца праз імёны паведамленняў і сігналаў;
мадэль працэсу мае нумар версіі, які мы павялічваем, калі ўносім у гэтую мадэль несумяшчальныя змены, і гэты нумар захоўваецца разам са станам асобніка працэсу;
персістэнтны стан працэсу вычытваецца з базы спачатку ў зручную аб'ектную мадэль, з якой можа працаваць працэдура міграцыі, калі змяніўся нумар версіі мадэлі;
працэдура міграцыі размяшчаецца побач з бізнес-логікай і выклікаецца "гультаявата" для кожнага экзэмпляра бізнес-працэсу ў момант яго аднаўлення з базы;
калі трэба міграваць стан усіх асобнікаў працэсу аператыўна і сінхронна, ужываюцца больш класічныя рашэнні па міграцыі БД, але тамака прыходзіцца працаваць з JSON.
Ці патрэбен яшчэ адзін фрэймворк для бізнэс-працэсаў?
Апісаныя ў артыкуле рашэнні дазволілі нам прыкметна спрасціць сабе жыццё, пашырыць круг пытанняў, развязальных на ўзроўні прыкладнай распрацоўкі, зрабіць больш прывабнымі ідэі вылучэння бізнэс-логікі ў мікрасэрвісы. Для гэтага было праведзена шмат працы, створаны вельмі «лёгаважны» фрэймворк для бізнэс-працэсаў, а таксама службовыя кампаненты для рашэння пазначаных праблем у кантэксце шырокага круга прыкладных задач. У нас ёсць жаданне падзяліцца гэтымі вынікамі, вынесці распрацоўку агульных кампанентаў у адкрыты доступ пад свабоднай ліцэнзіяй. Гэта запатрабуе пэўных намаганняў і часу. Разуменне запатрабаванасці такіх рашэнняў магло б стаць для нас дадатковым стымулам. У прапанаваным артыкуле вельмі мала ўвагі нададзена магчымасцям самога фрэймворка, але некаторыя з іх бачныя з прадстаўленых прыкладаў. Калі мы ўсё ж такі апублікуем свой фрэймворк, яму будзе прысвечаны асобны артыкул. А пакуль будзем удзячныя, калі пакінеце невялікі фідбэк, адказаўшы на пытанне:
Толькі зарэгістраваныя карыстачы могуць удзельнічаць у апытанні. Увайдзіце, Калі ласка.
ці патрэбен яшчэ адзін фрэймворк для бізнэс-працэсаў?
18,8%так, даўно шукаем нешта падобнае3
12,5%цікава даведацца пра вашу рэалізацыю пабольш, можа спатрэбіцца2
6,2%выкарыстоўваем адзін з існуючых фрэймворкаў, але падумваем аб замене1
18,8%выкарыстоўваем адзін з існуючых фрэймворкаў, усё задавальняе3