
Pozdrav, Habr!
Naša tvrtka specijalizira se za razvoj ERP softverskih rješenja, pri čemu veliki dio zauzimaju transakcijski sustavi s ogromnom količinom poslovne logike i dokumentarnog prometa. Moderne verzije naših proizvoda temelje se na JavaEE tehnologijama, ali također aktivno eksperimentiramo s mikroservisima. Jedno od najproblematičnijih područja takvih rješenja je integracija različitih pod sustava koji se odnose na srodna područja. Zadaci integracije uvijek su nam zadavali velike glavobolje, bez obzira na arhitektonske stilove, tehnološke skupove i okvire koje smo koristili, međutim, u posljednje vrijeme došlo je do napretka u rješavanju takvih zadataka.
U članku koji vam predstavljamo, reći ću vam o iskustvima i arhitektonskim istraživanjima NPO 'Krista' u ovoj oblasti. Također ćemo razmotriti primjer jednostavnog rješenja integracijskog zadatka iz perspektive aplikacijskog programera i otkriti što se krije iza te jednostavnosti.
Odricanje
Arhitektonska i tehnička rješenja opisana u članku predlažem na temelju osobnog iskustva u kontekstu specifičnih zadataka. Ova rješenja ne pretendiraju na univerzalnost i mogu se pokazati neoptimalnim u drugim uvjetima korištenja.
Što BPM ima s tim?
Za odgovor na ovo pitanje potrebno je malo dublje zaroniti u specifičnosti aplikacijskih zadataka naših rješenja. Glavni dio poslovne logike u našem tipičnom transakcijskom sustavu je unos podataka u Bazu podataka putem korisničkih sučelja, ručna i automatizirana provjera tih podataka, njihovo provođenje kroz određeni workflow, objavljivanje u drugi sustav / analitičku bazu / arhivu i generiranje izvještaja. Tako, ključna funkcija sustava za klijente je automatizacija njihovih unutarnjih poslovnih procesa.
Radi praktičnosti, u komunikaciji koristimo termin 'dokumenat' kao neku apstrakciju skupa podataka povezanih zajedničkim ključem na koji se može 'priključiti' određeni workflow.
Ali što je s logikom integracije? Jer zadaća integracije proizlazi iz arhitekture sustava, koja je 'razerana' na dijelove NE po zahtjevu klijenta, već pod utjecajem drugih čimbenika:
- pod utjecajem Conwayevog zakona;
- kroz ponovnu upotrebu podsustava prethodno razvijenih za druge proizvode;
- prema odluci arhitekta, uzimajući u obzir nefunkcionalne zahtjeve.
Postoji velika iskušenja razdvojiti integracijsku logiku od poslovne logike glavnog radnog toka kako ne bi onečistili poslovnu logiku integracijskim artefaktima i oslobodili aplikacijskog programera potrebe da razmatra specifičnosti arhitektonskog pejzaža sustava. Ovaj pristup ima niz prednosti, međutim praksa pokazuje njegovu neučinkovitost:
- rješenje integracijskih zadataka obično se svodi na najjednostavnije opcije u obliku sinkronih poziva zbog ograničenih točaka proširenja u implementaciji glavnog radnog toka (o nedostacima sinkrone integracije – nešto kasnije);
- integracijski artefakti svejedno prodiru u glavnu poslovnu logiku kada je potrebna povratna informacija iz druge podsustava;
- aplikacijski programer ignorira integraciju i može je lako slomiti mijenjajući radni tok;
- sustav prestaje biti jedinstvena cjelina s gledišta korisnika, postaju vidljive 'šavove' između podsustava, pojavljuju se suvišni korisnički operacije koje pokreću prijenos podataka iz jednog podsustava u drugi.
Drugi pristup je razmatranje integracijskih interakcija kao neizostavnog dijela osnovne poslovne logike i radnih procesa. Kako bi zahtjevi za kvalifikacijom aplikacijskih programera ostali razmjerni, stvaranje novih integracijskih interakcija treba biti lako i bez napora, s minimalnim mogućnostima za izbor načina rješavanja. To je teže nego što se čini: alat mora biti dovoljno moćan da omogući korisniku potrebnu raznolikost primjena, a istovremeno ne smije dopustiti „pucanje u vlastitu nogu“. Postoji mnoštvo pitanja na koja inženjer mora odgovoriti u kontekstu integracijskih zadataka, a za koja aplikacijski programer ne bi trebao razmišljati u svom svakodnevnom radu: granice transakcija, dosljednost, atomarnost, sigurnost, skalabilnost, raspodjela opterećenja i resursa, usmjeravanje, maršaliranje, širenje i prebacivanje konteksta itd. Potrebno je ponuditi aplikacijskim programerima dovoljno jednostavne obrasce rješenja u kojima su već skriveni odgovori na sva takva pitanja. Ti obrasci trebaju biti dovoljno sigurni: poslovna logika se često mijenja, što povećava rizik od pogrešaka, a cijena pogrešaka trebala bi ostati na dovoljno niskoj razini.
Ali kakve to veze ima s BPM-om? Ima mnogo načina za implementaciju radnog toka...
Doista, u našim rješenjima vrlo je popularna druga realizacija poslovnih procesa – putem deklarativnog definiranja dijagrama prijelaza stanja i povezivanja handlera s poslovnom logikom na prijelazima. Pri tome je stanje koje određuje trenutnu poziciju „dokumenta“ u poslovnom procesu atribut samog „dokumenta“.

Ovako izgleda proces na početku projekta
Popularnost ove realizacije proizlazi iz relativne jednostavnosti i brzine stvaranja linearnih poslovnih procesa. Međutim, kako se programski sustavi konstantno komplikuju, automatizirani deo poslovnog procesa raste i postaje složeniji. Pojavljuje se potreba za dekompozicijom, ponovnom upotrebom delova procesa, kao i za grananjem procesa, tako da se svaka grana izvršava paralelno. U takvim okolnostima alat postaje nezgodan, a dijagram prelaza stanja gubi informativnost (integracijska interakcija se uopšte ne odražava na dijagramu).

Ovako izgleda proces nakon nekoliko iteracija razjašnjavanja zahteva.
Rešenje za ovu situaciju bila je integracija motora u neke proizvode s najsloženijim poslovnim procesima. U kratkoročnoj perspektivi ovo rešenje je imalo određeni uspeh: pojavila se mogućnost realizacije složenih poslovnih procesa uz očuvanje dovoljno informativnog i aktuelnog dijagrama u notaciji .

Mali deo složenog poslovnog procesa
U dugoročnoj perspektivi, rešenje nije ispunilo očekivanja: visoka radna opterećenost pri stvaranju poslovnih procesa putem vizualnih alata nije omogućila postizanje prihvatljivih pokazatelja produktivnosti, a sam alat postao je jedan od najomraženijih među programerima. Takođe su postojale primedbe na unutrašnju strukturu motora, što je dovelo do pojave mnogih "zakrpa" i "štaka".
Glavna pozitivna tačka primene jBPM-a bila je spoznaja koristi i štete od postojanja vlastitog persistentnog stanja instancije poslovnog procesa. Takođe smo videli mogućnost primene procesnog pristupa za realizaciju složenih protokola integracije između različitih aplikacija sa korišćenjem asinkronih interakcija putem signala i poruka. Postojanje persistentnog stanja igra u tome ključnu ulogu.
Na osnovu rečenog možemo doneti zaključak: procesni pristup u stilu BPM omogućava nam rešavanje širokog spektra zadataka automatizacije sve složenijih poslovnih procesa, harmonično uvođenje integracionih aktivnosti u ove procese i očuvanje mogućnosti vizualnog prikazivanja realizovanog procesa u odgovarajućoj notaciji.
Nedostaci sinkronih poziva kao integracijskog obrazca
Pod sinkronom integracijom podrazumijeva se najjednostavniji blokirajući poziv. Jedna podsustav predstavlja serversku stranu i izlaže API s potrebnom metodom. Druga podsustav predstavlja klijentsku stranu i u pravom trenutku izvršava poziv s čekanjem na rezultat. Ovisno o arhitekturi sustava, klijentska i serverska strana mogu biti smještene ili u jednoj aplikaciji i procesu, ili u različitim. U drugom slučaju, potrebno je primijeniti neku implementaciju RPC-a i osigurati maršaliranje parametara i rezultata poziva.

Ovaj integracijski obrazac ima dovoljno velik set nedostataka, ali se vrlo široko koristi u praksi zbog svoje jednostavnosti. Brzina implementacije privlači i tjera ga da se koristi iznova i iznova u uvjetima "hitnih" rokova, upisujući rješenje u tehnički dug. Međutim, dogodi se i da neiskusni programeri primjenjuju ga nesvjesno, jednostavno ne sumnjajući u negativne posljedice.
Osim najočitijeg povećanja povezanosti podsustava, postoje i manje očiti problemi s "raspiranjem" i "produžavanjem" transakcija. Naime, ako poslovna logika unosi neke promjene, ne može se izbjeći bez transakcija, a transakcije, zauzvrat, blokiraju određene resurse aplikacije koji su zahvaćeni tim promjenama. To znači da dok jedna podsustav ne dobije odgovor od druge, neće moći završiti transakciju i ukloniti blokade. To značajno povećava rizik od različitih efekata:
- gubi se odziv sustava, korisnici dugo čekaju na odgovore na upite;
- server u potpunosti prestaje odgovarati na upite korisnika zbog prepunog bazena niti: većina niti "stoji" na blokadi resursa koje zauzima transakcija;
- počinju se pojavljivati deadlock-ovi: vjerojatnost njihova nastanka uvelike ovisi o trajanju transakcija, broju poslovnih logika uključenih u transakciju i blokadama;
- javljaju se greške zbog isteka vremena transakcije;
- server "pada" zbog OutOfMemory, ako zadatak zahtijeva obradu i promjenu velikih količina podataka, a prisutnost sinkronih integracija značajno otežava dijeljenje obrade na "lakše" transakcije.
S aspekta arhitekture, korištenje blokirajućih poziva tijekom integracije dovodi do gubitka kontrole kvalitete pojedinih podsustava: nije moguće osigurati ciljne pokazatelje kvalitete jednog podsustava odvojeno od pokazatelja kvalitete drugog podsustava. Ako se podsustavi razvijaju od strane različitih timova, to može predstavljati veliki problem.
Sve postaje još zanimljivije ako su integrirani podsustavi smješteni u različitim aplikacijama i treba izvršiti sinkronizirane promjene s obje strane. Kako osigurati transakcionost ovih promjena?
Ako se promjene unose u odvojenim transakcijama, tada će biti potrebno osigurati pouzdano upravljanje iznimkama i kompenzacijama, što u potpunosti neutralizira glavnu prednost sinkronih integracija – jednostavnost.
Na pamet također padaju distribuirane transakcije, ali ih ne koristimo u našim rješenjima: teško je osigurati pouzdanost.
«Saga» kao rješenje problema transakcija
S porastom popularnosti mikroslužbi, sve veću potražnju dobiva .
Ovaj obrazac odlično rješava prethodno navedene probleme dugih transakcija, kao i proširuje mogućnosti upravljanja stanjem sustava s poslovne logike: kompenzacija nakon neuspjele transakcije ne mora vraćati sustav u izvorno stanje, već može osigurati alternativnu putanju obrade podataka. To također omogućava da se uspješno završeni koraci obrade podataka ne ponavljaju prilikom ponovnih pokušaja dovođenja procesa do «dobrog» završetka.
Što je zanimljivo, u monolitnim sustavima ovaj obrazac također je relevantan kada se radi o integraciji slabo povezanih podsustava, a primjećuju se negativni efekti uzrokovani dugim transakcijama i odgovarajućim blokadama resursa.
U odnosu na naše poslovne procese u BPM stilu, implementacija «Sagi» se pokazuje vrlo jednostavnom: pojedini koraci «Sage» mogu biti zadani u obliku aktivnosti unutar poslovnog procesa, a postojano stanje poslovnog procesa određuje uključujući i unutarnje stanje «Sage». Dakle, nije nam potreban nikakav dodatni koordinacijski mehanizam. Biće potreban samo posrednik poruka koji podržava „barem jednom” garancije kao transport.
No, takvo rješenje također ima svoju «cijenu»:
- poslovna logika postaje složenija: potrebno je obraditi odštete;
- bit će potrebno odustati od pune dosljednosti, što može biti posebno osjetljivo za monolitne sustave;
- arhitektura se malo komplikuje, pojavljuje se dodatna potreba za posrednikom poruka;
- bit će potrebna dodatna sredstva za nadzor i administraciju (iako je to u cjelini dobro: kvaliteta usluge sustava će se poboljšati).
Za monolitne sustave opravdanost korištenja „Saga“ nije tako očita. Za mikroservise i druge SOA, gdje vjerojatno već postoji posrednik, a puna dosljednost je žrtvovana još na početku projekta, korisnost ovog obrasca može znatno nadmašiti nedostatke, posebno uz prisustvo prikladnog API-ja na razini poslovne logike.
Enkapsulacija poslovne logike u mikroservisima
Kada smo počeli eksperimentirati s mikroservisima, postavilo se razumno pitanje: gdje smjestiti pravilnu poslovnu logiku u odnosu na servis koji osigurava perzistenciju domennih podataka?
Kada se pogleda arhitektura različitih BPMS-a, može se činiti razumnim odvojiti poslovnu logiku od perzistencije: stvoriti sloj platformskih i domena neovisnih mikroservisa koji formiraju okruženje i kontejner za izvršavanje domenne poslovne logike, dok se perzistencija domennih podataka može urediti kao poseban sloj jednostavnih i laganih mikroservisa. U tom bi slučaju poslovni procesi izvodili orkestraciju servisa sloja perzistencije.

Takav pristup ima veliku prednost: funkcionalnost platforme može se neograničeno povećavati, a 'debljati' će se samo odgovarajući sloj platformskih mikroservisa. Poslovni procesi iz bilo kojeg domena odmah dobivaju mogućnost korištenja nove funkcionalnosti platforme čim bude ažurirana.
Detaljnija analiza otkrila je značajne nedostatke takvog pristupa:
- platformska usluga koja izvršava poslovnu logiku odmah za mnoge domene nosi velike rizike kao jedinstvena točka kvara. Česte promjene poslovne logike povećavaju rizik od grešaka koje mogu uzrokovati kvarove, koji se šire na cijeli sustav;
- problemi s performansama: poslovna logika radi sa svojim podacima kroz uski i spori sučelje:
- podatci će se ponovo marširati i obrađivati kroz mrežni stog;
- domen servis često će isporučivati više podataka nego što poslovna logika zahteva za obradu, zbog nedovoljnih mogućnosti parametrizacije upita na nivou spoljnog API servisa;
- nekoliko nezavisnih delova poslovne logike može ponovo tražiti iste podatke za obradu (može se ublažiti ovaj problem dodatkom sesijskih komponenti koje keširaju podatke, ali to dodatno komplikuje arhitekturu i stvara probleme sa aktuelnošću podataka i invalidacijom kaširanja);
- problemi transakcionalnosti:
- poslovni procesi sa persistentnim stanjem, čije čuvanje vrši platformski servis, razilaziće se sa domen podacima, a jednostavni putevi za rešenje ovog problema nisu predviđeni;
- iznošenje zaključavanja domen podataka van transakcije: ako domen poslovnoj logici treba da unese promene nakon provere ispravnosti aktuelnih podataka, potrebno je isključiti mogućnost konkurentne promene obrađivanih podataka. Spoljašnje zaključavanje podataka može pomoći u rešavanju problema, ali takvo rešenje nosi dodatne rizike i smanjuje ukupnu pouzdanost sistema;
- dodatne komplikacije prilikom ažuriranja: u nekim slučajevima, ažuriranje servisa za perzistenciju i poslovne logike mora se obaviti sinkrono ili u strogoj sekvenci.
Na kraju je bilo potrebno vratiti se korenima: enkapsulirati domen podatke i domen poslovnu logiku u jedan mikroservis. Ovaj pristup pojednostavljuje percepciju mikroservisa kao celokupne komponente u okviru sistema i ne generiše prethodno navedene probleme. To takođe nije besplatno:
- potrebna je standardizacija API-a za interakciju sa poslovnom logikom (posebno za obezbeđivanje korisničkih aktivnosti u okviru poslovnih procesa) i API platformskih servisa; potrebna je pažljivija briga o promeni API-a, direktnoj i obrnutoj kompatibilnosti;
- potrebno je dodati dodatne runtime biblioteke kako bi se obezbedila funkcionalnost poslovne logike unutar svakog takvog mikroservisa, i to postavlja nove zahteve za te biblioteke: lakonakornost i minimum tranzitivnih zavisnosti;
- Razvijači poslovne logike trebaju pratiti verzije biblioteka: ako neki mikroservis dugo nije ažuriran, najvjerojatnije će sadržavati zastarjelu verziju biblioteka. To može postati neočekivana prepreka za dodavanje nove funkcionalnosti i može zahtijevati migraciju stare poslovne logike tog servisa na nove verzije biblioteka, ukoliko su između verzija došlo do nekompatibilnih promjena.

Sloj platformskih servisa u toj arhitekturi također postoji, ali ovaj sloj ne oblikuje kontejner za izvršavanje domena poslovne logike, već samo njeno okruženje, pružajući pomoćne «platformske» funkcije. Ovaj sloj nije potreban samo za očuvanje lakoće domena mikroservisa, već i za centralizaciju upravljanja.
Na primjer, korisničke aktivnosti u poslovnim procesima dovode do nastanka zadataka. Međutim, radeći sa zadacima, korisnik mora vidjeti zadatke iz svih domena u zajedničkom popisu, što znači da mora postojati odgovarajući platformski servis za registraciju zadataka, oslobođen od domena poslovne logike. Očuvanje inkapsulacije poslovne logike u tom kontekstu prilično je problematično, i to je još jedan kompromis ove arhitekture.
Integracija poslovnih procesa kroz oči aplikativnog programera
Kao što je ranije spomenuto, aplikativni programer treba biti apstrahiran od tehničkih i inženjerskih aspekata implementacije interakcije nekoliko aplikacija, kako bi se mogla osigurati dobra produktivnost razvoja.
Pokušajmo riješiti prilično složen integracijski zadatak, posebno osmišljen za ovaj članak. To će biti «igra» s tri aplikacije, gdje svaka od njih određuje određeno domena: «app1», «app2», «app3».
Unutar svake aplikacije pokreću se poslovni procesi koji počinju «igrati loptu» preko integracijske sabirnice. Uloga lopte bit će zadaci s nazivom «Ball».
Pravila igre:
- prvi igrač – inicijator. On poziva druge igrače u igru, započinje igru i može je u bilo kojem trenutku završiti;
- ostali igrači izražavaju svoj sudjelovanje u igri, «upoznaju» jedni druge i prvog igrača;
- primivši loptu, igrač odabire drugog sudionika i predaje mu loptu. Broje se ukupni prijenosi.
- Svaki igrač ima "energiju", koja se smanjuje s svakim dodavanjem lopte od strane tog igrača. Kada energija istekne, igrač izlazi iz igre, objavljujući svoj odlazak;
- ako igrač ostane sam, odmah objavljuje odlazak;
- kada svi igrači izađu, prvi igrač objavljuje završetak igre. Ako je ranije izašao iz igre, ostaje da prati igru kako bi je završio.
Za rješavanje ovog zadatka iskoristit ću naš DSL za poslovne procese, koji omogućuje opisivanje logike na Kotlinu kompaktno, s minimumom boilerplatea.
U aplikaciji app1 će funkcionirati poslovni proces prvog igrača (koji je i inicijator igre):
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()
// Ova klasa predstavlja instancu procesa: enkapsulira njeno interno stanje
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
}
// Ovo je deklaracija modela procesa: kreira se jednom, koristi se od strane svih
// instanci procesa odgovarajuće klase
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Prema pravilima, prvi igrač je inicijator igre i mora biti jedini
uniqueConstraint = UniqueConstraints.singleton
// Deklariramo aktivnosti koje čine poslovni proces
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "Priključilo se ${players.size} igrača. Počinje li?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... pridružuje se igrač ${signal.data} ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... igrač ${signal.data} je napustio igru ...")
}
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
}
// Sada konstruiramo graf procesa od deklarisanih aktivnosti
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)
}
// Dodajemo dodatne obradne metode za logiranje na aktivnosti
sendNewGameSignal.onExit { println("Krenimo da igramo!") }
sendStopGameSignal.onExit { println("Stani!") }
sendPlayerOut.onExit { println("$playerName: Napustio sam igru!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Korak ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Osim izvršavanja poslovne logike, navedeni kod može izdati objektni model poslovnog procesa, koji se može vizualizirati u obliku dijagrama. Vizualizator još nismo implementirali, pa smo malo vremena proveli crtajući (ovdje sam malo pojednostavio BPMN notaciju u dijelu korištenja vrata, kako bih poboljšao usklađenost dijagrama s danim kodom):

Aplikacija app2 uključit će poslovni proces drugog igrač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()
class RandomPlayer : ProcessImpl(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(name = "RandomPlayer",
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() {
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}")
}Dijagram:

U aplikaciji app3 napravit ćemo igrača s malo drugačijim ponašanjem: umjesto nasumičnog odabira sljedećeg igrača, djelovat će prema 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}")
}Inače, ponašanje igrača se ne razlikuje od prethodnog, stoga dijagram ostaje nepromijenjen.
Sada nam je potreban test da bismo sve ovo pokrenuli. Navest ću samo kod samog testa kako ne bih opterećivao članak boilerplate-om (zapravo sam iskoristio testno okruženje stvoreno ranije za testiranje integracije drugih poslovnih procesa):
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");
// Sada treba malo pričekati dok se igrači "upoznaju" međusobno.
// Čekanje putem sleep - loša ideja, ali najjednostavnija.
// Nemojte ovako raditi u ozbiljnim testovima!
Thread.sleep(1000);
// Pokrećemo igru, zatvarajući korisničku aktivnost
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;
}Pokrećemo test, gledamo log:
izlaz konzole
Zaključana ključna pozicija lock://app1/process/InitialPlayer
Hajde da igramo!
Otključana ključna pozicija lock://app1/process/InitialPlayer
Igrač2: Tu sam!
Igrač3: Tu sam!
Igrač4: Tu sam!
Igrač5: Tu sam!
... pridružite se igraču Igrač2 ...
... pridružite se igraču Igrač4 ...
... pridružite se igraču Igrač3 ...
... pridružite se igraču Igrač5 ...
Korak 1: Igrač1 >>> Igrač3
Korak 2: Igrač3 >>> Igrač5
Korak 3: Igrač5 >>> Igrač3
Korak 4: Igrač3 >>> Igrač4
Korak 5: Igrač4 >>> Igrač3
Korak 6: Igrač3 >>> Igrač4
Korak 7: Igrač4 >>> Igrač5
Korak 8: Igrač5 >>> Igrač2
Korak 9: Igrač2 >>> Igrač5
Korak 10: Igrač5 >>> Igrač4
Korak 11: Igrač4 >>> Igrač2
Korak 12: Igrač2 >>> Igrač4
Korak 13: Igrač4 >>> Igrač1
Korak 14: Igrač1 >>> Igrač4
Korak 15: Igrač4 >>> Igrač3
Korak 16: Igrač3 >>> Igrač1
Korak 17: Igrač1 >>> Igrač2
Korak 18: Igrač2 >>> Igrač3
Korak 19: Igrač3 >>> Igrač1
Korak 20: Igrač1 >>> Igrač5
Korak 21: Igrač5 >>> Igrač1
Korak 22: Igrač1 >>> Igrač2
Korak 23: Igrač2 >>> Igrač4
Korak 24: Igrač4 >>> Igrač5
Korak 25: Igrač5 >>> Igrač3
Korak 26: Igrač3 >>> Igrač4
Korak 27: Igrač4 >>> Igrač2
Korak 28: Igrač2 >>> Igrač5
Korak 29: Igrač5 >>> Igrač2
Korak 30: Igrač2 >>> Igrač1
Korak 31: Igrač1 >>> Igrač3
Korak 32: Igrač3 >>> Igrač4
Korak 33: Igrač4 >>> Igrač1
Korak 34: Igrač1 >>> Igrač3
Korak 35: Igrač3 >>> Igrač4
Korak 36: Igrač4 >>> Igrač3
Korak 37: Igrač3 >>> Igrač2
Korak 38: Igrač2 >>> Igrač5
Korak 39: Igrač5 >>> Igrač4
Korak 40: Igrač4 >>> Igrač5
Korak 41: Igrač5 >>> Igrač1
Korak 42: Igrač1 >>> Igrač5
Korak 43: Igrač5 >>> Igrač3
Korak 44: Igrač3 >>> Igrač5
Korak 45: Igrač5 >>> Igrač2
Korak 46: Igrač2 >>> Igrač3
Korak 47: Igrač3 >>> Igrač2
Korak 48: Igrač2 >>> Igrač5
Korak 49: Igrač5 >>> Igrač4
Korak 50: Igrač4 >>> Igrač2
Korak 51: Igrač2 >>> Igrač5
Korak 52: Igrač5 >>> Igrač1
Korak 53: Igrač1 >>> Igrač5
Korak 54: Igrač5 >>> Igrač3
Korak 55: Igrač3 >>> Igrač5
Korak 56: Igrač5 >>> Igrač2
Korak 57: Igrač2 >>> Igrač1
Korak 58: Igrač1 >>> Igrač4
Korak 59: Igrač4 >>> Igrač1
Korak 60: Igrač1 >>> Igrač4
Korak 61: Igrač4 >>> Igrač3
Korak 62: Igrač3 >>> Igrač2
Korak 63: Igrač2 >>> Igrač5
Korak 64: Igrač5 >>> Igrač4
Korak 65: Igrač4 >>> Igrač5
Korak 66: Igrač5 >>> Igrač1
Korak 67: Igrač1 >>> Igrač5
Korak 68: Igrač5 >>> Igrač3
Korak 69: Igrač3 >>> Igrač4
Korak 70: Igrač4 >>> Igrač2
Korak 71: Igrač2 >>> Igrač5
Korak 72: Igrač5 >>> Igrač2
Korak 73: Igrač2 >>> Igrač1
Korak 74: Igrač1 >>> Igrač4
Korak 75: Igrač4 >>> Igrač1
Korak 76: Igrač1 >>> Igrač2
Korak 77: Igrač2 >>> Igrač5
Korak 78: Igrač5 >>> Igrač4
Korak 79: Igrač4 >>> Igrač3
Korak 80: Igrač3 >>> Igrač1
Korak 81: Igrač1 >>> Igrač5
Korak 82: Igrač5 >>> Igrač1
Korak 83: Igrač1 >>> Igrač4
Korak 84: Igrač4 >>> Igrač5
Korak 85: Igrač5 >>> Igrač3
Korak 86: Igrač3 >>> Igrač5
Korak 87: Igrač5 >>> Igrač2
Korak 88: Igrač2 >>> Igrač3
Igrač2: Izlazim!
Korak 89: Igrač3 >>> Igrač4
... igrač Igrač2 je van ...
Korak 90: Igrač4 >>> Igrač1
Korak 91: Igrač1 >>> Igrač3
Korak 92: Igrač3 >>> Igrač1
Korak 93: Igrač1 >>> Igrač4
Korak 94: Igrač4 >>> Igrač3
Korak 95: Igrač3 >>> Igrač5
Korak 96: Igrač5 >>> Igrač1
Korak 97: Igrač1 >>> Igrač5
Korak 98: Igrač5 >>> Igrač3
Korak 99: Igrač3 >>> Igrač5
Korak 100: Igrač5 >>> Igrač4
Korak 101: Igrač4 >>> Igrač5
Igrač4: Izlazim!
... igrač Igrač4 je van ...
Korak 102: Igrač5 >>> Igrač1
Korak 103: Igrač1 >>> Igrač3
Korak 104: Igrač3 >>> Igrač1
Korak 105: Igrač1 >>> Igrač3
Korak 106: Igrač3 >>> Igrač5
Korak 107: Igrač5 >>> Igrač3
Korak 108: Igrač3 >>> Igrač1
Korak 109: Igrač1 >>> Igrač3
Korak 110: Igrač3 >>> Igrač5
Korak 111: Igrač5 >>> Igrač1
Korak 112: Igrač1 >>> Igrač3
Korak 113: Igrač3 >>> Igrač5
Korak 114: Igrač5 >>> Igrač3
Korak 115: Igrač3 >>> Igrač1
Korak 116: Igrač1 >>> Igrač3
Korak 117: Igrač3 >>> Igrač5
Korak 118: Igrač5 >>> Igrač1
Korak 119: Igrač1 >>> Igrač3
Korak 120: Igrač3 >>> Igrač5
Korak 121: Igrač5 >>> Igrač3
Igrač5: Izlazim!
... igrač Igrač5 je van ...
Korak 122: Igrač3 >>> Igrač5
Korak 123: Igrač5 >>> Igrač1
Igrač5: Izlazim!
Korak 124: Igrač1 >>> Igrač3
... igrač Igrač5 je van ...
Korak 125: Igrač3 >>> Igrač1
Korak 126: Igrač1 >>> Igrač3
Igrač1: Izlazim!
... igrač Igrač1 je van ...
Korak 127: Igrač3 >>> Igrač3
Igrač3: Izlazim!
Stop!
Korak 128: Igrač3 >>> Igrač3
Igrač3: Izlazim!Iz svega ovoga možemo izvući nekoliko važnih zaključaka:
- uz potrebne alate, aplikacijski developeri mogu stvoriti integracijske interakcije između aplikacija bez prekidanja poslovne logike;
- složenost (complexity) integracijskog zadatka, koji zahtijeva inženjerske kompetencije, može se sakriti unutar okvira, ako se to od samog početka ugradi u arhitekturu okvira. Teškoća zadatka (difficulty) se ne može sakriti, stoga će rješenje teškog zadatka u kodu izgledati odgovarajuće;
- prilikom razvoja integracijske logike potrebno je obavezno uzeti u obzir eventually consistency i odsutnost linearizabilnosti promjene stanja svih sudionika integracije. To prisiljava na kompliciranje logike kako bi je učinilo neosjetljivom na redoslijed pojave vanjskih događaja. U našem primjeru, igrač mora sudjelovati u igri samo nakon što se izjasni o izlasku iz igre: drugi igrači će mu nastaviti dodavati loptu dok informacije o njegovom izlasku ne stignu i obrade svi sudionici. Ova logika ne proizlazi iz pravila igre i predstavlja kompromisno rješenje unutar odabrane arhitekture.
Sljedeće ćemo razgovarati o raznim nijansama našeg rješenja, kompromisima i drugim aspektima.
Sve poruke – u jednom redu
Sve integrirane aplikacije rade s jednom integracijskom sabirnicom, koja je predstavljena kao vanjski posrednik, jednim redom BPMQueue – za poruke i jednom temom BPMTopic – za signale (događaje). Propuštanje svih poruka kroz jedan red samo po sebi predstavlja kompromis. Na razini poslovne logike sada je moguće uvesti neograničen broj novih tipova poruka, ne uvodeći promjene u strukturu sustava. To je značajno pojednostavljenje, ali nosi određene rizike, koji su nam u kontekstu naših standardnih zadataka djelovali manje značajni.

Međutim, postoji jedna važna nijansa: svaka aplikacija filtrira 'svoje' poruke iz reda već na ulazu, prema imenu svog domene. Također, domena se može navesti i u signalima ako je potrebno ograničiti 'vidljivost' signala na jedinstvenu aplikaciju. To bi trebalo povećati propusnost sabirnice, ali poslovna logika sada mora raditi s imenima domena: što je obavezno za adresiranje poruka – za signale je poželjno.
Osiguranje pouzdanosti integracijske sabirnice
Pouzdanost se sastoji od nekoliko elemenata:
- izabrani posrednik poruka – kritično važna komponenta arhitekture i jedinstvena točka otkaza: mora biti dovoljno otporan na greške. Trebalo bi koristiti samo provjerene implementacije s dobrom podrškom i velikom zajednicom;
- mora se osigurati visoka dostupnost posrednika poruka, zbog čega on mora biti fizički odvojen od integriranih aplikacija (visoka dostupnost aplikacija s poslovnom logikom je znatno složenija i skuplja);
- posrednik mora osigurati 'barem jednom' garancije isporuke. To je obavezni zahtjev za pouzdan rad integracijske sabirnice. Nema potrebe za garancijama razine 'točno jednom': poslovni procesi obično nisu osjetljivi na ponovnu isporuku poruka ili događaja, a u posebnim zadacima gdje je to važno, lakše je dodati dodatnu provjeru u poslovnu logiku nego stalno koristiti dovoljno 'skupe' garancije;
- slanje poruka i signala treba uključiti u zajedničku transakciju s promjenom stanja poslovnih procesa i domena podataka. Preferirana opcija bi bila korištenje obrasca , ali će to zahtijevati dodatnu tablicu u bazi i preusmjerivač. U JEE aplikacijama ovo pitanje može se pojednostaviti korištenjem lokalnog JTA menadžera, ali povezivanje s odabranim posrednikom treba biti sposobno raditi u režimu ;
- obrada dolaznih poruka i događaja također treba raditi s transakcijom promjene stanja poslovnog procesa: ako se takva transakcija poništi, tada se i prijem poruke mora otkazati;
- poruke koje nisu uspjele biti isporučene zbog grešaka trebaju se pohraniti u posebno skladište (Dead Letter Queue). Za to smo stvorili poseban platformski mikroservis koji pohranjuje takve poruke u svoje spremište, indeksira ih prema atributima (radi brze grupacije i pretrage) i izlaže API za pregled, ponovno slanje na odredište i brisanje poruka. Sustavni administratori mogu raditi s ovom uslugom putem svoje web sučelja;
- u postavkama brokera potrebno je prilagoditi broj pokušaja ponovnog slanja i kašnjenje između isporuka kako bi se smanjila vjerojatnost ulaska poruka u DLQ (izračunati optimalne parametre praktički je nemoguće, no možete djelovati empirički i prilagođavati ih tijekom korištenja);
- DLQ spremište treba kontinuirano nadzirati, a sustav praćenja treba obavještavati sistemske administratore kako bi brzo reagirali na neisporučene poruke. To će smanjiti "zonu oštećenja" nastalog kvara ili greške u poslovnoj logici;
- integracijska sabirnica treba biti otporna na privremeno nedostupne aplikacije: pretplate na temu trebaju biti trajne, a naziv domene aplikacije mora biti jedinstven kako bi se spriječilo da netko drugi pokuša obraditi poruke iz reda tijekom nedostupnosti aplikacije.
Osiguranje sigurnosti poslovne logike
Isti primjerak poslovnog procesa može primiti nekoliko poruka i događaja koja će se paralelno obraditi. Istovremeno, za aplikativnog programera sve bi trebalo biti jednostavno i sigurno u smislu više niti.
Poslovna logika procesa obrađuje svaki vanjski događaj koji utječe na taj poslovni proces posebno. Takvi događaji mogu uključivati:
- pokretanje primjerka poslovnog procesa;
- radnju korisnika koja se odnosi na aktivnost unutar poslovnog procesa;
- prijem poruke ili signala na koji je primjerak poslovnog procesa pretplaćen;
- aktiviranje tajmera koji je postavio primjerak poslovnog procesa;
- kontrolni utjecaj putem API-ja (npr. hitno prekidanje procesa).
Svaki takav događaj može promijeniti stanje instance poslovnog procesa: mogu završiti neke aktivnosti i započeti druge, mogu se promijeniti vrijednosti perzistentnih svojstava. Zatvaranje bilo koje aktivnosti može dovesti do aktiviranja jedne ili više sljedećih aktivnosti. One, pak, mogu stati na čekanje drugih događaja ili ako im nisu potrebni nikakvi dodatni podaci, mogu završiti u istoj transakciji. Prije zatvaranja transakcije, novo stanje poslovnog procesa sprema se u Baza podataka, gdje će čekati dolazak sljedećeg vanjskog događaja.
Perzistentni podaci poslovnog procesa, spremljeni u relacijsku Baza podataka, predstavljaju vrlo zgodnu točku sinkronizacije obrade, ako koristimo SELECT FOR UPDATE. Ako je jednoj transakciji uspjelo dobiti stanje poslovnog procesa iz baze za promjenu, tada nijedna druga transakcija istovremeno ne može dobiti to isto stanje za drugu promjenu, a nakon završetka prve transakcije, druga će zajamčeno dobiti već promijenjeno stanje.
Korištenjem pesimističkih blokada na strani SVE, ispunjavamo sve potrebne zahtjeve , a također zadržavamo mogućnost skaliranja aplikacije s poslovnom logikom povećanjem broja pokrenutih instanci.
Međutim, pesimističke blokade nam prijete deadlock-ima, stoga SELECT FOR UPDATE ipak treba ograničiti nekim razumnim vremenskim ograničenjem u slučaju nastanka deadlock-a u nekim očitim slučajevima u poslovnoj logici.
Još jedan problem je sinkronizacija početka poslovnog procesa. Dok nema instance poslovnog procesa, nema ni njegovog stanja u bazi, stoga opisani metod nije prikladan. Ako je potrebno osigurati jedinstvenost instance poslovnog procesa u određenom opsegu, tada će biti potreban neki objekt sinkronizacije, asociiran s klasom procesa i odgovarajućim opsegom. Za rješenje ovog problema koristimo drugi mehanizam blokada, koji omogućava uzimanje blokade proizvoljnog resursa, određenog ključem u formatu URI, putem vanjske usluge.
U našim primjerima poslovni proces InitialPlayer sadrži deklaraciju
uniqueConstraint = UniqueConstraints.singletonStoga, u dnevniku se nalaze poruke o stavljanju i otpuštanju zaključavanja odgovarajućeg ključa. Za druge poslovne procese takvih poruka nema: uniqueConstraint nije definiran.
Problemi poslovnih procesa s trajnim stanjem
Ponekad prisutnost trajnog stanja ne samo da pomaže, već i značajno smeta u razvoju.
Problemi počinju kada je potrebno unijeti promjene u poslovnu logiku i/ili model poslovnog procesa. Nisu sve takve promjene kompatibilne sa starim stanjem poslovnih procesa. Ako u bazi podataka ima mnogo 'živih' instanci, tada unošenje nekompatibilnih promjena može izazvati mnoge neprilike s kojima smo se često susretali pri korištenju jBPM-a.
Ovisno o dubini promjena, može se postupiti na dva načina:
- stvoriti novu vrstu poslovnog procesa, kako ne bi unosili nekompatibilne promjene u stari, i koristiti je umjesto starog prilikom pokretanja novih instanci. Stare instance će nastaviti raditi 'na stari način';
- migrirati trajno stanje poslovnih procesa prilikom ažuriranja poslovne logike.
Prvi put je jednostavniji, ali ima svoja ograničenja i nedostatke, na primjer:
- dupliciranje poslovne logike u mnogim modelima poslovnih procesa, povećanje volumena poslovne logike;
- često je potrebna trenutna migracija na novu poslovnu logiku (u dijelu integracijskih zadataka – gotovo uvijek);
- programer ne zna kada može brisati zastarjele modele.
U praksi koristimo oba pristupa, ali smo donijeli niz odluka kako bismo sebi olakšali život:
- u bazi podataka trajno stanje poslovnog procesa čuva se u lako čitljivom i lako obrađivom formatu: u JSON formatu. To omogućava migracije kako unutar aplikacije, tako i izvan nje. U krajnjem slučaju, može se i ručno prilagoditi (osobito korisno u razvoju tijekom debagiranja);
- integracijska poslovna logika ne koristi nazive poslovnih procesa, kako bi se u bilo kojem trenutku mogla zamijeniti implementacija jednog od uključenih procesa s novom, s novim imenom (na primjer, 'InitialPlayerV2'). Povezivanje se odvija putem naziva poruka i signala;
- Model procesa ima broj verzije koji povećavamo kada uvodimo nekompatibilne promjene u ovu model, a taj broj se čuva zajedno s statusom instancije procesa;
- Persistantno stanje procesa se učitava iz baze prvo u prikladan objektnu model, s kojim može raditi procedura migracije, ako se broj verzije modela promijenio;
- Procedura migracije se smješta blizu poslovne logike i poziva se ‘lenjo’ za svaku instancu poslovnog procesa u trenutku njenog obnavljanja iz baze;
- Ako je potrebno brzo i sinkrono migrirati stanje svih instanci procesa, primjenjuju se klasičnija rješenja za migraciju baze podataka, ali tada se mora raditi s JSON-om.
Da li je još jedan okvir potreban za poslovne procese?
Rješenja opisana u članku omogućila su nam da značajno pojednostavimo život, proširimo krug pitanja koja se rješavaju na razini aplikacijske razvoj i učinimo privlačnijim ideje izdvajanja poslovne logike u mikrousluge. U tu svrhu urađeno je mnogo posla, stvoren je vrlo 'lagan' okvir za poslovne procese, kao i pomoćni komponente za rješavanje navedenih problema u kontekstu širokog spektra aplikacijskih zadataka. Imamo želju podijeliti ove rezultate, iznijeti razvoj zajedničkih komponenti u otvoreni pristup pod slobodnom licencom. To će zahtijevati određene napore i vrijeme. Razumijevanje potražnje za takvim rješenjima moglo bi za nas biti dodatni poticaj. U predloženom članku obraćena je vrlo malo pažnje mogućnostima samog okvira, ali neka od njih su vidljiva iz predstavljenih primjera. Ako ipak objavimo naš okvir, osnovat ćemo zaseban članak za njega. A za sada bismo bili zahvalni ako ostavite mali povratni odgovor, odgovarajući na pitanje:
Samo registrirani korisnici mogu sudjelovati u anketi. , molimo.
Da li je još jedan okvir potreban za poslovne procese?
18,8%Da, već dugo tražimo nešto slično3
12,5%Zanima me malo više o vašoj implementaciji, možda će biti korisno2
6,2%Koristimo jedan od postojećih okvira, ali razmišljamo o promjeni1
18,8%Koristimo jedan od postojećih okvira, sve nam odgovara3
18,8%Snalazimo se bez okvira3
25,0%Pisemo svoj4
Glasalo je 16 korisnika. Suzdržalo se 7 korisnika.
Izvor: habr.com
