
Živjo, Хабр!
Naša podjetje se specializira za razvoj ERP programske rešitve, kjer velika večina predstavlja transakcijske sisteme z obsežno poslovno logiko in dokumentnim poslovanjem, podobnim SED. Sodobne različice naših izdelkov temeljijo na tehnologijah JavaEE, vendar aktivno eksperimentiramo tudi z mikroservisi. Ena najbolj problematičnih točk takšnih rešitev je integracija različnih pod sistemov, povezanih s sorodnimi domenami. Naloge integracije so nam vedno povzročale ogromne preglavice, ne glede na uporabljene arhitekturne sloge, tehnološke kupčke in okvire, vendar smo v zadnjem času opazili napredek pri reševanju teh težav.
V članku, ki ga predstavljamo, bom govoril o izkušnjah in arhitekturnih raziskavah NPO 'Krista' na tem področju. Prav tako bomo obravnavali primer preproste rešitve integracijske naloge z vidika aplikativnega razvijalca in ugotovili, kaj se skriva za to preprostostjo.
Omejitev odgovornosti
Arhitekturne in tehnične rešitve, opisane v članku, so predlagane na podlagi osebnih izkušenj v kontekstu specifičnih nalog. Te rešitve ne trdijo, da so univerzalne in morda niso optimalne v drugih okoliščinah.
Kakšna je povezava z BPM?
Da bi odgovorili na to vprašanje, se moramo malo poglobiti v specifičnosti aplikativnih nalog naših rešitev. Glavni del poslovne logike v našem tipičnem transakcijskem sistemu je vnos podatkov v BDP prek uporabniških vmesnikov, ročna in avtomatizirana preverjanja teh podatkov, obdelava po določenem delovnem toku, objava v drug sistem / analitično bazo / arhiv, oblikovanje poročil. Tako je ključna funkcija sistema za naročnike avtomatizacija njihovih notranjih poslovnih procesov.
Za lažjo komunikacijo uporabljamo termin 'dokuments' kot določeno abstrakcijo niza podatkov, povezanih s skupnim ključem, na katerega lahko 'pripnemo' določen delovni tok.
Ampak kako je z integracijsko logiko? Saj je naloga integracije posledica arhitekture sistema, ki je 'razdeljena' na dele NE na zahtevo naročnika, temveč pod vplivom povsem drugih dejavnikov:
- pod vplivom Conwayevega zakona;
- kot se ponovna uporaba podsistem, ki so bile prej razvite za druge produkte;
- po odločitvi arhitekta, ob upoštevanju nefunkcionalnih zahtev.
Obstaja velika skušnjava, da se integracijska logika loči od poslovne logike osnovnega delovnega toka, da ne bi obremenjevali poslovne logike z integracijskimi artefakti in rešili aplikativnega razvijalca potrebe po razumevanju značilnosti arhitekturnega okolja sistema. Ta pristop ima nekaj prednosti, a praksa kaže na njegovo neučinkovitost:
- reševanje integracijskih nalog običajno pade na najpreprostejše variante v obliki sinhronih klicev zaradi omejenosti točk razširitve v izvedbi osnovnega delovnega toka (o slabostih sinhrone integracije – malo niže);
- integracijski artefakti vseeno pronica v glavno poslovno logiko, ko obstaja potreba po povratnih informacijah iz druge podsistema;
- aplikativni razvijalec ignorira integracijo in jo lahko zlahka pokvari s spremembo delovnega toka;
- sistem ne preneha biti celota z vidika uporabnika, postanejo opazni "šivi" med podsistemi, pojavijo se odvečne uporabniške operacije, ki sprožijo prenos podatkov iz enega podsistema v drugega.
Drugična perspektiva je obravnava integracijskih interakcij kot neodtujljivega dela osnovne poslovne logike in delovnega procesa. Da bi zahteve po usposobljenosti aplikacijskih razvijalcev ne šle v nebo, mora biti ustvarjanje novih integracijskih interakcij enostavno in sproščeno, z minimalnimi možnostmi izbire načina reševanja. To je težje, kot se zdi: orodje mora biti dovolj zmogljivo, da uporabniku zagotovi potrebno število možnosti za uporabo, hkrati pa ne sme dovoliti, da si 'strelja v nogo'. Obstaja veliko vprašanj, na katera mora inženir odgovoriti v kontekstu integracijskih nalog, a o katerih se aplikacijski razvijalec ne sme ukvarjati v svojem vsakodnevnem delu: meje transakcij, doslednost, atomarnost, varnost, skaliranje, porazdelitev obremenitev in virov, usklajevanje, maršalizacija, širjenje in preklapljanje kontekstov itd. Treba je ponuditi aplikacijskim razvijalcem dovolj preproste predloge rešitev, v katerih so že skriti odgovori na vsa takšna vprašanja. Ti predlogi morajo biti dovolj varni: poslovna logika se zelo pogosto spreminja, kar povečuje tveganje za napake, cena napak pa mora ostati na dovolj nizki ravni.
Toda kakšna je povezava z BPM? Obstaja številne možnosti za izvajanje delovnih postopkov...
Res je, v naših rešitvah je zelo priljubljena drugačna realizacija poslovnih procesov – preko deklarativnega določanja diagramov prehodov stanj in povezovanja obdelovalnikov s poslovno logiko na prehode. Pri tem je stanje, ki določa trenutno mesto 'dokumenta' v poslovnem procesu, atribut samega 'dokumenta'.

Tako se proces začne na začetku projekta
Priljubljenost te implementacije izhaja iz sorazmerne enostavnosti in hitrosti ustvarjanja linearnih poslovnih procesov. Vendar pa se s stalnim zapletanjem programski sistemi del avtomatiziranih poslovnih procesov širijo in zapletajo. Pojavi se potreba po dekompoziciji, ponovni uporabi delov procesov in razvejanju procesov, da se lahko vsaka veja izvaja vzporedno. V takih pogojih postane orodje nepraktično, diagram prehodov stanj pa izgubi informativnost (integracijska sodelovanja se sploh ne odražajo na diagramu).

Tako izgleda proces skozi več iteracij razjasnitve zahtev.
Izhod iz te situacije je bila integracija motorja. v nekatere izdelke z najzahtevnejšimi poslovnimi procesi. Na kratki rok je ta rešitev dosegla določen uspeh: pojavila se je možnost izvajanja zapletenih poslovnih procesov ob ohranjanju dovolj informativnega in aktualnega diagrama v notaciji. .

Majhen del zapletenega poslovnega procesa.
Na dolgi rok rešitev ni izpolnila pričakovanj: visoka delovna intenzivnost pri ustvarjanju poslovnih procesov s vizualnimi orodji ni omogočila dosego sprejemljivih produktivnostnih kazalnikov, sam instrument pa je postal eden najmanj priljubljenih med razvijalci. K notranji strukturi motorja so bile tudi pripombe, ki so privedle do nastanka številnih 'popravkov' in 'škripcev'.
Glavna pozitivna plat uporabe jBPM je bila spoznanje koristi in škode, ki izhaja iz obstoja lastnega obstojnega stanja primerka poslovnega procesa. Prav tako smo opazili priložnost uporabe procesnega pristopa za izvajanje kompleksnih protokolov integracije med različnimi aplikacijami z uporabo asinhronih interakcij preko signalov in sporočil. Obstoječnost obstojnega stanja igra pri tem izjemno pomembno vlogo.
Na podlagi navedenega lahko zaključimo: procesni pristop v slogu BPM nam omogoča reševanje širokega spektra nalog avtomatizacije nenehno zapletajočih poslovnih procesov, harmonično vključevanje integracijskih aktivnosti v te procese ter ohranjanje možnosti vizualnega prikaza realiziranega procesa v primerni notaciji.
Pomanjkljivosti sinhronih klicev kot integracijski vzorec
Pod sinhrono integracijo razumemo najpreprostejši blokirajoči klic. Ena podsistem deluje kot strežniška stran in zagotavlja API z ustrezno metodo. Druga podsistem deluje kot odjemalska stran in v pravem trenutku izvede klic in čaka na rezultat. Glede na arhitekturo sistema se lahko odjemalska in strežniška stran nahajata bodisi v isti aplikaciji in procesu bodisi v različnih. V drugem primeru je potrebna neka implementacija RPC in zagotavljanje marshalling parametrov in rezultata klica.

Tak integracijski vzorec ima precejšen nabor pomanjkljivosti, a se kljub temu zelo široko uporablja v praksi zaradi svoje preprostosti. Hitrost implementacije prepriča in ga znova in znova prisili k uporabi v pogojih "gorečih" rokov, zapisovanje rešitve v tehnični dolg. Včasih pa neizkušeni razvijalci uporabljajo brez zavesti, preprosto ne zavedajoč se negativnih posledic.
Poleg najbolj očitnega povečanja povezanosti podsistemov obstajajo tudi manj očitni problemi s "raztrganjem" in "raztezanjem" transakcij. Res je, če poslovna logika vnaša kakršne koli spremembe, potem ne moremo brez transakcij, transakcije pa po drugi strani blokirajo določene vire aplikacije, ki jih te spremembe vplivajo. To pomeni, da dokler ena podsistem ne prejme odgovora od druge, ne more zaključiti transakcije in odstraniti blokade. To bistveno povečuje tveganje za pojav najrazličnejših učinkov:
- se izgubi odzivnost sistema, uporabniki dolgo čakajo na odgovore na poizvedbe;
- strežnik sploh ne odgovarja na poizvedbe uporabnikov zaradi prenatrpanega bazena niti: večina niti je "stagnirala" na blokadi vira, ki ga zaseda transakcija;
- se začnejo pojavljati smrtne blokade: verjetnost njihovega pojava močno odvisna od dolžine transakcij, količine poslovne logike, vključene v transakcijo in blokad;
- se pojavijo napake poteka časa transakcij;
- strežnik "pade" zaradi OutOfMemory, če naloga zahteva obdelavo in spremembo velikih količin podatkov, medtem ko sinhrone integracije močno otežujejo razdelitev obdelave na bolj "lahke" transakcije.
Z arhitekturnega vidika uporaba blokirajočih klicev pri integraciji privede do izgube nadzora nad kakovostjo posameznih pod sistemov: nemogoče je zagotoviti ciljne kazalnike kakovosti enega pod sistema, ne da bi upoštevali kazalnike kakovosti drugega pod sistema. Če pod sistemi razvijajo različne ekipe, to predstavlja velik problem.
Vse postane še bolj zanimivo, če so integrirani pod sistemi v različnih aplikacijah in je treba izvesti sinhrone spremembe z obeh strani. Kako zagotoviti transakčnost teh sprememb?
Če se spremembe izvajajo v ločenih transakcijah, bo potrebno zagotoviti zanesljivo obravnavo izjem in kompenzacij, kar popolnoma izniči glavno prednost sinhronih integracij – enostavnost.
Na misel mi pridejo tudi razdeljene transakcije, a jih ne uporabljamo v svojih rešitvah: težko je zagotoviti zanesljivost.
»Saga« kot rešitev problema transakcij
S povečanjem priljubljenosti mikro storitev postaja vse bolj iskana .
Ta vzorec odlično rešuje zgoraj omenjene težave dolgotrajnih transakcij in širi možnosti upravljanja stanja sistema s strani poslovne logike: kompenzacija po neuspešni transakciji ne more vrniti sistema v prvotno stanje, temveč zagotavlja alternativno pot obravnave podatkov. To tudi omogoča, da se uspešno zaključeni koraki obravnave podatkov ne ponavljajo pri ponovnih poskusih dokončanja procesa do »dobrega« zaključka.
Kar je zanimivo, je, da je ta vzorec v monolitnih sistemih tudi aktualen, ko gre za integracijo slabo povezanih pod sistemov in opazimo negativne učinke, povzročene z dolgimi transakcijami in ustreznimi blokadami virov.
Kar zadeva naše poslovne procese v stilu BPM, se vpeljava »Sag« izkaže za zelo enostavno: posamezni koraki »Sage« so lahko določeni kot aktivnosti znotraj poslovnega procesa, trajno stanje poslovnega procesa pa določa tudi notranje stanje »Sage«. To pomeni, da ne potrebujemo nobenega dodatnega mehanizma usklajevanja. Potreben bo le sporočilni posrednik z podporo garancijam »vsaj enkrat« kot transport.
Toda takšna rešitev ima tudi svojo »ceno«:
- Poslovna logika postaje složenija: potrebno je obraditi kompenzacije;
- Bit će potrebno odustati od pune konzistentnosti, što može biti posebno osjetljivo za monolitne sustave;
- Arhitektura postaje malo složenija, pojavljuje se dodatna potreba za posrednikom porukama;
- Bit će potrebna dodatna sredstva za nadzor i administraciju (iako je to zapravo 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 konzistentnost je žrtvovana još na početku projekta, korist od korištenja ove šablone može značajno nadmašiti nedostatke, posebno uz prisutnost prikladnog API-ja na razini poslovne logike.
Enkapsulacija poslovne logike u mikroservisima
Kada smo počeli eksperimentirati s mikroservisima, postavilo se razmjerno pitanje: gdje smjestiti domensku poslovnu logiku u odnosu na servis koji osigurava postojanost domenskih podataka?
Pogled na arhitekturu raznih BPMS-a može sugerirati razumnu ideju o odjeljivanju poslovne logike od postojanosti: stvoriti sloj platformskih i domenski neovisnih mikroservisa koji oblikuju okruženje i kontejner za izvršavanje domenske poslovne logike, dok bi postojanost domenskih podataka bila organizirana u odvojenom sloju vrlo jednostavnih i laganih mikroservisa. Poslovni procesi u tom slučaju obavljaju orkestraciju servisa sloja postojanosti.

Ovaj pristup ima veliku prednost: može se neograničeno povećavati funkcionalnost platforme, 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 se ona ažurira.
Detaljnija analiza otkrila je značajne nedostatke takvog pristupa:
- Platformski servis koji izvršava poslovnu logiku mnogih domena nosi velike rizike kao jedina tačka kvara. Česte promjene poslovne logike povećavaju rizik od grešaka koje mogu uzrokovati kvarove, a koji se šire na cijeli sustav;
- Problemi s performansama: poslovna logika radi sa svojim podacima kroz uski i spor sučelje:
- podaci će se dodatno obrađivati i prolaziti kroz mrežni sloj;
- domen usluga često će isporučivati više podataka nego što je potrebno poslovnoj logici za obradu, zbog nedostatnih mogućnosti parametrizacije zahteva na nivou spoljne API usluge;
- nekoliko nezavisnih delova poslovne logike može ponovo tražiti iste podatke za obradu (ovaj problem se može ublažiti dodavanjem sesijskih komponenti koje keširaju podatke, ali to dodatno komplikuje arhitekturu i stvara probleme s tačnošću podataka i invalidacijom keša);
- problemi transakcionalnosti:
- poslovni procesi sa postojanim stanjem, čije čuvanje obezbeđuje platformska usluga, neće se slagati sa domenskim podacima, a jednostavna rešenja za ovaj problem nisu na vidiku;
- iznošenje blokade domena podataka van transakcije: ako domeni poslovnoj logici treba da izvrši promene, prethodno proverivši tačnost aktuelnih podataka, potrebno je isključiti mogućnost konkurentne promene obrađenih podataka. Spoljna blokada podataka može pomoći u rešavanju problema, ali to rešenje nosi dodatne rizike i smanjuje ukupnu pouzdanost sistema;
- dodatne poteškoće prilikom ažuriranja: u nekim slučajevima je potrebno da se usluga postojanosti i poslovna logika ažuriraju sinhrono ili strogo redosledom.
Na kraju se moralo vratiti korenima: inkapsulirati domen podataka i domen poslovne logike u jedan mikroservis. Ovaj pristup pojednostavljuje doživljaj mikroservisa kao celokupnog komponente u okviru sistema i ne stvara prethodno navedene probleme. I to ima svoju cenu:
- neophodna je standardizacija API-a za interakciju s poslovnom logikom (posebno za obezbeđivanje korisničkih aktivnosti u okviru poslovnih procesa) i API-a platformskih usluga; potrebna je pažljivija pažnja na promene API-ja, direktnu i obratnu kompatibilnost;
- neophodno je dodati dodatne runtime biblioteke za obezbeđivanje funkcionisanja poslovne logike u okviru svakog od tih mikroservisa, a to postavlja nove zahteve tim bibliotekama: lakoća i minimum tranzitivnih zavisnosti;
- Razvijalci poslovne logike moraju spremljati verzije biblioteka: ako neki mikroservis dugo nije ažuriran, velika je verovatnoća da će sadržati zastarjelu verziju biblioteka. To može postati neočekivana prepreka prilikom dodavanja nove funkcionalnosti i može zahtevati migraciju stare poslovne logike takvog servisa na nove verzije biblioteka, ako su postojale nekompatibilne promene između verzija.

Sloj platformskih servisa u takvoj arhitekturi takođe je prisutan, ali ovaj sloj ne formira kontejner za izvršenje 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 mikros servisa, već i za centralizaciju upravljanja.
Na primer, korisničke aktivnosti u poslovnim procesima generišu zadatke. Međutim, radeći sa zadacima, korisnik treba da vidi zadatke iz svih domena u zajedničkoj listi, što znači da treba postojati odgovarajući platformski servis za registraciju zadataka, očišćen od domena poslovne logike. Održavati inkapsulaciju poslovne logike u takvom kontekstu je prilično problematično, i to je još jedan kompromis ove arhitekture.
Integracija poslovnih procesa iz perspektive aplikativnog programera
Kao što je već rečeno, aplikativni programer mora biti apstrahiran od tehničkih i inženjerskih karakteristika implementacije interakcije više aplikacija, kako bi se moglo računati na dobru produktivnost razvoja.
Pokušaćemo da rešimo prilično komplikovanu integracionu zadatak, posebno osmišljenu za ovaj članak. Ovo će biti „igrana“ situacija sa učešćem tri aplikacije, gde svaka od njih definiše određeno domena ime: „app1“, „app2“, „app3“.
Unutar svake aplikacije pokreću se poslovni procesi koji počinju da „igre loptom“ preko integracione bus. U ovoj igri, lopta će biti poruke sa imenom „Ball“.
Pravila igre:
- prvi igrač – inicijator. On poziva druge igrače u igru, započinje igru i može je završiti u bilo trenutku;
- ostali igrači prijavljuju svoje učešće u igri, „upoznaju“ jedni druge kao i prvog igrača;
- prihvatajući loptu, igrač bira drugog učesnika i predaje mu loptu. Broji se ukupni broj dodavanja;
- Vsak igralec ima "energijo", ki se zmanjšuje z vsakim podajanjem žoge. Ko energija poči, igralec napove svoj odhod iz igre;
- če ostane sam, takoj napove svoj odhod;
- ko vsi igralci izpadejo, prvi igralec napove zaključek igre. Če je on izpadel prej, mora še vedno spremljati igro, da jo zaključi.
Za reševanje te naloge bom uporabil naš DSL za poslovne procese, ki omogoča kompaktno opisovanje logike v Kotlinu z minimalno količino 'boilerplate' kode.
V aplikaciji app1 bo deloval poslovni proces prvega igralca (ki je tudi iniciator 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()
// Ta klasa predstavljena procesa: enkapsulira njeno unutrašnje 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 treba da bude jedini
uniqueConstraint = UniqueConstraints.singleton
// Objavljujemo aktivnosti, od kojih se sastoji 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činjemo?" }
}
}
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} izlazi ...")
}
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 iz objavljenih 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 obradnike na aktivnostima za logovanje
sendNewGameSignal.onExit { println("Hajde da igramo!") }
sendStopGameSignal.onExit { println("Stani!") }
sendPlayerOut.onExit { println("$playerName: Izlazim!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Korak ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Poleg izvajanja poslovne logike, predstavljena koda omogoča izhod objektnega modela poslovnega procesa, ki ga je mogoče vizualizirati v obliki diagrama. Vizualizator še ni bil implementiran, zato sem moral porabiti nekaj časa za risanje (tukaj sem malo poenostavil BPMN notacijo glede na uporabo vrat, da bi izboljšal skladnost diagrama s predstavljeno kodo):

Aplikacija app2 bo vključila poslovni proces drugega igralca:
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}")
}Diagram:

V aplikaciji app3 bomo igralca nekoliko spremenili: namesto naključnega izbora naslednjega igralca bo deloval po 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}")
}V ostalem obnašanju igralca ni razlike v primerjavi s prejšnjim, zato se diagram ne spreminja.
Zdaj potrebujem test, da vse to zaženem. Prikazujem le kodo samega testa, da ne obremenjujem članka s šabloni (v resnici sem uporabil testno okolje, ustvarjeno prej za testiranje integracije drugih poslovnih procesov):
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");
// Zdaj moramo malce počakati, da se igralci "spoznajo" med seboj.
// Čakati prek sleep - slaba rešitev, a najbolj preprosta.
// Ne delajte tako v resnih testih!
Thread.sleep(1000);
// Začnemo igro, zapiramo uporabniško 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;
}Zaganjamo test, gledamo log:
izhod konzole
Zablokiran ključ lock://app1/process/InitialPlayer
Igrajmo!
Otključan ključ lock://app1/process/InitialPlayer
Igrač2: Tu sam!
Igrač3: Tu sam!
Igrač4: Tu sam!
Igrač5: Tu sam!
... pridruži se igraču Igrač2 ...
... pridruži se igraču Igrač4 ...
... pridruži se igraču Igrač3 ...
... pridruži 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: Napuštam igru!
Korak 89: Igrač3 >>> Igrač4
... igrač Igrač2 je napustio igru ...
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: Napuštam igru!
... igrač Igrač4 je napustio igru ...
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: Napuštam igru!
... igrač Igrač5 je napustio igru ...
Korak 122: Igrač3 >>> Igrač5
Korak 123: Igrač5 >>> Igrač1
Igrač5: Napuštam igru!
Korak 124: Igrač1 >>> Igrač3
... igrač Igrač5 je napustio igru ...
Korak 125: Igrač3 >>> Igrač1
Korak 126: Igrač1 >>> Igrač3
Igrač1: Napuštam igru!
... igrač Igrač1 je napustio igru ...
Korak 127: Igrač3 >>> Igrač3
Igrač3: Napuštam igru!
Korak 128: Igrač3 >>> Igrač3
... igrač Igrač3 je napustio igru ...
Igrač3: Napuštam igru!
Stani!
Korak 129: Igrač3 >>> Igrač3
Igrač3: Napuštam igru!Iz vsega tega lahko naredimo nekaj pomembnih zaključkov:
- ob prisotnosti potrebnih orodij lahko razvijalci aplikacij ustvarjajo integracijske interakcije med aplikacijami, ne da bi se oddaljili od poslovne logike;
- zapletenost (complexity) integracijske naloge, ki zahteva inženirske kompetence, je mogoče skriti znotraj ogrodja, če je to sprva zasnovano v arhitekturi ogrodja. Težave (difficulty) naloge ni mogoče skriti, zato bo reševanje težke naloge v kodi ustrezno izgledalo;
- pri razvoju integracijske logike je nujno upoštevati končno doslednost in odsotnost linearizabilnosti spremembe stanja vseh udeležencev integracije. To sili zapletati logiko, da postane neobčutljiva na vrstni red zunanjih dogodkov. V našem primeru je igralec prisiljen sodelovati v igri šele potem, ko se izjavlja o izstopu iz igre: drugi igralci mu bodo še naprej dodajali žogo, dokler informacija o njegovem izstopu ne prispe in ne obdelajo vsi udeleženci. Ta logika ne izhaja iz pravil igre in predstavlja kompromisno rešitev v okviru izbrane arhitekture.
Nato se bomo pogovorili o različnih odtenkih naše rešitve, kompromisih in drugih vidikih.
Vse sporočila - v eni vrsti
Vse integrirane aplikacije delujejo z eno integracijsko vodjo, ki je predstavljena kot zunanji posrednik, ena vrsta BPMQueue – za sporočila in ena tema BPMTopic – za signale (dogodke). Prepuščanje vseh sporočil skozi eno vrsto je samo po sebi kompromis. Na ravni poslovne logike je zdaj mogoče uvajati neomejeno število novih tipov sporočil, ne da bi bilo treba spreminjati strukturo sistema. To je pomembno poenostavitev, vendar prinaša določena tveganja, ki so se v kontekstu naših tipičnih nalog zdela precej manj pomembna.

Vendar pa obstaja ena podrobnost: vsaka aplikacija filtrira "svoja" sporočila iz vrste že ob vstopu, po imenu svoje domene. Domeno je mogoče navesti tudi v signalih, če je treba omejiti "vidnost" signala na eno samo aplikacijo. To bi moralo povečati prepustnost vodila, vendar mora zdaj poslovna logika delovati z imeni domen: za naslovitev sporočil – obvezno, za signale – zaželeno.
Zagotavljanje zanesljivosti integracijske vodila
Zanesljivost izhaja iz več dejavnikov:
- izbrani posrednik sporočil – kritično pomembna komponenta arhitekture in ena točka odpovedi: mora biti dovolj odporno zanesljiv. Uporabljati je treba le preverjene izvedbe s kakovostno podporo in velikim skupnostim;
- potrebno je zagotoviti visoko razpoložljivost posrednika sporočil, za kar mora biti fizično ločen od integriranih aplikacij (zagotoviti visoko razpoložljivost aplikacij s poslovno logiko je bistveno težje in dražje);
- posrednik mora zagotavljati "vsaj enkrat" garancije dostave. To je obvezen pogoj za zanesljivo delovanje integracijske vodila. Garancij na ravni "natančno enkrat" ni potrebno: poslovni procesi običajno niso občutljivi na ponavljajoče se dostave sporočil ali dogodkov, v posebnih nalogah, kjer je to pomembno, pa je lažje dodati dodatno preverjanje v poslovno logiko, kot pa nenehno uporabljati dovolj "drage" garancije;
- pošiljanje sporočil in signalov je treba vključiti v skupno transakcijo s spremembo stanja poslovnih procesov in domen. Prednostna možnost bo uporaba vzorca , vendar bo to zahtevalo dodatno tabelo v bazi in posrednika. V aplikacijah JEE je mogoče to vprašanje poenostaviti z uporabo lokalnega upravitelja JTA, vendar mora povezava z izbranim posrednikom znati delovati v načinu ;
- obdelovalci prihajajočih sporočil in dogodkov morajo prav tako delovati z transakcijo spremembe stanja poslovnega procesa: če se taka transakcija razveljavi, je treba tudi sprejem sporočila preklicati;
- sporočila, ki jih ni bilo mogoče dostaviti zaradi napak, je treba shraniti v ločeno skladišče (Dead Letter Queue). Z tohoto důvodu jsme vytvořili samostatnou platformní mikroslužbu, která uchovává takové zprávy ve svém úložišti, indexuje je podle atributů (pro rychlou skupinovou práci a vyhledávání) a vystavuje API pro zobrazení, opětovné odesílání na cílovou adresu a odstraňování zpráv. Správci systému mohou s touto službou pracovat prostřednictvím svého webového rozhraní;
- v nastavení brokeru je třeba nastavit počet pokusů o doručení a zpoždění mezi doručeními, aby se snížila pravděpodobnost, že zprávy se dostanou do DLQ (vypočítat optimální parametry je téměř nemožné, ale je možné postupovat empiricky a upravovat je během provozu);
- úložiště DLQ by mělo být neustále monitorováno a monitorovací systém by měl informovat administrátory systému, aby na nedoručené zprávy co nejrychleji reagovali. To pomůže snížit „zónu porážky“ vzniklého selhání nebo chyby obchodní logiky;
- integrační sběrnice by měla být necitlivá na dočasnou absenci aplikací: předplatné na téma by mělo být trvalé a doménové jméno aplikace by mělo být jedinečné, aby během doby nepřítomnosti aplikace nikdo jiný nesnažil zpracovat její zprávy z fronty.
Zajištění vlákno-bezpečnosti obchodní logiky
Jednomu a tomu samému příkladu obchodního procesu může přijít více zpráv a událostí najednou, jejichž zpracování se spustí paralelně. Zároveň by pro aplikačního developera mělo být vše jednoduché a vlákno-bezpečné.
Obchodní logika procesu zpracovává každou externí událost, která na tento obchodní proces působí, zvlášť. Takovými událostmi mohou být:
- spuštění příkladu obchodního procesu;
- akce uživatele související s aktivitami uvnitř obchodního procesu;
- příchod zprávy nebo signálu, na který je přihlášen příklad obchodního procesu;
- vypršení časovače, který nastavila instance obchodního procesu;
- řízení prostřednictvím API (například nouzové přerušení procesu).
Vsako takšno dogajanje lahko spremeni stanje primerka poslovnega procesa: lahko se končajo ene dejavnosti in začnejo druge, lahko se spremenijo vrednosti trajnih lastnosti. Zaprtje katere koli dejavnosti lahko povzroči aktivacijo ene ali več naslednjih dejavnosti. Te pa lahko ustavijo in čakajo na druge dogodke ali pa, če ne potrebujejo nobenih dodatnih podatkov, končajo v istem transakciji. Pred zaprtjem transakcije se novo stanje poslovnega procesa shrani v BDP, kjer čaka na naslednji zunanji dogodek.
Trajni podatki poslovnega procesa, shranjeni v relacijski BDP, so zelo priročna točka sinhronizacije obdelave, če uporabimo SELECT FOR UPDATE. Če je eni transakciji uspelo pridobiti stanje poslovnega procesa iz baze za njegovo spremembo, nobena druga transakcija hkrati ne more pridobiti istega stanja za drugo spremembo, po zaključku prve transakcije pa bo druga zagotovo pridobila že spremenjeno stanje.
Z uporabo pesimističnih blokad na strani SODB izpolnjujemo vse potrebne zahteve , ter hkrati ohranjamo možnost skaliranja aplikacije s poslovno logiko s povečanjem števila zagnanih primerkov.
Vendar pa pesimistične blokade grozijo z mrtvimi zaklenitvami, zato je smiselno omejiti SELECT FOR UPDATE na nek razumljiv časovni odmik v primeru nastopa mrtvih zaklenitev v kakšnih očitnih primerih v poslovni logiki.
Še en problem je sinhronizacija začetka poslovnega procesa. Dokler ni primerka poslovnega procesa, ni tudi njegovega stanja v bazi, zato opisana metoda ne ustreza. Če je potrebno zagotoviti edinstvenost primerka poslovnega procesa v določenem obsegu, bo potreben nek objekt za sinhronizacijo, povezan s procesno klasifikacijo in ustreznim obsegom. Za rešitev tega problema uporabljamo mehanizem zaklepanja, ki omogoča pridobitev zaklepanja naključnega vira, določenega s ključem v formatu URI, preko zunanje storitve.
V naših primerih poslovni proces InitialPlayer vsebuje objavo
uniqueConstraint = UniqueConstraints.singletonZato se v dnevniku pojavljajo sporočila o pridobitvi in sprostitvi zaklepa ustrezne ključne vrednosti. Pri drugih poslovnih procesih takšnih sporočil ni: uniqueConstraint ni določen.
Težave poslovnih procesov s trajnim stanjem
Včasih prisotnost trajnega stanja ne le olajša, temveč tudi močno ovira razvoj.
Težave se začnejo, ko je treba spremeniti poslovno logiko in/ali model poslovnega procesa. Ne vsaka takšna sprememba se izkaže za združljivo s starim stanjem poslovnih procesov. Če je v podatkovni bazi veliko 'živih' primerkov, lahko nezdružljive spremembe prinesejo številne nevšečnosti, s katerimi smo se pogosto srečevali pri uporabi jBPM.
Glede na globino sprememb se lahko odločimo za dve poti:
- ustvariti nov tip poslovnega procesa, da ne bi uvajali nezdružljivih sprememb v stari, in ga uporabiti namesto starega pri zagonu novih primerkov. Stari primerki bodo še naprej delovali 'na star način';
- migrirati trajno stanje poslovnih procesov pri posodobitvi poslovne logike.
Prva pot je preprostejša, ima pa svoje omejitve in pomanjkljivosti, na primer:
- dupliranje poslovne logike v več modelih poslovnih procesov, povečanje obsega poslovne logike;
- pogosto je potreben takojšen prehod na novo poslovno logiko (v delu integracijskih nalog – skoraj vedno);
- razvijalec ne ve, kdaj lahko odstrani zastarele modele.
V praksi uporabljamo oba pristopa, hkrati pa smo sprejeli vrsto odločitev, da si poenostavimo življenje:
- v podatkovni bazi se trajno stanje poslovnega procesa hrani v lahko berljivi in enostavno obdelovni obliki: v vrstici v formatu JSON. To omogoča migracije tako znotraj aplikacije kot zunaj nje. V skrajnem primeru lahko tudi ročno prilagodimo (kar je še posebej koristno pri razvoju med odpravljanjem napak);
- integracijska poslovna logika ne uporablja imen poslovnih procesov, da bi lahko kadarkoli zamenjali implementacijo enega od sodelujočih procesov z novo, z novim imenom (na primer 'InitialPlayerV2'). Povezovanje poteka preko imen sporočil in signalov;
- Model procesa ima verziju broja koju povećavamo kada uvedemo nekompatibilne izmene u ovu model, a taj broj se čuva zajedno sa stanjem instance procesa;
- Persistantno stanje procesa se učitava iz baze u pogodnu objekt model, sa kojom može raditi procedura migracije, ako se broj verzije modela promenio;
- Procedura migracije se postavlja pored poslovne logike i poziva se 'lenjo' za svaku instancu poslovnog procesa u trenutku njenog obnavljanja iz baze;
- Ako je potrebno brzo i sinhrono migrirati stanje svih instanci procesa, primenjuju se klasičnija rešenja za migraciju Baze podataka, ali tu se mora raditi sa JSON-om.
Da li je potreban još jedan okvir za poslovne procese?
Rešenja opisana u članku omogućila su nam da značajno olakšamo sebi život, proširimo krug pitanja koja se rešavaju na nivou aplikacione razvoj, učinimo ideje o izdavanju poslovne logike u mikroservise privlačnijim. Za to je urađeno mnogo posla, kreiran je veoma 'lagan' okvir za poslovne procese, kao i pomoćne komponente za rešavanje navedenih problema u kontekstu širokog spektra aplikacionih zadataka. Imamo želju da podelimo ove rezultate, da razvijanje zajedničkih komponenti učinimo dostupnim javnosti pod slobodnom licencom. To će zahtevati određene napore i vreme. Razumevanje potražnje za takvim rešenjima moglo bi biti dodatni podsticaj za nas. U predloženom članku je vrlo malo pažnje posvećeno mogućnostima samog okvira, ali su neke od njih vidljive iz predstavljenih primera. Ako ipak objavimo svoj okvir, biće mu posvećen poseban članak. A za sada ćemo biti zahvalni ako ostavite mali povratni komentar, odgovarajući na pitanje:
Samo registrovani korisnici mogu učestvovati u anketi. , molim vas.
Da li je potreban još jedan okvir za poslovne procese?
18,8%Da, već dugo tražimo nešto slično3
12,5%Zanima me da saznam više o vašoj realizaciji, možda može biti korisno2
6,2%Koristimo jedan od postojećih okvira, ali razmišljamo o promeni1
18,8%Koristimo jedan od postojećih okvira, sve nam odgovara3
18,8%Snalazimo se bez okvira3
25,0%Pišemo svoj4
Glasalo je 16 korisnika. Uzdržano 7 korisnika.
Izvor: habr.com
