Integracija BPM stila

Integracija BPM stila

Pozdrav, Habr!

Naša tvrtka specijalizirana je za razvoj softverskih rješenja ERP klase, čiji lavovski udio zauzimaju transakcijski sustavi s velikom količinom poslovne logike i protoka dokumenata a la EDMS. Trenutne 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 podsustava koji pripadaju susjednim domenama. Problemi s integracijom uvijek su nam zadavali veliku glavobolju, bez obzira na arhitektonske stilove, tehnološke skupove i okvire koje koristimo, no nedavno je došlo do napretka u rješavanju takvih problema.

U članku koji vam predstavljam govorit ću o iskustvima i arhitektonskim istraživanjima koje NPO Krista ima na označenom području. Također ćemo pogledati primjer jednostavnog rješenja problema integracije sa stajališta programera aplikacije 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 tvrde da su univerzalna i možda neće biti optimalna u drugim uvjetima uporabe.

Kakve veze BPM ima s tim?

Da bismo odgovorili na ovo pitanje, moramo malo dublje zaroniti u specifičnosti primijenjenih problema 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, provođenje kroz neki tijek rada, objavljivanje u drugom sustavu / analitičkoj bazi podataka / arhivi, generiranje izvješća . Stoga je ključna funkcija sustava za korisnike automatizacija njihovih internih poslovnih procesa.

Radi lakšeg snalaženja, u komunikaciji koristimo pojam "dokument" kao neku apstrakciju skupa podataka objedinjenih zajedničkim ključem s kojim se određeni tijek rada može "povezati".
Ali što je s logikom integracije? Uostalom, integracijski zadatak generira arhitektura sustava, koja je "rezana" na dijelove NE na zahtjev kupca, već pod utjecajem potpuno različitih čimbenika:

  • podliježe Conwayevom zakonu;
  • kao rezultat ponovne upotrebe podsustava prethodno razvijenih za druge proizvode;
  • prema procjeni arhitekta, na temelju nefunkcionalnih zahtjeva.

Postoji veliko iskušenje odvojiti integracijsku logiku od poslovne logike glavnog tijeka rada, kako se poslovna logika ne bi zagadila integracijskim artefaktima i oslobodila razvojnog programera aplikacije od potrebe da ulazi u specifičnosti arhitektonskog krajolika sustava. Ovaj pristup ima niz prednosti, ali praksa pokazuje njegovu neučinkovitost:

  • rješavanje problema integracije obično se vraća na najjednostavnije opcije u obliku sinkronih poziva zbog ograničenih točaka proširenja u implementaciji glavnog tijeka rada (nedostaci sinkrone integracije razmatraju se u nastavku);
  • integracijski artefakti i dalje prodiru u središnju poslovnu logiku kada je potrebna povratna informacija iz drugog podsustava;
  • programer aplikacije zanemaruje integraciju i može je lako prekinuti promjenom tijeka rada;
  • sustav prestaje biti jedinstvena cjelina s korisnikove točke gledišta, "šavovi" između podsustava postaju vidljivi i pojavljuju se suvišne korisničke operacije koje pokreću prijenos podataka iz jednog podsustava u drugi.

Drugi pristup je razmatranje integracijskih interakcija kao sastavnog dijela osnovne poslovne logike i tijeka rada. Kako biste spriječili vrtoglavi porast kvalifikacija programera aplikacija, stvaranje novih integracijskih interakcija trebalo bi biti jednostavno i bez napora, s minimalnom mogućnošću odabira rješenja. To je teže učiniti nego što se čini: alat mora biti dovoljno moćan da korisniku pruži potrebnu raznolikost mogućnosti za njegovo korištenje, a da mu pritom ne dopusti da si “puca u nogu”. Postoje mnoga pitanja na koja inženjer mora odgovoriti u kontekstu integracijskih zadataka, ali o kojima programer aplikacija ne bi trebao razmišljati u svom svakodnevnom radu: granice transakcija, dosljednost, atomičnost, sigurnost, skaliranje, distribucija opterećenja i resursa, usmjeravanje, marshaling, distribucija i prebacivanje konteksta itd. Potrebno je programerima aplikacija ponuditi prilično jednostavne predloške rješenja u kojima su već skriveni odgovori na sva takva pitanja. Ovi predlošci moraju biti prilično sigurni: poslovna logika se vrlo često mijenja, što povećava rizik od unošenja pogrešaka, cijena pogrešaka mora ostati na prilično niskoj razini.

Ali kakve veze BPM ima s tim? Postoje mnoge mogućnosti za implementaciju tijeka rada...
Dapače, još jedna implementacija poslovnih procesa vrlo je popularna u našim rješenjima - kroz deklarativno definiranje dijagrama prijelaza stanja i povezivanje rukovatelja s poslovnom logikom za prijelaze. U ovom slučaju, stanje koje određuje trenutnu poziciju “dokumenta” u poslovnom procesu je atribut samog “dokumenta”.

Integracija BPM stila
Ovako izgleda proces na početku projekta

Popularnost ove implementacije je zbog relativne jednostavnosti i brzine kreiranja linearnih poslovnih procesa. Međutim, kako softverski sustavi neprestano postaju sve složeniji, automatizirani dio poslovnog procesa raste i postaje sve složeniji. Postoji potreba za dekompozicijom, ponovnom upotrebom dijelova procesa, kao i grananjem procesa tako da se svako grananje izvodi paralelno. U takvim uvjetima alat postaje nezgodan, a dijagram prijelaza stanja gubi svoj informacijski sadržaj (integracijske interakcije se uopće ne odražavaju na dijagramu).

Integracija BPM stila
Ovako izgleda proces nakon nekoliko iteracija pojašnjenja zahtjeva.

Izlaz iz ove situacije bila je integracija motora jBPM u neke proizvode s najsloženijim poslovnim procesima. Kratkoročno, ovo je rješenje imalo uspjeha: postalo je moguće implementirati složene poslovne procese uz zadržavanje prilično informativnog i relevantnog dijagrama u notaciji BPMN2.

Integracija BPM stila
Mali dio složenog poslovnog procesa

Dugoročno, rješenje nije opravdalo očekivanja: veliki intenzitet rada kreiranja poslovnih procesa putem vizualnih alata nije omogućio postizanje prihvatljivih pokazatelja produktivnosti, a sam alat postao je jedan od najomiljenijih među programerima. Također je bilo pritužbi na unutarnju strukturu motora, što je dovelo do pojave mnogih "zakrpa" i "štaka".

Glavni pozitivni aspekt korištenja jBPM-a bila je svijest o prednostima i štetnostima postojanog stanja instance poslovnog procesa. Također smo vidjeli mogućnost korištenja procesnog pristupa za implementaciju složenih integracijskih protokola između različitih aplikacija korištenjem asinkronih interakcija putem signala i poruka. Prisutnost postojanog stanja igra ključnu ulogu u tome.

Na temelju navedenog možemo zaključiti: Procesni pristup u BPM stilu omogućuje nam rješavanje širokog spektra zadataka za automatizaciju sve složenijih poslovnih procesa, skladno uklapanje integracijskih aktivnosti u te procese i zadržavanje sposobnosti vizualnog prikaza implementiranog procesa u prikladnoj notaciji.

Nedostaci sinkronih poziva kao obrasca integracije

Sinkrona integracija odnosi se na najjednostavniji blokirajući poziv. Jedan podsustav djeluje kao strana poslužitelja i izlaže API potrebnom metodom. Drugi podsustav djeluje kao strana klijenta i u pravo vrijeme upućuje poziv i čeka rezultat. Ovisno o arhitekturi sustava, klijentska i poslužiteljska strana mogu se nalaziti u istoj aplikaciji i procesu ili u različitim. U drugom slučaju, morate primijeniti neku RPC implementaciju i omogućiti marshalling parametara i rezultata poziva.

Integracija BPM stila

Ovaj integracijski obrazac ima prilično velik skup nedostataka, ali je vrlo široko korišten u praksi zbog svoje jednostavnosti. Brzina implementacije osvaja i tjera vas da je uvijek iznova koristite u uvjetima kratkih rokova, bilježeći rješenje kao tehnički dug. Ali također se događa da ga neiskusni programeri koriste nesvjesno, jednostavno ne shvaćajući negativne posljedice.

Osim najočiglednijeg povećanja povezivosti podsustava, postoje i manje očiti problemi s "rastućim" i "rastegnutim" transakcijama. Doista, ako poslovna logika napravi neke promjene, tada se transakcije ne mogu izbjeći, a transakcije zauzvrat blokiraju određene resurse aplikacije na koje te promjene utječu. To jest, dok jedan podsustav ne čeka odgovor od drugog, neće moći dovršiti transakciju i ukloniti zaključavanja. To značajno povećava rizik od raznih učinaka:

  • Gubi se responzivnost sustava, korisnici dugo čekaju odgovore na zahtjeve;
  • poslužitelj općenito prestaje odgovarati na zahtjeve korisnika zbog prenatrpanog skupa niti: većina niti je zaključana na resursu koji je zauzet transakcijom;
  • Počinju se pojavljivati ​​zastoji: vjerojatnost njihovog pojavljivanja jako ovisi o trajanju transakcija, količini poslovne logike i zaključavanja uključenih u transakciju;
  • pojavljuju se pogreške isteka transakcije;
  • poslužitelj "zakaže" s OutOfMemory ako zadatak zahtijeva obradu i promjenu velikih količina podataka, a prisutnost sinkronih integracija čini vrlo teškim dijeljenje obrade na "lakše" transakcije.

S arhitektonskog gledišta, korištenje blokirajućih poziva tijekom integracije dovodi do gubitka kontrole nad kvalitetom pojedinih podsustava: nemoguće je osigurati ciljne pokazatelje kvalitete jednog podsustava izolirano od pokazatelja kvalitete drugog podsustava. Ako podsustave razvijaju različiti timovi, to je veliki problem.

Stvari postaju još zanimljivije ako su podsustavi koji se integriraju u različitim aplikacijama i morate izvršiti sinkrone promjene na obje strane. Kako osigurati transakcijsku sposobnost ovih promjena?

Ako se promjene izvrše u odvojenim transakcijama, tada ćete morati osigurati pouzdano rukovanje iznimkama i kompenzaciju, a to u potpunosti eliminira glavnu prednost sinkronih integracija - jednostavnost.

Distribuirane transakcije također padaju na pamet, ali ih ne koristimo u našim rješenjima: teško je osigurati pouzdanost.

"Saga" kao rješenje problema transakcije

S rastućom popularnošću mikrousluga, potražnja za Saga uzorak.

Ovaj obrazac savršeno rješava gore navedene probleme dugih transakcija, a također proširuje mogućnosti upravljanja stanjem sustava sa strane poslovne logike: naknada nakon neuspjele transakcije možda neće vratiti sustav u prvobitno stanje, ali pružiti alternativni način obrade podataka. To vam također omogućuje da izbjegnete ponavljanje uspješno dovršenih koraka obrade podataka kada pokušavate dovesti proces do "dobrog" kraja.

Zanimljivo je da je u monolitnim sustavima ovaj obrazac također relevantan kada se radi o integraciji labavo povezanih podsustava i uočavaju se negativni učinci uzrokovani dugotrajnim transakcijama i odgovarajućim zaključavanjima resursa.

U odnosu na naše poslovne procese u BPM stilu, pokazalo se da je vrlo jednostavno implementirati “Sage”: pojedini koraci “Sage” mogu se specificirati kao aktivnosti unutar poslovnog procesa, a perzistentno stanje poslovnog procesa također određuje unutarnje stanje “Sage”. Odnosno, ne trebamo nikakav dodatni mehanizam koordinacije. Sve što trebate je posrednik za poruke koji podržava "barem jednom" jamstva kao prijenos.

Ali i ovo rješenje ima svoju “cijenu”:

  • poslovna logika postaje složenija: potrebno je razraditi kompenzaciju;
  • bit će potrebno napustiti punu dosljednost, što može biti posebno osjetljivo za monolitne sustave;
  • Arhitektura postaje malo kompliciranija i pojavljuje se dodatna potreba za brokerom poruka;
  • bit će potrebni dodatni alati za praćenje i administraciju (iako je to općenito dobro: kvaliteta usluge sustava će se povećati).

Za monolitne sustave, opravdanje za korištenje "Sag" nije toliko očito. Za mikroservise i druge SOA-e, gdje najvjerojatnije već postoji posrednik, a puna dosljednost žrtvuje se na početku projekta, prednosti korištenja ovog obrasca mogu značajno nadmašiti nedostatke, posebno ako postoji prikladan API u poslovnoj logici razini.

Enkapsulacija poslovne logike u mikroservisima

Kada smo počeli eksperimentirati s mikroservisima, postavilo se razumno pitanje: gdje smjestiti poslovnu logiku domene u odnosu na uslugu koja osigurava postojanost podataka domene?

Gledajući arhitekturu različitih BPMS-ova, može se činiti razumnim odvojiti poslovnu logiku od postojanosti: stvoriti sloj mikroservisa neovisnih o platformi i domeni koji čine okruženje i spremnik za izvođenje poslovne logike domene i dizajnirati postojanost podataka domene kao zaseban sloj vrlo jednostavnih i laganih mikroservisa. Poslovni procesi u ovom slučaju provode orkestraciju usluga perzistentnog sloja.

Integracija BPM stila

Ovaj pristup ima vrlo veliku prednost: možete povećavati funkcionalnost platforme koliko god želite, a samo će odgovarajući sloj mikroservisa platforme postati “mastan” od toga. Poslovni procesi iz bilo koje domene odmah su u mogućnosti koristiti novu funkcionalnost platforme čim se ona ažurira.

Detaljnije istraživanje otkrilo je značajne nedostatke ovog pristupa:

  • usluga platforme koja izvršava poslovnu logiku mnogih domena odjednom nosi velike rizike kao jedna točka kvara. Česte promjene poslovne logike povećavaju rizik od pogrešaka koje dovode do kvarova u cijelom sustavu;
  • problemi s izvedbom: poslovna logika radi sa svojim podacima kroz usko i sporo sučelje:
    • podaci će se još jednom razvrstati i pumpati kroz mrežni stog;
    • usluga domene će često pružiti više podataka nego što je potrebno za obradu poslovne logike zbog nedovoljnih mogućnosti za parametrizaciju zahtjeva na razini vanjskog API-ja usluge;
    • nekoliko neovisnih dijelova poslovne logike može opetovano ponovno zahtijevati iste podatke za obradu (ovaj problem se može ublažiti dodavanjem komponenti sesije koje spremaju podatke u predmemoriju, ali to dodatno komplicira arhitekturu i stvara probleme relevantnosti podataka i poništavanja predmemorije);
  • problemi s transakcijom:
    • poslovni procesi s postojanim stanjem, koje pohranjuje usluga platforme, nisu u skladu s podacima domene i ne postoje jednostavni načini za rješavanje ovog problema;
    • postavljanje blokade podataka domene izvan transakcije: ako poslovna logika domene treba izvršiti promjene nakon prethodne provjere ispravnosti trenutnih podataka, potrebno je isključiti mogućnost konkurentske promjene obrađenih podataka. Vanjsko blokiranje podataka može pomoći u rješavanju problema, ali takvo rješenje nosi dodatne rizike i smanjuje ukupnu pouzdanost sustava;
  • dodatne poteškoće prilikom ažuriranja: u nekim slučajevima, usluga postojanosti i poslovna logika moraju se ažurirati sinkrono ili u strogom slijedu.

Naposljetku smo se morali vratiti osnovama: enkapsulirati podatke domene i poslovnu logiku domene u jednu mikrouslugu. Ovakav pristup pojednostavljuje percepciju mikroservisa kao sastavne komponente sustava i ne dovodi do gore navedenih problema. Ovo također nije besplatno:

  • Standardizacija API-ja potrebna je za interakciju s poslovnom logikom (posebno za pružanje korisničkih aktivnosti kao dijela poslovnih procesa) i usluge API platforme; zahtijeva veću pozornost na promjene API-ja, kompatibilnost naprijed i nazad;
  • potrebno je dodati dodatne runtime biblioteke kako bi se osiguralo funkcioniranje poslovne logike kao dijela svake takve mikrousluge, a to dovodi do novih zahtjeva za takve biblioteke: lakoća i minimum tranzitivnih ovisnosti;
  • programeri poslovne logike moraju pratiti verzije knjižnica: ako mikrousluga nije dovršena dulje vrijeme, tada će najvjerojatnije sadržavati zastarjelu verziju knjižnica. To može biti neočekivana prepreka za dodavanje nove značajke i može zahtijevati migraciju stare poslovne logike takve usluge na nove verzije biblioteka ako je došlo do nekompatibilnih promjena između verzija.

Integracija BPM stila

U takvoj arhitekturi prisutan je i sloj platformskih usluga, ali taj sloj više ne čini spremnik za izvršavanje domenske poslovne logike, već samo njegovo okruženje, pružajući pomoćne „platformske“ funkcije. Takav je sloj potreban ne samo za održavanje lagane prirode domenskih mikroservisa, već i za centralizaciju upravljanja.

Na primjer, aktivnosti korisnika u poslovnim procesima generiraju zadatke. Međutim, kada radi sa zadacima, korisnik mora vidjeti zadatke sa svih domena na općem popisu, što znači da mora postojati odgovarajuća usluga registracije zadataka platforme, očišćena od poslovne logike domene. Održavanje enkapsulacije poslovne logike u takvom kontekstu prilično je problematično, a to je još jedan kompromis ove arhitekture.

Integracija poslovnih procesa kroz oči programera aplikacija

Kao što je gore spomenuto, programer aplikacije mora biti apstrahiran od tehničkih i inženjerskih značajki implementacije interakcije nekoliko aplikacija kako bi se moglo računati na dobru razvojnu produktivnost.

Pokušajmo riješiti prilično težak problem integracije, posebno izmišljen za članak. Ovo će biti zadatak “igre” koji uključuje tri aplikacije, gdje svaka od njih definira određeni naziv domene: “app1”, “app2”, “app3”.

Unutar svake aplikacije pokreću se poslovni procesi koji počinju „igrati loptu“ kroz integracijsku sabirnicu. Poruke s nazivom “Ball” ponašat će se kao lopta.

Pravila igre:

  • prvi igrač je inicijator. Poziva druge igrače u igru, započinje igru ​​i može je završiti u bilo kojem trenutku;
  • ostali igrači izjavljuju svoje sudjelovanje u igri, „upoznaju“ jedni druge i prvog igrača;
  • nakon primljene lopte, igrač odabire drugog igrača koji sudjeluje i dodaje mu loptu. Broji se ukupan broj prijenosa;
  • Svaki igrač ima "energiju" koja se smanjuje sa svakim dodavanjem lopte od strane tog igrača. Kada ponestane energije, igrač napušta igru, najavljujući svoju ostavku;
  • ako igrač ostane sam, odmah najavljuje svoj odlazak;
  • Kada su svi igrači eliminirani, prvi igrač proglašava igru ​​završenom. Ako ranije napusti igru, ostaje pratiti igru ​​kako bi je završio.

Kako bih riješio ovaj problem, upotrijebit ću naš DSL za poslovne procese, koji nam omogućuje da kompaktno opišemo logiku u Kotlinu, s minimumom šablona.

Poslovni proces prvog igrača (tj. pokretača igre) radit će u aplikaciji app1:

klasa InitialPlayer

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

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

class PlayersList : ArrayList<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    startFrom(sendNewGameSignal)
            .fork("mainFork") {
                next(startTask)
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut)
                        .branch("checkPlayers") {
                            ifTrue { players.isEmpty() }
                                    .next(sendStopGameSignal)
                                    .terminate()
                            ifElse().next(waitPlayerOut)
                        }
            }
    startTask.fork("afterStart") {
        next(throwStartBall)
                .branch("mainLoop") {
                    ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
                    ifElse().next(waitBall).next(throwBall).loop()
                }
        next(stopTask).next(sendStopGameSignal)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

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

Osim izvršavanja poslovne logike, gornji kod može proizvesti objektni model poslovnog procesa, koji se može vizualizirati u obliku dijagrama. Još nismo implementirali vizualizator, pa smo morali potrošiti malo vremena na crtanje (ovdje sam malo pojednostavio BPMN notaciju u vezi s upotrebom vrata kako bih poboljšao dosljednost dijagrama s kodom u nastavku):

Integracija BPM stila

app2 će uključivati ​​poslovni proces drugog igrača:

klasa 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}")
}

Dijagram:

Integracija BPM stila

U aplikaciji app3 napravit ćemo igrača s malo drugačijim ponašanjem: umjesto nasumičnog odabira sljedećeg igrača, on će djelovati prema kružnom algoritmu:

klasa RoundRobinPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

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

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

val roundRobinPlayerModel = processModel<RoundRobinPlayer>(
        name = "RoundRobinPlayer", 
        version = 1) {

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

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

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

private fun MessageSendInstance<RoundRobinPlayer, Int>.selectNextPlayer() {
    var idx = process.nextPlayerIndex + 1
    if (idx >= process.players.size) {
        idx = 0
    }
    process.nextPlayerIndex = idx
    val player = if (process.players.isNotEmpty()) 
        process.players[idx] 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Inače, ponašanje igrača se ne razlikuje od prethodnog, tako da se dijagram ne mijenja.

Sada nam treba test da sve ovo pokrenemo. Dat ću samo kod samog testa, kako ne bih zatrpao članak šablonom (u stvari, koristio sam testno okruženje stvoreno ranije za testiranje integracije drugih poslovnih procesa):

testIgra()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

private Map<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Pokrenimo test i pogledajmo dnevnik:

konzolni izlaz

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

Iz svega ovoga možemo izvući nekoliko važnih zaključaka:

  • s potrebnim alatima, programeri aplikacija mogu stvoriti integracijske interakcije između aplikacija bez prekidanja poslovne logike;
  • složenost integracijskog zadatka koji zahtijeva inženjerske kompetencije može biti skrivena unutar okvira ako je to inicijalno uključeno u arhitekturu okvira. Težina problema se ne može sakriti, pa će rješenje teškog problema u kodu izgledati tako;
  • Pri razvoju integracijske logike nužno je voditi računa o eventualnoj konzistentnosti i nedostatku linearizabilnosti promjena stanja svih sudionika integracije. To nas tjera da kompliciramo logiku kako bismo je učinili neosjetljivom na redoslijed kojim se događaju vanjski događaji. U našem primjeru, igrač je prisiljen sudjelovati u igri nakon što je proglasio izlazak iz igre: ostali igrači će mu nastaviti dodavati loptu sve dok informacija o njegovom izlasku ne stigne i ne bude obrađena od strane svih sudionika. Ova logika ne proizlazi iz pravila igre i kompromisno je rješenje u okviru odabrane arhitekture.

Zatim ćemo govoriti o raznim zamršenostima našeg rješenja, kompromisima i drugim točkama.

Sve poruke su u jednom redu

Sve integrirane aplikacije rade s jednom integracijskom sabirnicom, koja je predstavljena u obliku vanjskog brokera, jednog BPMQueuea za poruke i jedne BPMTopic teme za signale (događaje). Stavljanje svih poruka u jedan red samo je kompromis. Na razini poslovne logike, sada možete uvesti onoliko novih tipova poruka koliko želite bez promjena strukture sustava. Ovo je značajno pojednostavljenje, ali nosi određene rizike, koji nam se u kontekstu naših tipičnih zadataka nisu činili toliko značajnima.

Integracija BPM stila

No, tu postoji jedna suptilnost: svaka aplikacija filtrira "svoje" poruke iz reda čekanja na ulazu, prema nazivu svoje domene. Domena se također može navesti u signalima ako trebate ograničiti "opseg vidljivosti" signala na jednu aplikaciju. To bi trebalo povećati propusnost sabirnice, ali poslovna logika sada mora raditi s nazivima domena: za adresiranje poruka - obavezno, za signale - poželjno.

Osiguravanje pouzdanosti integracijske sabirnice

Pouzdanost se sastoji od nekoliko točaka:

  • Odabrani posrednik poruka kritična je komponenta arhitekture i jedina točka kvara: mora biti dovoljno tolerantan na pogreške. Trebali biste koristiti samo provjerene implementacije, s dobrom podrškom i velikom zajednicom;
  • potrebno je osigurati visoku dostupnost brokera poruka, za što on mora biti fizički odvojen od integriranih aplikacija (visoku dostupnost aplikacija s primijenjenom poslovnom logikom puno je teže i skuplje osigurati);
  • posrednik je dužan "barem jednom" dati jamstva isporuke. Ovo je obavezan uvjet za pouzdan rad integracijske sabirnice. Nema potrebe za jamstvima razine “točno jednom”: poslovni procesi u pravilu nisu osjetljivi na ponovni dolazak poruka ili događaja, a kod posebnih zadataka gdje je to važno lakše je dodati dodatne provjere poslovanju logike nego stalno koristiti prilično “skupa” jamstva;
  • slanje poruka i signala mora biti uključeno u cjelokupnu transakciju s promjenama stanja poslovnih procesa i podataka domene. Poželjna opcija bila bi upotreba uzorka Transakcijski izlazni spremnik, ali će zahtijevati dodatnu tablicu u bazi i repetitor. U JEE aplikacijama to se može pojednostaviti korištenjem lokalnog JTA upravitelja, ali veza s odabranim brokerom mora moći raditi u XA;
  • rukovatelji dolaznim porukama i događajima također moraju raditi s transakcijom koja mijenja stanje poslovnog procesa: ako se takva transakcija vrati, tada se mora poništiti primitak poruke;
  • poruke koje se ne mogu isporučiti zbog grešaka moraju se pohraniti u zasebno spremište D.L.Q. (Mrtvo pismo u redu). U tu svrhu kreirali smo zasebnu platformsku mikrouslugu koja takve poruke pohranjuje u svoju pohranu, indeksira ih po atributima (za brzo grupiranje i pretraživanje), te izlaže API za pregled, ponovno slanje na odredišnu adresu i brisanje poruka. Administratori sustava mogu raditi s ovom uslugom putem svog web sučelja;
  • u postavkama brokera potrebno je prilagoditi broj ponovnih pokušaja isporuke i kašnjenja između isporuka kako bi se smanjila vjerojatnost ulaska poruka u DLQ (gotovo je nemoguće izračunati optimalne parametre, ali možete djelovati empirijski i prilagoditi ih tijekom rada );
  • DLQ pohrana mora se kontinuirano nadzirati, a nadzorni sustav mora upozoriti administratore sustava kako bi u slučaju neisporučene poruke mogli odgovoriti što je brže moguće. To će smanjiti "pogođeno područje" kvara ili pogreške poslovne logike;
  • integracijska sabirnica mora biti neosjetljiva na privremenu odsutnost aplikacija: pretplate na temu moraju biti trajne, a naziv domene aplikacije mora biti jedinstven tako da dok je aplikacija odsutna, netko drugi neće pokušati obraditi njezine poruke iz red.

Osiguravanje sigurnosti niti poslovne logike

Ista instanca poslovnog procesa može primiti više poruka i događaja odjednom, čija će obrada započeti paralelno. U isto vrijeme, za programera aplikacija sve bi trebalo biti jednostavno i sigurno za niti.

Poslovna logika procesa pojedinačno obrađuje svaki vanjski događaj koji utječe na taj poslovni proces. Takvi događaji mogu biti:

  • pokretanje instance poslovnog procesa;
  • radnja korisnika vezana uz aktivnost unutar poslovnog procesa;
  • primitak poruke ili signala na koji je instanca poslovnog procesa pretplaćena;
  • pokretanje mjerača vremena postavljenog od strane instance poslovnog procesa;
  • kontrolna radnja putem API-ja (na primjer, prekid procesa).

Svaki takav događaj može promijeniti stanje instance poslovnog procesa: neke aktivnosti mogu završiti, a druge započeti, a vrijednosti trajnih svojstava mogu se promijeniti. Zatvaranje bilo koje aktivnosti može rezultirati aktivacijom jedne ili više sljedećih aktivnosti. Oni pak mogu prestati čekati druge događaje ili, ako im nisu potrebni nikakvi dodatni podaci, mogu završiti u istoj transakciji. Prije zatvaranja transakcije, novo stanje poslovnog procesa sprema se u bazu podataka, gdje će čekati da se dogodi sljedeći vanjski događaj.

Trajni podaci poslovnog procesa pohranjeni u relacijskoj bazi podataka vrlo su zgodna točka za sinkronizaciju obrade ako koristite SELECT FOR UPDATE. Ako je jedna transakcija uspjela dobiti stanje poslovnog procesa iz baze za njegovu promjenu, tada nijedna druga paralelna transakcija neće moći dobiti isto stanje za drugu promjenu, a nakon završetka prve transakcije, druga je zajamčeno primiti već promijenjeno stanje.

Korištenjem pesimističnih zaključavanja na strani DBMS-a ispunjavamo sve potrebne zahtjeve ACID, a također zadržati mogućnost skaliranja aplikacije s poslovnom logikom povećanjem broja pokrenutih instanci.

Međutim, pesimistična zaključavanja prijete nam zastojima, što znači da bi SELECT FOR UPDATE ipak trebao biti ograničen na neko razumno vrijeme čekanja u slučaju da do zastoja dođe u nekim nečuvenim slučajevima u poslovnoj logici.

Drugi problem je sinkronizacija početka poslovnog procesa. Dok ne postoji instanca poslovnog procesa, nema stanja u bazi podataka, pa opisana metoda neće raditi. Ako trebate osigurati jedinstvenost instance poslovnog procesa u određenom opsegu, tada će vam trebati neka vrsta sinkronizacijskog objekta povezanog s klasom procesa i odgovarajućim opsegom. Da bismo riješili ovaj problem, koristimo drugačiji mehanizam zaključavanja koji nam omogućuje zaključavanje proizvoljnog resursa određenog ključem u URI formatu putem vanjske usluge.

U našim primjerima poslovni proces InitialPlayer sadrži deklaraciju

uniqueConstraint = UniqueConstraints.singleton

Stoga zapisnik sadrži poruke o preuzimanju i otključavanju odgovarajućeg ključa. Ne postoje takve poruke za druge poslovne procese: uniqueConstraint nije postavljen.

Problemi poslovnih procesa s postojanim stanjem

Ponekad postojano stanje ne samo da pomaže, već i stvarno koči razvoj.
Problemi počinju kada je potrebno napraviti promjene u poslovnoj logici i/ili modelu poslovnih procesa. Nije svaka takva promjena kompatibilna sa starim stanjem poslovnih procesa. Ako postoji mnogo živih instanci u bazi podataka, tada nekompatibilne promjene mogu uzrokovati mnogo problema, s kojima smo se često susreli pri korištenju jBPM-a.

Ovisno o dubini promjena, možete djelovati na dva načina:

  1. kreirajte novi tip poslovnog procesa kako ne biste napravili nekompatibilne promjene na starom i koristite ga umjesto starog prilikom pokretanja novih instanci. Stare kopije će nastaviti raditi "kao prije";
  2. migrirati trajno stanje poslovnih procesa prilikom ažuriranja poslovne logike.

Prvi način 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 potreban trenutni prijelaz na novu poslovnu logiku (u smislu integracijskih zadataka - gotovo uvijek);
  • programer ne zna u kojem se trenutku mogu izbrisati zastarjeli modeli.

U praksi koristimo oba pristupa, ali donijeli smo niz odluka koje će nam olakšati život:

  • U bazi podataka, trajno stanje poslovnog procesa pohranjuje se u lako čitljivom i lako obradivom obliku: u nizu JSON formata. To omogućuje izvođenje migracija i unutar aplikacije i izvan nje. U krajnjem slučaju, možete ga ispraviti ručno (osobito korisno u razvoju tijekom otklanjanja pogrešaka);
  • integracijska poslovna logika ne koristi nazive poslovnih procesa, tako da je u svakom trenutku moguće implementaciju jednog od procesa koji sudjeluju zamijeniti novim s novim nazivom (npr. “InitialPlayerV2”). Povezivanje se događa putem imena poruka i signala;
  • model procesa ima broj verzije, koji povećavamo ako napravimo nekompatibilne promjene u ovom modelu, a taj se broj sprema zajedno sa stanjem instance procesa;
  • postojano stanje procesa prvo se čita iz baze podataka u prikladan objektni model, s kojim procedura migracije može raditi ako se broj verzije modela promijenio;
  • procedura migracije se nalazi uz poslovnu logiku i naziva se “lijenom” za svaku instancu poslovnog procesa u trenutku njegove obnove iz baze podataka;
  • ako trebate migrirati stanje svih instanci procesa brzo i sinkrono, koriste se klasičnija rješenja za migraciju baza podataka, ali morate raditi s JSON-om.

Trebate li još jedan okvir za poslovne procese?

Rješenja opisana u članku omogućila su nam da značajno pojednostavimo svoj život, proširimo raspon problema koji se rješavaju na razini razvoja aplikacija i učinimo ideju o razdvajanju poslovne logike na mikroservise privlačnijom. Da bi se to postiglo, učinjeno je puno posla, kreiran je vrlo „lagani“ okvir za poslovne procese, kao i servisne komponente za rješavanje identificiranih problema u kontekstu širokog spektra aplikativnih problema. Imamo želju podijeliti ove rezultate i učiniti razvoj zajedničkih komponenti otvorenim pristupom pod slobodnom licencom. To će zahtijevati malo truda i vremena. Razumijevanje potražnje za takvim rješenjima moglo bi nam biti dodatni poticaj. U predloženom članku vrlo je malo pažnje posvećeno mogućnostima samog okvira, no neke od njih su vidljive iz prikazanih primjera. Ako objavimo naš okvir, o njemu ćemo posvetiti poseban članak. U međuvremenu, bili bismo vam zahvalni ako ostavite povratnu informaciju odgovorom na pitanje:

U anketi mogu sudjelovati samo registrirani korisnici. Prijaviti se, molim.

Trebate li još jedan okvir za poslovne procese?

  • 18,8%Da, dugo sam tražio ovako nešto

  • 12,5%Zanima me više o vašoj implementaciji, moglo bi biti korisno2

  • 6,2%Koristimo jedan od postojećih okvira, ali razmišljamo o zamjeni1

  • 18,8%Koristimo jedan od postojećih okvira, sve je u redu3

  • 18,8%snalazimo se bez okvira3

  • 25,0%napiši svoje4

Glasovalo je 16 korisnika. Suzdržano je bilo 7 korisnika.

Izvor: www.habr.com

Dodajte komentar