
Ahoj, Habr!
Naša spoločnosť sa špecializuje na vývoj softvérových riešení triedy ERP, pričom významnú časť tvoria transakčné systémy s obrovským objemom business logiky a dokumentového obeh. Súčasné verzie našich produktov sú založené na technológiách JavaEE, ale aktívne experimentujeme aj s mikroservismi. Jedným z najproblémovejších oblastí takýchto riešení je integrácia rôznych subsystémov týkajúcich sa príbuzných domén. Úlohy integrácie nám vždy spôsobovali obrovské problémy, bez ohľadu na používané architektonické štýly, technologické stacky a frameworky, avšak v poslednom čase sa v riešení týchto úloh objavili pokroky.
V článku, ktorý vám predkladáme, sa podelím o skúsenosti a architektonické úvahy NPO „Krista“ v danej oblasti. Taktiež zvážime príklad jednoduchého riešenia integračnej úlohy z pohľadu aplikačného vývojára a zistíme, čo sa skrýva za touto jednoduchosťou.
Zrieknutie sa zodpovednosti
Opísané v článku architektonické a technické riešenia sú založené na osobnej skúsenosti v kontexte konkrétnych úloh. Tieto riešenia si nenárokujú na univerzálnosť a môžu sa ukázať ako neoptimálne za iných podmienok.
Aký má súvis BPM?
Aby sme odpovedali na túto otázku, musíme sa trochu ponoriť do špecifík aplikačných úloh našich riešení. Hlavná časť business logiky v našom typickom transakčnom systéme spočíva vo vkladaní údajov do DB cez užívateľské rozhrania, manuálnu a automatizovanú kontrolu týchto údajov, ich prevod cez určitý pracovný tok, publikáciu do iného systému / analytickej databázy / archívu, generovanie správ. Kľúčovou funkciou systému pre zákazníkov je teda automatizácia ich vnútorných podnikových procesov.
Pre zjednodušenie používame v komunikácii termín „dokument“ ako určitú abstrakciu súboru údajov spojených spoločným kľúčom, ku ktorému môžeme „pripojiť“ určitý pracovný tok.
Ale ako sa vysporiadať s integračnou logikou? Veď úloha integrácie vzniká architektúrou systému, ktorá je „rozdelená“ na časti NIE na požiadanie zákazníka, ale pod vplyvom úplne iných faktorov:
- pod vplyvom Conwayovho zákona;
- v dôsledku opätovného využívania subsystémov, ktoré boli predtým vyvinuté pre iné produkty;
- na základe rozhodnutia architekta, vychádzajúc z nefunkčných požiadaviek.
Existuje veľké pokušenie oddeliť integračnú logiku od obchodnej logiky hlavného workflow, aby sa neznečistila obchodná logika integračnými artefaktmi a aby sa aplikovanému vývojárovi uľahčila orientácia v špecifikách architektonickej krajiny systému. Tento prístup má niekoľko výhod, avšak prax ukazuje jeho neefektivitu:
- riešenie integračných úloh sa zvyčajne zjednodušuje na najjednoduchšie možnosti v podobe synchronných volaní kvôli obmedzenému počtu bodov rozšírenia v realizácii hlavného workflow (o nevýhodách synchronnej integrácie – nižšie);
- integračné artefakty sa predsa len dostávajú do hlavnej obchodnej logiky, keď je potrebná spätná väzba z iného subsystému;
- aplikovaný vývojár ignoruje integráciu a môže ju jednoducho rozbiť, ak zmení workflow;
- systém prestáva byť celistvým z pohľadu používateľa, sú viditeľné "švy" medzi subsystémami, objavujú sa nadbytočné používateľské operácie, ktoré iniciujú prenos údajov z jedného subsystému do druhého.
Iný prístup spočíva v posudzovaní integračných interakcií ako neoddeliteľnej súčasti základnej obchodnej logiky a workflow. Aby požiadavky na kvalifikáciu aplikovaných vývojárov nevystúpili do nebies, vytváranie nových integračných interakcií by sa malo vykonávať jednoducho a bezproblémovo, s minimálnymi možnosťami výberu spôsobu riešenia. Urobiť to je ťažšie, než sa zdá: nástroj musí byť dostatočne výkonný, aby používateľovi poskytol potrebný rozsah jeho aplikácie a zároveň mu neumožniť „vystreliť si do nohy“. Existuje mnoho otázok, ktorým musí inžinier v kontexte integračných úloh odpovedať, ale na ktoré by sa aplikovaný vývojár nemal vo svojej každodennej práci zamýšľať: hranice transakcií, konzistencia, atomickosť, bezpečnosť, škálovanie, rozdeľovanie záťaží a zdrojov, routovanie, marshaling, šírenie a prepínanie kontextov atď. Je potrebné ponúknuť aplikovaným vývojárom dostatočne jednoduché vzory riešení, v ktorých sú už skryté odpovede na všetky podobné otázky. Tieto vzory by mali byť dostatočne bezpečné: obchodná logika sa mení veľmi často, čo zvyšuje riziko vzniku chýb, cena chýb by mala zostať na dostatočne nízkej úrovni.
Ale čo má BPM s týmto spoločné? Existuje množstvo variantov implementácie workflow…
Naozaj, v našich riešeniach je veľmi obľúbená iná realizácia obchodných procesov – prostredníctvom deklaratívneho nastavenia diagramu prechodov stavov a pripojenia spracovateľov s obchodnou logikou na prechody. Pri tom je stav, ktorý určuje aktuálnu polohu „dokumentu“ v obchodnom procese, atribútom samotného „dokumentu“.

Takto vyzerá proces na začiatku projektu.
Popularita takéhoto prístupu je spôsobená relatívnou jednoduchosťou a rýchlosťou vytvárania lineárnych podnikových procesov. Avšak s neustálym zložitým vývojom softvérových systémov sa automatizovaná časť podnikového procesu rozrastá a komplikuje. Objavuje sa potreba dekompozície, opätovného využívania častí procesov a taktiež rozvetvovania procesov, aby každá vetva prebiehala súbežne. V takýchto podmienkach sa nástroj stáva nepohodlným a schéma stavových prechodov stráca informačnosť (integračné interakcie sa na schéme vôbec neodzrkadľujú).

Takto vyzerá proces po niekoľkých iteráciách upresňovania požiadaviek.
Riešením tejto situácie bola integrácia enginu do niektorých produktov s najkomplexnejšími podnikových procesmi. V krátkodobom horizonte malo toto riešenie určitý úspech: vznikla možnosť realizácie komplexných podnikových procesov so zachovaním dostatočne informačnej a aktuálnej schémy v notácii .

Malá časť zložitých podnikových procesov.
Dlhodobo však riešenie nesplnilo očakávania: vysoká pracnosť vytvárania podnikových procesov prostredníctvom vizuálnych nástrojov neumožnila dosiahnuť prijateľné ukazovatele produktivity, a samotný nástroj sa stal jedným z najneobľúbenejších medzi vývojármi. K vnútornému usporiadaniu enginu tiež existovali výhrady, ktoré viedli k vzniku mnohých "záplat" a "barličiek".
Hlavným pozitívnym aspektom využitia jBPM bolo uvedomenie si užitočnosti a škodlivosti existencie vlastného perzistentného stavu u inštancie podnikového procesu. Taktiež sme videli možnosť aplikácie procesného prístupu na realizáciu zložitých integračných protokolov medzi rôznymi aplikáciami s využitím asynchrónnych interakcií prostredníctvom signálov a správ. Existencia perzistentného stavu má v tom zásadnú úlohu.
Na základe vyššie uvedeného môžeme formulovať záver: procesný prístup v štýle BPM nám umožňuje riešiť široké spektrum úloh automatizácie neustále sa komplikujúcich podnikových procesov, harmonicky zakomponovať do týchto procesov integračné aktivity a zachovať možnosť vizuálneho zobrazenia realizovaného procesu v vhodnej pre tento účel notácii.
Nevýhody synchronných volaní ako integračného vzoru
Pod synchronnou integráciou rozumieme najjednoduchšie blokujúce volanie. Jedna subsystém vystupuje ako serverová strana a poskytuje API s potrebnou metódou. Druhá subsystém vystupuje ako klientská strana a v potrebnom momente vykonáva volanie s očakávaním výsledku. V závislosti od architektúry systému sa klientská a serverová strana môžu nachádzať buď v jednej aplikácii a procese, alebo v rôznych. V druhom prípade je potrebné aplikovať určitú realizáciu RPC a zabezpečiť marshalling parametrov a výsledku volania.

Takýto integračný vzor má dosť veľký súbor nedostatkov, ale je veľmi široko používaný v praxi kvôli svojej jednoduchosti. Rýchlosť implementácie láka a núti ho aplikovať znovu a znovu v podmienkach "horúcich" termínov, zapisujúc riešenie do technického dlhu. Ale stáva sa aj to, že menej skúsení vývojári ho používajú nevedomky, len si neuvedomujúc negatívne dôsledky.
Okrem najzjavnejšieho zvýšenia prepojenosti subsystémov existujú aj menej zrejmé problémy s "rozťahovaním" a "predlžovaním" transakcií. Skutočne, ak podniková logika vykonáva nejaké zmeny, potom nie je možné sa obísť bez transakcií, a transakcie, na druhej strane, blokujú určité zdroje aplikácie, ktorých sa tieto zmeny týkajú. Teda, kým jedna subsystém nečaká na odpoveď od druhej, nebude schopná dokončiť transakciu a uvoľniť blokácie. To výrazne zvyšuje riziko vzniku rôznych efektov:
- stráca sa reagovanosť systému, používatelia dlho čakajú na odpovede na požiadavky;
- server prestáva reagovať na požiadavky používateľov kvôli naplnenému poolu vlákien: väčšina vlákien sa "zastavila" na blokáde zdroja, ktorý je obsadený transakciou;
- začínajú sa objavovať deadlocky: pravdepodobnosť ich vzniku silno závisí od trvania transakcií, množstva zapojenej podnikovej logiky a blokácií;
- objavujú sa chyby vypršania časového limitu transakcie;
- server "padá" z dôvodu OutOfMemory, ak úloha vyžaduje spracovanie a zmeny veľkých objemov dát, pričom prítomnosť synchronných integrácií veľmi sťažuje rozdelenie spracovania na "ľahšie" transakcie.
Z architektonického hľadiska vedie použitie blokujúcich volaní pri integrácii k strate kontroly nad kvalitou jednotlivých subsystémov: nemožno zabezpečiť cielené ukazovatele kvality jedného subsystému bez ohľadu na ukazovatele kvality iného subsystému. Ak sú subsystémy vyvíjané rôznymi tímami, ide o veľký problém.
Všetko sa stáva ešte zaujímavejším, ak sú integrované subsystémy v rôznych aplikáciách a je potrebné vykonať synchronné zmeny z oboch strán. Ako zabezpečiť transakčnosť týchto zmien?
Ak sú zmeny vykonávané samostatnými transakciami, bude potrebné zabezpečiť spoľahlivé spracovanie výnimiek a kompenzácie, čo úplne znižuje hlavné výhody synchronných integrácií – jednoduchosti.
Na um tiež prichádzajú distribuované transakcie, ale ich v našich riešeniach nepoužívame: ťažko sa zabezpečuje spoľahlivosť.
„Saga“ ako riešenie problému transakcií
S rastúcou popularitou mikroslužieb sa čoraz viac uplatňuje .
Tento vzor výborne rieši vyššie uvedené problémy dlhých transakcií a tiež rozširuje možnosti správy stavu systému z pohľadu biznis logiky: kompenzácia po neúspešnej transakcii nemusí vracať systém do pôvodného stavu, ale môže zabezpečiť alternatívnu trasu spracovania dát. To tiež umožňuje nevykonávať úspešne dokončené kroky spracovania dát pri opakovaných pokusoch doviesť proces k „dobrému“ finále.
Zaujímavé je, že v monolitických systémoch je tento vzor tiež aktuálny, ak ide o integráciu slabozlúčených subsystémov a pozorujú sa negatívne efekty, spôsobené dlhými transakciami a zodpovedajúcimi blokáciami zdrojov.
Pre naše biznis procesy v štýle BPM je implementácia „Ság“ veľmi jednoduchá: jednotlivé kroky „Ságy“ môžu byť definované ako aktivity v rámci biznis procesu, a persistované stavy biznis procesu určujú aj vnútorný stav „Ságy“. To znamená, že nám nie je potrebný žiaden ďalší koordinačný mechanizmus. Potrebujeme len broker správ s podporou záruk „aspoň raz“ ako dopravy.
Ale aj toto riešenie má svoju „cenu“:
- biznisová logika sa stáva zložitejšou: je potrebné spracovať kompenzácie;
- bude potrebné vzdať sa úplnej konzistencie, čo môže byť obzvlášť citlivé pre monolitické systémy;
- architektúra sa trochu komplikuje, objavuje sa dodatočná potreba správcovskej správy správ;
- budú potrebné ďalšie prostriedky na monitorovanie a administráciu (aj keď celkovo je to dokonca dobré: kvalita poskytovania systému sa zvýši).
Pre monolitické systémy nie je opodstatnenosť používania „Ság“ taká zrejmá. Pre mikroslužby a ďalšie SOA, kde už pravdepodobne existuje broker, a úplna konzistencia bola obetovaná už na začiatku projektu, prínos z použitia tejto šablóny môže výrazne prevážiť nevýhody, najmä pri existencii pohodlného API na úrovni biznisovej logiky.
Inkapsulácia biznisovej logiky v mikroslužbách
Keď sme začali experimentovať s mikroslužbami, vznikla rozumná otázka: kam umiestniť doménovú biznisovú logiku v súvislosti so službou, ktorá zabezpečuje perzistenciu doménových dát?
Pri pohľade na architektúru rôznych BPMS sa môže zdať rozumné oddeliť biznisovú logiku od perzistencie: vytvoriť vrstvu platformových a doménovo nezávislých mikroslužieb, ktoré tvoria prostredie a kontajner na vykonávanie doménovej biznisovej logiky, pričom perzistenciu doménových dát sa vytvorí ako samostatná vrstva z veľmi jednoduchých a ľahkých mikroslužieb. Biznisové procesy v takom prípade vykonávajú orchestráciu služieb vrstvy perzistencie.

Tento prístup má jeden veľký plus: funkčnosť platformy je možné rozširovať bez obmedzenia a „zhustnúť“ z toho bude iba príslušná vrstva platformových mikroslužieb. Biznisové procesy z ktoréhokoľvek doména okamžite získavajú možnosť používať novú funkčnosť platformy, hneď ako bude aktualizovaná.
Podrobnejšie spracovanie odhalilo podstatné nedostatky tohto prístupu:
- platformová služba, ktorá vykonáva biznisovú logiku viacerých domén, nesie veľké riziká ako jednotné miesto zlyhania. Časté zmeny biznisovej logiky zvyšujú riziko vzniku chýb, ktoré vedú k zlyhaniam rozširujúcim sa na celý systém;
- problémy s výkonom: biznisová logika pracuje so svojimi údajmi cez úzky a pomalý rozhranie:
- dáta sa budú zbytočne spracovávať a zisťovať cez sieťový stack;
- doménová služba často poskytuje viac údajov, než je potrebné pre spracovanie biznis logiky, kvôli nedostatočným možnostiam parametralizácie dopytov na úrovni externého API služby;
- niekoľko nezávislých častí biznis logiky môže opakovane žiadať tie isté dáta na spracovanie (tento problém je možné zmierniť pridaním sedených komponentov na cacheovanie údajov, avšak to ďalej komplikuje architektúru a spôsobuje problémy s aktuálnosťou údajov a invalidovaním cache);
- problémy s tranzakčnosťou:
- biznis procesy s trvalým stavom, ktorého uchovanie zabezpečuje platformová služba, sa nezhodujú s doménovými dátami a nenachádza sa žiadne jednoduché riešenie tohto problému;
- vytlačenie zamykania doménových dát mimo tranzakcie: ak doménová biznis logika potrebuje vykonať zmeny po predchádzajúcej kontrole aktuálnych údajov, je potrebné vylúčiť možnosť konkurentného zmenenia spracovávaných údajov. Externé zamykanie údajov môže pomôcť vyriešiť úlohu, ale toto riešenie so sebou nesie dodatočné riziká a znižuje celkovú spoľahlivosť systému;
- dodatočné komplikácie pri aktualizácii: v niektorých prípadoch je potrebné synchronizovane alebo v striktnej sekvencii aktualizovať službu persistencie a biznis logiku.
Nakoniec sme sa museli vrátiť k základom: enkapsulovať doménové dáta a doménovú biznis logiku do jedného mikroslužby. Tento prístup zjednodušuje vnímanie mikroslužby ako celistvého komponentu v rámci systému a nevytvára vyššie uvedené problémy. To však tiež nie je zadarmo:
- je potrebná štandardizácia API pre interakciu s biznis logikou (najmä na zabezpečenie používateľských aktivít v rámci biznis procesov) a API platformových služieb; je potrebné venovať pozornosť zmene API, priamej a spätnej kompatibilite;
- je potrebné pridať ďalšie runtime knižnice na zabezpečenie fungovania biznis logiky v rámci každej takej mikroslužby, a to vytvára nové požiadavky na takéto knižnice: ľahkosť a minimum transitívnych závislostí;
- Vývojári obchodnej logiky musia sledovať verzie knižníc: ak nejaká mikroservis nebola dlhšie aktualizovaná, pravdepodobne obsahuje zastaranú verziu knižníc. To môže predstavovať nečakanú prekážku pri pridávaní novej funkcionality a môže si vyžadovať migráciu starej obchodnej logiky tejto služby na nové verzie knižníc, ak medzi verziami došlo k nekompatibilným zmenám.

Vrstva platformových služieb v takejto architektúre tiež existuje, ale táto vrstva už netvorí kontajner na vykonávanie doménovej obchodnej logiky, ale len jej okolie, poskytujúc podporné "platformové" funkcie. Takáto vrstva je potrebná nielen na zachovanie nízkej hmotnosti doménových mikroservisov, ale aj na centralizáciu správy.
Napríklad, používateľské aktivity v obchodných procesoch generujú úlohy. Avšak, pri práci s úlohami musí používateľ vidieť úlohy zo všetkých domén v spoločnom zozname, a preto by mal existovať zodpovedajúci platformový servis pre registráciu úloh, zbavený doménovej obchodnej logiky. Zachovanie enkapsulácie obchodnej logiky v takomto kontexte je dosť problematické a je to ďalší kompromis tejto architektúry.
Integrácia obchodných procesov očami aplikačného vývojára
Ako už bolo uvedené vyššie, aplikačný vývojár musí byť abstrahovaný od technických a inžinierskych aspektov implementácie interakcie viacerých aplikácií, aby sa mohla očakávať dobrá produktivita vývoja.
Skúsime vyriešiť dosť zložitú integračnú úlohu, špeciálne vymyslenú pre tento článok. Bude to "herná" úloha s účasťou troch aplikácií, kde každá z nich definuje nejaké doménové meno: "app1", "app2", "app3".
Vo vnútri každej aplikácie sa spúšťajú obchodné procesy, ktoré začínajú "hráť s loptou" cez integračnú zbernicu. V úlohe lopty budú vystupovať správy s názvom "Lopta".
Pravidlá hry:
- prvý hráč – iniciátor. Pozýva ostatných hráčov do hry, začína hru a môže ju kedykoľvek ukončiť;
- ostatní hráči oznamujú svoju účasť v hre, "zoznamujú" sa navzájom a s prvým hráčom;
- prijatím lopty, hráč si vyberá iného zúčastneného hráča a odovzdáva mu loptu. Vedie sa evidencia celkového počtu prihrávok;
- Každý hráč má "energiu", ktorá sa znižuje s každým prihratím lopty týmto hráčom. Po vyčerpaní energie hráč odstúpi z hry a vyhlási svoj odchod;
- ak hráč zostane sám, okamžite vyhlasuje svoj odchod;
- keď všetci hráči odstúpia, prvý hráč vyhlasuje koniec hry. Ak bol z hry vylúčený skôr, zostáva sledovať hru, aby ju ukončil.
Na vyriešenie tejto úlohy využijem našu DSL pre biznis procesy, ktorá umožňuje popísať logiku v Kotline kompaktným spôsobom s minimom boilerplate kódu.
V aplikácii app1 bude fungovať biznis proces prvého hráča (ten, ktorý hru inicioval):
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()
// Toto je trieda instance procesu: enkapsuluje jej vnútorný stav
class InitialPlayer : ProcessImpl(initialPlayerModel) {
var playerName: String by persistent("Player1")
var energy: Int by persistent(30)
var players: PlayersList by persistent(PlayersList())
var shotCounter: Int = 0
}
// Toto je deklarácia modelu procesu: vytvára sa raz, používa sa všetkými
// instanciami procesu zodpovedajúcej triedy
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Podľa pravidiel je prvý hráč iniciátorom hry a musí byť jediný
uniqueConstraint = UniqueConstraints.singleton
// Vyhlasujeme aktivity, z ktorých sa skladá podnikový proces
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "Pripojilo sa ${players.size} hráčov. Začneme?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... pripojil sa hráč ${signal.data} ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... hráč ${signal.data} je vonku ...")
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val throwStartBall = messageSend("Ball") {
messageData = { 1 }
activation = { selectNextPlayer() }
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
// Teraz konštruujeme graf procesu z vyhlásených aktivít
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)
}
// Na aktivity pripojíme ďalšie spracovatele pre logovanie
sendNewGameSignal.onExit { println("Začnime hrať!") }
sendStopGameSignal.onExit { println("Zastavte!") }
sendPlayerOut.onExit { println("$playerName: Som vonku!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Krok ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Okrem vykonávania obchodnej logiky dokáže uvedený kód generovať objektový model obchodného procesu, ktorý môže byť vizualizovaný vo forme diagramu. Vizualizátor sme zatiaľ nerealizovali, takže sme museli venovať trochu času kresleniu (tu som ľahko zjednodušil notáciu BPMN v časti používania brán, aby som zlepšil konzistenciu diagramu s uvedeným kódom):

Aplikácia app2 bude obsahovať obchodný proces iného hráča:
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}")
}Diagram:

V aplikácii app3 urobíme hráča s mierne odlišným správaním: namiesto náhodného výberu ďalšieho hráča bude konať podľa algoritmu 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()
class RoundRobinPlayer : ProcessImpl(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(
name = "RoundRobinPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("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("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("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.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}")
}In other respects, the player's behavior is the same as before, so the diagram remains unchanged.
Now we need a test to run all of this. I will only present the test code to avoid cluttering the article with boilerplate (in fact, I used a test environment created earlier for testing the integration of other business processes):
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");
// Now we need to wait a bit while the players "get to know" each other.
// Waiting through sleep is a bad solution, but it's the simplest.
// Don't do this in serious tests!
Thread.sleep(1000);
// Starting the game, closing user activity
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 playerParams(String name, int energy) {
Map params = new HashMap();
params.put("playerName", name);
params.put("energy", energy);
return params;
}We are starting the test, checking the log:
console output
Zamknutie kľúča lock://app1/process/InitialPlayer
Začnime hrať!
Zrušenie blokácie kľúča lock://app1/process/InitialPlayer
Hráč2: Som tu!
Hráč3: Som tu!
Hráč4: Som tu!
Hráč5: Som tu!
... pripojenie hráča Hráč2 ...
... pripojenie hráča Hráč4 ...
... pripojenie hráča Hráč3 ...
... pripojenie hráča Hráč5 ...
Krok 1: Hráč1 >>> Hráč3
Krok 2: Hráč3 >>> Hráč5
Krok 3: Hráč5 >>> Hráč3
Krok 4: Hráč3 >>> Hráč4
Krok 5: Hráč4 >>> Hráč3
Krok 6: Hráč3 >>> Hráč4
Krok 7: Hráč4 >>> Hráč5
Krok 8: Hráč5 >>> Hráč2
Krok 9: Hráč2 >>> Hráč5
Krok 10: Hráč5 >>> Hráč4
Krok 11: Hráč4 >>> Hráč2
Krok 12: Hráč2 >>> Hráč4
Krok 13: Hráč4 >>> Hráč1
Krok 14: Hráč1 >>> Hráč4
Krok 15: Hráč4 >>> Hráč3
Krok 16: Hráč3 >>> Hráč1
Krok 17: Hráč1 >>> Hráč2
Krok 18: Hráč2 >>> Hráč3
Krok 19: Hráč3 >>> Hráč1
Krok 20: Hráč1 >>> Hráč5
Krok 21: Hráč5 >>> Hráč1
Krok 22: Hráč1 >>> Hráč2
Krok 23: Hráč2 >>> Hráč4
Krok 24: Hráč4 >>> Hráč5
Krok 25: Hráč5 >>> Hráč3
Krok 26: Hráč3 >>> Hráč4
Krok 27: Hráč4 >>> Hráč2
Krok 28: Hráč2 >>> Hráč5
Krok 29: Hráč5 >>> Hráč2
Krok 30: Hráč2 >>> Hráč1
Krok 31: Hráč1 >>> Hráč3
Krok 32: Hráč3 >>> Hráč4
Krok 33: Hráč4 >>> Hráč1
Krok 34: Hráč1 >>> Hráč3
Krok 35: Hráč3 >>> Hráč4
Krok 36: Hráč4 >>> Hráč3
Krok 37: Hráč3 >>> Hráč2
Krok 38: Hráč2 >>> Hráč5
Krok 39: Hráč5 >>> Hráč4
Krok 40: Hráč4 >>> Hráč5
Krok 41: Hráč5 >>> Hráč1
Krok 42: Hráč1 >>> Hráč5
Krok 43: Hráč5 >>> Hráč3
Krok 44: Hráč3 >>> Hráč5
Krok 45: Hráč5 >>> Hráč2
Krok 46: Hráč2 >>> Hráč3
Krok 47: Hráč3 >>> Hráč2
Krok 48: Hráč2 >>> Hráč5
Krok 49: Hráč5 >>> Hráč4
Krok 50: Hráč4 >>> Hráč2
Krok 51: Hráč2 >>> Hráč5
Krok 52: Hráč5 >>> Hráč1
Krok 53: Hráč1 >>> Hráč5
Krok 54: Hráč5 >>> Hráč3
Krok 55: Hráč3 >>> Hráč5
Krok 56: Hráč5 >>> Hráč2
Krok 57: Hráč2 >>> Hráč1
Krok 58: Hráč1 >>> Hráč4
Krok 59: Hráč4 >>> Hráč1
Krok 60: Hráč1 >>> Hráč4
Krok 61: Hráč4 >>> Hráč3
Krok 62: Hráč3 >>> Hráč2
Krok 63: Hráč2 >>> Hráč5
Krok 64: Hráč5 >>> Hráč4
Krok 65: Hráč4 >>> Hráč5
Krok 66: Hráč5 >>> Hráč1
Krok 67: Hráč1 >>> Hráč5
Krok 68: Hráč5 >>> Hráč3
Krok 69: Hráč3 >>> Hráč4
Krok 70: Hráč4 >>> Hráč2
Krok 71: Hráč2 >>> Hráč5
Krok 72: Hráč5 >>> Hráč2
Krok 73: Hráč2 >>> Hráč1
Krok 74: Hráč1 >>> Hráč4
Krok 75: Hráč4 >>> Hráč1
Krok 76: Hráč1 >>> Hráč2
Krok 77: Hráč2 >>> Hráč5
Krok 78: Hráč5 >>> Hráč4
Krok 79: Hráč4 >>> Hráč3
Krok 80: Hráč3 >>> Hráč1
Krok 81: Hráč1 >>> Hráč5
Krok 82: Hráč5 >>> Hráč1
Krok 83: Hráč1 >>> Hráč4
Krok 84: Hráč4 >>> Hráč5
Krok 85: Hráč5 >>> Hráč3
Krok 86: Hráč3 >>> Hráč5
Krok 87: Hráč5 >>> Hráč2
Krok 88: Hráč2 >>> Hráč3
Hráč2: Som vonku!
Krok 89: Hráč3 >>> Hráč4
... hráč Hráč2 je vonku ...
Krok 90: Hráč4 >>> Hráč1
Krok 91: Hráč1 >>> Hráč3
Krok 92: Hráč3 >>> Hráč1
Krok 93: Hráč1 >>> Hráč4
Krok 94: Hráč4 >>> Hráč3
Krok 95: Hráč3 >>> Hráč5
Krok 96: Hráč5 >>> Hráč1
Krok 97: Hráč1 >>> Hráč5
Krok 98: Hráč5 >>> Hráč3
Krok 99: Hráč3 >>> Hráč5
Krok 100: Hráč5 >>> Hráč4
Krok 101: Hráč4 >>> Hráč5
Hráč4: Som vonku!
... hráč Hráč4 je vonku ...
Krok 102: Hráč5 >>> Hráč1
Krok 103: Hráč1 >>> Hráč3
Krok 104: Hráč3 >>> Hráč1
Krok 105: Hráč1 >>> Hráč3
Krok 106: Hráč3 >>> Hráč5
Krok 107: Hráč5 >>> Hráč3
Krok 108: Hráč3 >>> Hráč1
Krok 109: Hráč1 >>> Hráč3
Krok 110: Hráč3 >>> Hráč5
Krok 111: Hráč5 >>> Hráč1
Krok 112: Hráč1 >>> Hráč3
Krok 113: Hráč3 >>> Hráč5
Krok 114: Hráč5 >>> Hráč3
Krok 115: Hráč3 >>> Hráč1
Krok 116: Hráč1 >>> Hráč3
Krok 117: Hráč3 >>> Hráč5
Krok 118: Hráč5 >>> Hráč1
Krok 119: Hráč1 >>> Hráč3
Krok 120: Hráč3 >>> Hráč5
Krok 121: Hráč5 >>> Hráč3
Hráč5: Som vonku!
... hráč Hráč5 je vonku ...
Krok 122: Hráč3 >>> Hráč5
Krok 123: Hráč5 >>> Hráč1
Hráč5: Som vonku!
Krok 124: Hráč1 >>> Hráč3
... hráč Hráč5 je vonku ...
Krok 125: Hráč3 >>> Hráč1
Krok 126: Hráč1 >>> Hráč3
Hráč1: Som vonku!
... hráč Hráč1 je vonku ...
Krok 127: Hráč3 >>> Hráč3
Hráč3: Som vonku!
Krok 128: Hráč3 >>> Hráč3
... hráč Hráč3 je vonku ...
Hráč3: Som vonku!
Zastavte!
Krok 129: Hráč3 >>> Hráč3
Hráč3: Som vonku!Z tohto všetkého môžeme urobiť niekoľko dôležitých záverov:
- pri použití potrebných nástrojov môžu aplikovaní vývojári vytvárať integračné interakcie medzi aplikáciami bez toho, aby sa odtrhli od obchodnej logiky;
- komplexnosť integračnej úlohy, ktorá si vyžaduje inžinierske zručnosti, je možné skryť v rámci rámca, ak je to pôvodne zakotvené v architektúre rámca. Ťažkosť úlohy sa však skryť nedá, takže riešenie ťažkej úlohy v kóde bude vyzerať zodpovedajúcim spôsobom;
- pri navrhovaní integračnej logiky je nevyhnutné brať do úvahy eventually consistency a absenciu linearizovateľnosti zmien stavu všetkých účastníkov integrácie. To núti komplikovať logiku, aby bola necitlivá na poradie vzniku vonkajších udalostí. V našom príklade je hráč nútený zapojiť sa do hry až po tom, čo oznámi svoj odchod z hry: ostatní hráči mu budú naďalej posielať loptu, kým informácia o jeho odchode nedorazí a nebude spracovaná všetkými účastníkmi. Táto logika nevyplýva z pravidiel hry a predstavuje kompromisné riešenie v rámci vybranej architektúry.
Ďalej sa porozprávame o rôznych nuansách nášho riešenia, kompromisoch a iných aspektoch.
Všetky správy – v jednej fronte
Všetky integrované aplikácie pracujú s jednou integračnou zbernicou, ktorá je predstavená ako externý broker, jednou frontou BPMQueue – pre správy a jedným témou BPMTopic – pre signály (udalosti). Posielať všetky správy cez jednu frontu je samo o sebe kompromisom. Na úrovni obchodnej logiky je teraz možné zavádzať nekonečne nové typy správ bez toho, aby sa menila štruktúra systému. Je to významné zjednodušenie, ale nesie so sebou určité riziká, ktoré sa v kontexte našich typických úloh zdali nám neprimerane veľké.

Existuje však jeden háček: každá aplikace filtruje své zprávy z fronty již na vstupu podle názvu své domény. Doména může být uvedena i v signálech, pokud je potřeba omezit "oblast viditelnosti" signálu na jednu jedinou aplikaci. To by mělo zvýšit propustnost sběrnice, ale obchodní logika nyní musí pracovat s názvy domén: pro adresaci zpráv – povinné, pro signály – žádoucí.
Zajištění spolehlivosti integrační sběrnice
Spolehlivost se skládá z několika aspektů:
- vybraný broker zpráv – kritická komponenta architektury a jediný bod selhání: musí být dostatečně odolný proti výpadkům. Je třeba používat pouze ověřené realizace s dobrou podporou a velkou komunitou;
- je nutné zajistit vysokou dostupnost brokera zpráv, k čemuž by měl být fyzicky oddělen od integrovaných aplikací (zajištění vysoké dostupnosti aplikací s obchodní logikou je výrazně složitější a dražší);
- broker musí zajistit záruky doručení "alespoň jednou". To je povinný požadavek pro spolehlivou práci integrační sběrnice. Na úrovni záruk "přesně jednou" není žádná potřeba: obchodní procesy obvykle nejsou citlivé na opakované přijetí zpráv nebo událostí, a v zvláštních úlohách, kde je to důležité, je jednodušší přidat dodatečnou kontrolu do obchodní logiky, než neustále používat dostatečně "drahé" záruky;
- odesílání zpráv a signálů je třeba zahrnout do celkové transakce se změnami stavu obchodních procesů a doménových dat. Preferovanou variantou bude použití vzoru , ale to bude vyžadovat existenci další tabulky v databázi a retranslátora. V aplikacích JEE lze tento bod zjednodušit použitím místního JTA manažera, ale připojení k vybranému brokerovi by mělo umět pracovat v režimu ;
- zpracovatelé příchozích zpráv a událostí by také měli pracovat s transakcí změny stavu obchodního procesu: pokud je taková transakce vrácena, musí být také přijetí zprávy zrušeno;
- zprávy, které se nepodařilo doručit kvůli chybám, je nutné uchovávat v samostatném úložišti (Dead Letter Queue). Na tento účel sme vytvorili samostatnú platformovú mikroservisu, ktorá uchováva tieto správy vo svojom úložisku, indexuje ich podľa atribútov (na rýchle zhromažďovanie a vyhľadávanie) a poskytuje API na prezeranie, opätovné odosielanie na cieľovú adresu a odstraňovanie správ. Správcovia systému môžu s touto službou pracovať cez svoj webový rozhranie;
- vo nastaveniach brokera je potrebné nastaviť počet opakovaných pokusov o doručenie a oneskorenie medzi dodaniami, aby sa znížila pravdepodobnosť, že správy sa dostanú do DLQ (vypočítať optimálne parametre je prakticky nemožné, ale je možné konať empiricky a prispôsobiť ich počas prevádzky);
- úložisko DLQ by malo byť neustále monitorované a monitorovací systém by mal upozorniť správcov systému, aby sa na nedoručené správy reagovalo čo najrýchlejšie. To umožní znížiť „oblasť postihnutia“ vzniknutej poruchy alebo chyby biznis logiky;
- integračná zbernica by mala byť necitlivá na dočasnú neprítomnosť aplikácií: prihlásenia na tému by mali byť trvalé a doménové meno aplikácie by malo byť jedinečné, aby počas neprítomnosti aplikácie nikto iný sa nepokúšal spracovať jej správy z frontu.
Zabezpečenie vlákna bezporuchovosti biznis logiky
Jednému a tomu istému inštanciu biznis procesu môže doraziť viacero správ a udalostí, ktorých spracovanie sa spustí paralelne. Zároveň by pre aplikačného vývojára malo byť všetko jednoduché a bezporuchové.
Biznis logika procesu spracováva každú externú udalosť ovplyvňujúcu tento biznis proces jednotlivé. Takéto udalosti môžu byť:
- spustenie inštancie biznis procesu;
- aktívneho užívateľa vzťahujúceho sa na aktivitu v rámci biznis procesu;
- príchod správy alebo signálu, na ktorý je prihlásená inštancia biznis procesu;
- spustenie časovača, ktorý nastavila inštancia biznis procesu;
- riadiaci zásah cez API (napríklad núdzové prerušenie procesu).
Každá taká udalosť môže zmeniť stav inštancie obchodného procesu: môžu sa ukončiť niektoré aktivity a začať iné, môžu sa zmeniť hodnoty perzistentných vlastností. Uzavretie akejkoľvek aktivity môže viesť k aktivácii jednej alebo viacerých nasledujúcich aktivít. Tieto, na druhej strane, sa môžu zastaviť v očakávaní iných udalostí alebo, ak nepotrebujú žiadne ďalšie údaje, sa môžu ukončiť v tej istej transakcii. Pred uzatvorením transakcie sa nový stav obchodného procesu uloží do databázy, kde bude čakať na nastúpenie ďalšej vonkajšej udalosti.
Perzistentné údaje obchodného procesu, uložené v relačnej databáze, sú veľmi praktickým bodom synchronizácie spracovania, ak sa použije SELECT FOR UPDATE. Ak jednej transakcii sa podarilo získať stav obchodného procesu z databázy na jeho zmenu, žiadna iná transakcia zároveň nemôže získať ten istý stav na iné zmeny, a po dokončení prvej transakcie druhá zaručene získa už zmenený stav.
Použitím pesimistických zámkov na strane databázového systému splníme všetky potrebné požiadavky , a zároveň zachováme možnosť škálovania aplikácie s obchodnou logikou zvýšením počtu spustených inštancií.
Avšak pesimistické zámky nám hrozia mŕtvymi zámkami, a teda SELECT FOR UPDATE predsa len stojí za to obmedziť na určitý rozumný časový limit v prípade výskytu mŕtvych zámkov pri nejakých háklivých prípadoch v obchodnej logike.
Ďalším problémom je synchronizácia štartu obchodného procesu. Kým nie je inštancia obchodného procesu, jeho stav v databáze tiež nie je, preto popísaná metóda nie je vhodná. Ak je potrebné zabezpečiť jedinečnosť inštancie obchodného procesu v určitom rozsahu, bude potrebný nejaký synchronizačný objekt, asociovaný s triedou procesu a zodpovedajúcim rozsahom. Na riešenie tohto problému používame iný mechanizmus zamykania, ktorý umožňuje získať zámok na ľubovoľný zdroj, zadaný kľúčom vo formáte URI, cez externú službu.
V našich príkladoch obchodný proces InitialPlayer obsahuje vyhlásenie
uniqueConstraint = UniqueConstraints.singletonPreto sa v logu objavujú správy o prevzatí a uvoľnení zámku príslušného kľúča. Pri iných obchodných procesoch takéto správy neexistujú: uniqueConstraint nebol stanovený.
Problémy obchodných procesov s pretrvávajúcim stavom
Niekedy pretrvávajúci stav nielenže pomáha, ale aj veľmi prekáža pri vývoji.
Problémy nastávajú, keď je potrebné vykonať zmeny v obchodnej logike a/alebo v modeli obchodného procesu. Nie každá takáto zmena sa ukazuje ako kompatibilná so starým stavom obchodných procesov. Ak existuje v databáze veľa "živých" inštancií, vykonanie nekompatibilných zmien môže spôsobiť množstvo nepríjemností, s ktorými sme sa často stretávali pri použití jBPM.
V závislosti od hĺbky zmien je možné postupovať dvoma cestami:
- vytvoriť nový typ obchodného procesu, aby sa do starého neuskutočnili nekompatibilné zmeny, a používať ho namiesto starého pri spúšťaní nových inštancií. Staré inštancie budú pokračovať v práci "starým spôsobom";
- migrácia pretrvávajúceho stavu obchodných procesov pri aktualizácii obchodnej logiky.
Prvá cesta je jednoduchšia, ale má svoje obmedzenia a nedostatky, ako napríklad:
- duplikovanie obchodnej logiky v mnohých modeloch obchodných procesov, nárast objemu obchodnej logiky;
- často je potrebný okamžitý prechod na novú obchodnú logiku (v oblasti integračných úloh – takmer vždy);
- vývojár nevie, kedy možno odstrániť zastarané modely.
V praxi používame oba prístupy, ale prijali sme niekoľko rozhodnutí, aby sme si uľahčili život:
- v databáze je pretrvávajúci stav obchodného procesu uchovávaný v ľahko čiteľnej a ľahko spracovateľnej forme: v riadku formátu JSON. To umožňuje vykonávať migrácie ako vo vnútri aplikácie, tak aj vonku. V krajnom prípade je možné manuálne upraviť (obzvlášť užitočné pri vývoji počas ladenia);
- integračná obchodná logika nepoužíva názvy obchodných procesov, aby sa v akomkoľvek momente mohla nahradiť implementácia jedného z účastníckych procesov novou, s novým názvom (napríklad „InitialPlayerV2“). Prepojenie sa deje prostredníctvom názvov správ a signálov.
- Model procesu má číslo verzie, ktoré zvyšujeme, ak do tejto modelovej štruktúry zavedieme nekompatibilné zmeny, a toto číslo je uložené spolu so stavom inštancie procesu;
- Persistovaný stav procesu sa načíta z databázy najprv do pohodlného objektového modelu, s ktorým môže pracovať migračný proces, ak sa číslo verzie modelu zmenilo;
- Migračný proces je umiestnený vedľa obchodnej logiky a je volaný „lenivé“ pre každú inštanciu obchodného procesu v momente jej obnovenia z databázy;
- Ak je potrebné migrovať stav všetkých inštancií procesu operatívne a súčasne, používajú sa klasickejšie riešenia migrácie databází, ale tam treba pracovať s JSON.
Potrebujeme ešte jeden rámec pre obchodné procesy?
V článku opísané riešenia nám umožnili značne si zjednodušiť život, rozšíriť okruh otázok riešených na úrovni aplikovanej vývojovej činnosti, a spraviť nápady na oddelenie obchodnej logiky do mikroslužieb atraktívnejšími. Bolo vykonané veľa práce, vytvorený veľmi „ľahký“ rámec pre obchodné procesy a tiež pomocné komponenty na riešenie označených problémov v kontexte širokého okruhu aplikovaných úloh. Máme chuť podeliť sa o tieto výsledky a uvoľniť vývoj spoločných komponentov do verejného prístupu pod slobodnou licenciou. To si bude vyžadovať určité úsilie a čas. Pochopenie dopytu po takýchto riešeniach by mohlo byť pre nás dodatočným podnetom. V navrhnutom článku sa venovalo veľmi málo pozornosti možnostiam samotného rámca, ale niektoré z nich sú viditeľné z predložených príkladov. Ak nakoniec zverejníme náš rámec, bude mu venovaný samostatný článok. Zatiaľ budeme vďační, ak zanecháte malú spätnú väzbu, odpovedaním na otázku:
Len registrovaní používatelia sa môžu zúčastniť prieskumu. , prosím.
Potrebujeme ešte jeden rámec pre obchodné procesy?
18,8%Áno, už dlhšie hľadáme niečo podobné3
12,5%Zaujíma ma, aby som sa o vašej implementácii dozvedel viac, môže sa to hodiť2
6,2%Používame jeden z existujúcich rámcov, ale zvažujeme výmenu1
18,8%Používame jeden z existujúcich rámcov, všetko vyhovuje3
18,8%Zvládame to bez rámca3
25,0%Píšeme si vlastný4
Hlasovalo 16 používateľov. Zdržalo sa 7 používateľov.
Zdroj: habr.com
