Integracija BPM stila

Integracija BPM stila

Ćao Habr!

Naša kompanija je specijalizovana za razvoj softverskih rešenja ERP klase, u kojima lavovski udeo zauzimaju transakcioni sistemi sa ogromnom količinom poslovne logike i toka rada a la EDMS. Moderne verzije naših proizvoda su bazirane na JavaEE tehnologijama, ali aktivno eksperimentišemo i sa mikroservisima. Jedno od najproblematičnijih područja ovakvih rješenja je integracija različitih podsistema vezanih za susjedne domene. Integracijski zadaci su nam uvijek zadavali ogromnu glavobolju, bez obzira na arhitektonske stilove, tehnološke stekove i okvire koje koristimo, ali u posljednje vrijeme postoji napredak u rješavanju takvih problema.

U članku na koji Vam je skrenuta pažnja govorit ću o iskustvu i arhitektonskim istraživanjima NPO Krista na predviđenom prostoru. Također ćemo razmotriti primjer jednostavnog rješenja problema integracije sa stanovišta programera aplikacije i otkriti šta se krije iza ove jednostavnosti.

Odricanje od odgovornosti

Arhitektonska i tehnička rješenja opisana u članku nudim na osnovu ličnog iskustva u kontekstu konkretnih zadataka. Ova rješenja ne tvrde da su univerzalna i možda neće biti optimalna pod drugim uvjetima korištenja.

Kakve veze BPM ima s tim?

Da bismo odgovorili na ovo pitanje, moramo se malo udubiti u specifičnosti primijenjenih problema naših rješenja. Glavni dio poslovne logike u našem tipičnom transakcionom sistemu je unos podataka u bazu podataka preko korisničkih interfejsa, ručna i automatska provjera ovih podataka, prolazak kroz neki radni tok, objavljivanje u drugom sistemu / analitička baza podataka / arhiva, generiranje izvještaja. Dakle, ključna funkcija sistema za kupce je automatizacija njihovih internih poslovnih procesa.

Radi praktičnosti, u komunikaciji koristimo termin "dokument" kao neku apstrakciju skupa podataka, ujedinjenih zajedničkim ključem, na koji se može "prikačiti" određeni tok posla.
Ali šta je sa logikom integracije? Na kraju krajeva, zadatak integracije je generisan arhitekturom sistema, koji se „preseče“ na delove NE na zahtev kupca, već pod uticajem potpuno različitih faktora:

  • pod uticajem Konvejevog zakona;
  • kao rezultat ponovne upotrebe podsistema prethodno razvijenih za druge proizvode;
  • prema odluci arhitekte, na osnovu nefunkcionalnih zahtjeva.

Postoji veliko iskušenje da se integraciona logika odvoji od poslovne logike glavnog toka posla kako se ne bi zagadila poslovna logika integracijskim artefaktima i spasio programera aplikacije od potrebe da se udubljuje u posebnosti arhitektonskog pejzaža sistema. Ovaj pristup ima niz prednosti, ali praksa pokazuje njegovu neefikasnost:

  • rješavanje problema integracije obično se spušta na najjednostavnije opcije u obliku sinhronih poziva zbog ograničenih tačaka proširenja u implementaciji glavnog toka posla (više o nedostacima sinhrone integracije u nastavku);
  • artefakti integracije i dalje prodiru u glavnu poslovnu logiku kada je potrebna povratna informacija iz drugog podsistema;
  • programer aplikacije ignorira integraciju i može je lako prekinuti promjenom toka posla;
  • sistem prestaje da bude jedinstvena celina sa stanovišta korisnika, postaju primetni "šavovi" između podsistema, pojavljuju se suvišne korisničke operacije koje iniciraju prenos podataka iz jednog podsistema u drugi.

Drugi pristup je razmatranje integracijskih interakcija kao integralnog dijela osnovne poslovne logike i toka posla. Kako zahtjevi za vještinama programera aplikacija ne bi naglo porasli, kreiranje novih integracijskih interakcija trebalo bi da se radi lako i prirodno, uz minimalne mogućnosti za odabir rješenja. Ovo je teže nego što izgleda: alat mora biti dovoljno moćan da korisniku pruži potrebnu raznolikost opcija za njegovu upotrebu, a da pritom ne dozvoli sebi da bude pogođen u nogu. Postoje mnoga pitanja na koja inženjer treba da odgovori u kontekstu integracijskih zadataka, ali o kojima programer aplikacije ne bi trebao razmišljati u svom svakodnevnom radu: granice transakcije, konzistentnost, atomičnost, sigurnost, skaliranje, distribucija opterećenja i resursa, rutiranje, maršaliranje, propagation and switching contexts, itd. Potrebno je ponuditi programerima aplikacija prilično jednostavne šablone odluka, u kojima su odgovori na sva ovakva pitanja već skriveni. Ovi obrasci bi trebali biti dovoljno sigurni: poslovna logika se vrlo često mijenja, što povećava rizik od unošenja grešaka, cijena grešaka treba ostati na prilično niskom nivou.

Ali ipak, kakve veze BPM ima s tim? Postoji mnogo opcija za implementaciju toka posla...
Zaista, još jedna implementacija poslovnih procesa je vrlo popularna u našim rješenjima - kroz deklarativno postavljanje dijagrama tranzicije stanja i povezivanje rukovatelja s poslovnom logikom na tranzicije. Istovremeno, 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 takve implementacije je zbog relativne jednostavnosti i brzine kreiranja linearnih poslovnih procesa. Međutim, kako softverski sistemi postaju složeniji, automatizirani dio poslovnog procesa raste i postaje složeniji. Postoji potreba za dekompozicijom, ponovnom upotrebom delova procesa, kao i forking procesima tako da se svaka grana izvršava paralelno. U takvim uslovima alat postaje nezgodan, a dijagram tranzicije stanja gubi svoj informacioni sadržaj (interakcije integracije se uopšte 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 sa najsloženijim poslovnim procesima. Kratkoročno, ovo rješenje je imalo određeni uspjeh: postalo je moguće implementirati složene poslovne procese uz održavanje prilično informativnog i ažurnog dijagrama u notaciji. BPMN2.

Integracija BPM stila
Mali dio složenog poslovnog procesa

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

Glavni pozitivni aspekt upotrebe jBPM-a je bio spoznaja koristi i štetnosti postojanja vlastitog postojanog stanja za instancu poslovnog procesa. Također smo vidjeli mogućnost korištenja procesnog pristupa za implementaciju složenih protokola integracije između različitih aplikacija korištenjem asinhronih interakcija putem signala i poruka. Prisustvo postojanog stanja igra ključnu ulogu u tome.

Na osnovu navedenog možemo zaključiti: Procesni pristup u BPM stilu omogućava nam da riješimo širok spektar zadataka za automatizaciju sve složenijih poslovnih procesa, harmonično uklopimo integracijske aktivnosti u ove procese i zadržimo mogućnost vizualnog prikaza implementiranog procesa u odgovarajućoj notaciji.

Nedostaci sinkronih poziva kao obrasca integracije

Sinhrona integracija se odnosi na najjednostavniji blokirajući poziv. Jedan podsistem djeluje kao serverska strana i izlaže API sa željenom metodom. Drugi podsistem djeluje kao strana klijenta i, u pravo vrijeme, upućuje poziv sa očekivanjem rezultata. U zavisnosti od arhitekture sistema, strana klijenta i servera mogu biti hostovani ili u istoj aplikaciji i procesu ili u različitim. U drugom slučaju, trebate primijeniti neku implementaciju RPC-a i obezbijediti maršaliranje parametara i rezultata poziva.

Integracija BPM stila

Takav integracijski obrazac ima prilično veliki skup nedostataka, ali se vrlo široko koristi u praksi zbog svoje jednostavnosti. Brzina implementacije pleni i tera vas da je primenjujete iznova i iznova u uslovima "gorućih" rokova, upisujući rešenje u tehnički dug. Ali dešava se i da ga neiskusni programeri koriste nesvjesno, jednostavno ne shvaćajući negativne posljedice.

Pored najočiglednijeg povećanja povezanosti podsistema, manje su očigledni problemi sa "rasipanjem" i "razvlačenjem" transakcija. Zaista, ako poslovna logika napravi bilo kakve promjene, onda su transakcije neophodne, a transakcije, zauzvrat, zaključavaju određene resurse aplikacije na koje utiču ove promjene. To jest, sve dok jedan podsistem ne čeka odgovor od drugog, neće moći dovršiti transakciju i osloboditi zaključavanja. Ovo značajno povećava rizik od raznih efekata:

  • gubi se odziv sistema, korisnici dugo čekaju na odgovore na zahtjeve;
  • server generalno prestaje da odgovara na korisničke zahtjeve zbog prepune baze niti: većina niti „stoji” na zaključavanju resursa koji zauzima transakcija;
  • počinju da se pojavljuju zastoji: vjerovatnoća njihovog pojavljivanja jako ovisi o trajanju transakcije, količini poslovne logike i zaključavanja uključenih u transakciju;
  • pojavljuju se greške isteka vremena čekanja transakcije;
  • server “padne” na OutOfMemory ako zadatak zahtijeva obradu i promjenu velike količine podataka, a prisustvo sinhronih integracija otežava podjelu obrade na “lakše” transakcije.

Sa arhitektonske tačke gledišta, upotreba blokiranja poziva tokom integracije dovodi do gubitka kontrole kvaliteta pojedinačnih podsistema: nemoguće je postići ciljeve kvaliteta za jedan podsistem izolovano od indikatora kvaliteta za drugi podsistem. Ako podsisteme razvijaju različiti timovi, to je veliki problem.

Stvari postaju još interesantnije ako su podsistemi koji se integrišu u različitim aplikacijama i ako je potrebno izvršiti sinhrone promjene na obje strane. Kako ove promjene učiniti transakcionim?

Ako se promjene izvrše u odvojenim transakcijama, tada će biti potrebno osigurati robusno rukovanje izuzecima i kompenzaciju, a to u potpunosti eliminira glavnu prednost sinhronih 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 transakcija

Sa rastućom popularnošću mikrousluga, postoji sve veća potražnja za njima Saga Pattern.

Ovaj obrazac savršeno rješava gore navedene probleme dugih transakcija, a također proširuje mogućnosti upravljanja stanjem sistema sa strane poslovne logike: kompenzacija nakon neuspješne transakcije možda neće vratiti sistem u prvobitno stanje, ali će omogućiti alternativni put obrade podataka. Takođe vam omogućava da ne ponavljate uspešno završene korake obrade podataka kada pokušate da dovedete proces do "dobrog" kraja.

Zanimljivo je da je u monolitnim sistemima ovaj obrazac relevantan i kada je u pitanju integracija labavo povezanih podsistema i postoje negativni efekti uzrokovani dugim transakcijama i odgovarajućim zaključavanjem resursa.

Što se tiče naših poslovnih procesa u BPM stilu, pokazalo se da je implementiranje Saga vrlo jednostavno: pojedinačni koraci Saga mogu se postaviti kao aktivnosti unutar poslovnog procesa, a trajno stanje poslovnog procesa određuje, između druge stvari, unutrašnje stanje Saga. Odnosno, ne treba nam nikakav dodatni mehanizam koordinacije. Sve što vam treba je broker poruka sa podrškom za "barem jednom" garancije kao transport.

Ali ovo rješenje ima i svoju "cijenu":

  • poslovna logika postaje složenija: potrebno je izraditi kompenzaciju;
  • biće potrebno napustiti punu konzistentnost, što može biti posebno osjetljivo za monolitne sisteme;
  • arhitektura postaje malo komplikovanija, postoji dodatna potreba za brokerom poruka;
  • biće potrebni dodatni alati za praćenje i administraciju (iako je generalno to čak i dobro: kvalitet sistemske usluge će se povećati).

Za monolitne sisteme, opravdanje za korištenje "Sags" nije tako očigledno. Za mikroservise i druge SOA-e, gdje, najvjerovatnije, već postoji broker, a puna konzistentnost je žrtvovana na početku projekta, prednosti korištenja ovog obrasca mogu značajno nadmašiti nedostatke, posebno ako postoji pogodan API na nivo poslovne logike.

Enkapsulacija poslovne logike u mikroservisima

Kada smo počeli da eksperimentišemo sa mikroservisima, postavilo se razumno pitanje: gde staviti poslovnu logiku domena u odnosu na servis koji obezbeđuje postojanost podataka domena?

Kada se posmatra arhitektura različitih BPMS-a, može se činiti razumnim odvojiti poslovnu logiku od postojanosti: kreirati sloj mikroservisa nezavisnih od platforme i domene koji formiraju okruženje i kontejner za izvršavanje poslovne logike domene i urediti postojanost podataka domene kao zaseban sloj vrlo jednostavnih i laganih mikroservisa. Poslovni procesi u ovom slučaju orkestriraju usluge sloja postojanosti.

Integracija BPM stila

Ovaj pristup ima vrlo veliki plus: možete povećati funkcionalnost platforme koliko god želite, a samo će se odgovarajući sloj platformskih mikroservisa "udebljati" od toga. Poslovni procesi iz bilo koje domene odmah dobijaju priliku da koriste novu funkcionalnost platforme čim se ona ažurira.

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

  • usluga platforme koja istovremeno izvršava poslovnu logiku mnogih domena nosi velike rizike kao jednu tačku neuspjeha. Česte promjene poslovne logike povećavaju rizik od grešaka koje dovode do kvarova u cijelom sistemu;
  • problemi performansi: poslovna logika radi sa svojim podacima kroz usko i sporo sučelje:
    • podaci će još jednom biti raspoređeni i pumpani kroz mrežni stog;
    • domenska usluga će često vratiti više podataka nego što poslovna logika zahtijeva za obradu, zbog nedovoljnih mogućnosti parametrizacije upita na nivou eksternog API-ja usluge;
    • nekoliko nezavisnih delova poslovne logike može više puta tražiti iste podatke za obradu (ovaj problem možete ublažiti dodavanjem sesijskih bean-ova koji keširaju podatke, ali to dodatno komplikuje arhitekturu i stvara probleme sa svežinom podataka i poništavanjem keša);
  • transakcijski problemi:
    • poslovni procesi sa postojanim stanjem koje pohranjuje usluga platforme nisu u skladu s podacima domene i ne postoje laki načini za rješavanje ovog problema;
    • pomicanje zaključavanja podataka domene iz transakcije: ako je potrebno izvršiti promjene u poslovnoj logici domene, nakon prve provjere ispravnosti stvarnih podataka, potrebno je isključiti mogućnost konkurentske promjene u obrađenim podacima. Eksterno blokiranje podataka može pomoći u rješavanju problema, ali takvo rješenje nosi dodatne rizike i smanjuje ukupnu pouzdanost sistema;
  • dodatne komplikacije prilikom ažuriranja: u nekim slučajevima morate ažurirati uslugu postojanosti i poslovnu logiku sinhrono ili u strogom redoslijedu.

Na kraju sam se morao vratiti osnovama: inkapsulirati podatke domene i poslovnu logiku domena u jedan mikroservis. Ovaj pristup pojednostavljuje percepciju mikroservisa kao integralne komponente u sistemu i ne izaziva gore navedene probleme. Ovo također nije besplatno:

  • API standardizacija je potrebna za interakciju sa poslovnom logikom (posebno za pružanje aktivnosti korisnika kao dio poslovnih procesa) i uslugama API platforme; potrebna je pažljivija pažnja na promjene API-ja, potrebna je kompatibilnost naprijed i nazad;
  • potrebno je dodati dodatne runtime biblioteke kako bi se osiguralo funkcionisanje poslovne logike kao dio svake takve mikroservise, a to dovodi do novih zahtjeva za takve biblioteke: lakoća i minimum tranzitivnih ovisnosti;
  • Programeri poslovne logike moraju pratiti verzije biblioteka: ako mikroservis nije finaliziran dugo vremena, onda će najvjerovatnije sadržavati zastarjelu verziju biblioteka. Ovo može biti neočekivana prepreka za dodavanje nove funkcije i može zahtijevati da se stara poslovna logika takve usluge migrira u nove verzije biblioteka ako je bilo nekompatibilnih promjena između verzija.

Integracija BPM stila

U takvoj arhitekturi je prisutan i sloj platformskih usluga, ali ovaj sloj više ne čini kontejner za izvršavanje poslovne logike domene, već samo njegovo okruženje, koje pruža pomoćne funkcije „platforme“. Takav sloj je potreban ne samo za održavanje lakoće domenskih mikroservisa, već i za centralizaciju upravljanja.

Na primjer, aktivnosti korisnika u poslovnim procesima generiraju zadatke. Međutim, prilikom rada sa zadacima, korisnik mora vidjeti zadatke sa svih domena u općoj listi, što znači da mora postojati odgovarajuća usluga platforme za registraciju zadataka, očišćena od domenske poslovne logike. Održavanje inkapsulacije poslovne logike u ovom kontekstu je prilično problematično, a ovo je još jedan kompromis ove arhitekture.

Integracija poslovnih procesa očima programera aplikacija

Kao što je već spomenuto, programer aplikacije mora biti apstrahovan od tehničkih i inženjerskih karakteristika implementacije interakcije nekoliko aplikacija kako bi mogao 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, pri čemu svaka od njih definira neki naziv domene: "app1", "app2", "app3".

Unutar svake aplikacije pokreću se poslovni procesi koji počinju "igrati loptu" kroz integracijsku magistralu. Poruke pod nazivom "Lopta" će se ponašati kao lopta.

Pravila igre:

  • prvi igrač je inicijator. On poziva druge igrače u igru, započinje igru ​​i može je završiti u bilo kojem trenutku;
  • drugi igrači se izjašnjavaju o svom učešću u igri, "upoznaju" se jedni sa drugima i prvim igračem;
  • nakon što primi loptu, igrač bira drugog igrača koji učestvuje i dodaje mu loptu. Računa se ukupan broj prolaza;
  • svaki igrač ima "energiju", koja se smanjuje sa svakim dodavanjem lopte tog igrača. Kada energija ponestane, igrač se eliminira iz igre, najavljujući svoje povlačenje;
  • ako igrač ostane sam, odmah izjavljuje svoj odlazak;
  • kada su svi igrači eliminisani, prvi igrač proglašava kraj igre. Ako je napustio igru ​​ranije, ostaje da prati igru ​​kako bi je završio.

Da riješim ovaj problem, koristit ću naš DSL za poslovne procese, koji vam omogućava da opišete logiku u Kotlinu kompaktno, s minimalnim šablonom.

U aplikaciji app1 će raditi poslovni proces prvog igrača (on je i pokretač 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<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}")
}

Pored izvršavanja poslovne logike, gornji kod može proizvesti objektni model poslovnog procesa koji se može vizualizirati kao dijagram. Još nismo implementirali vizualizator, pa smo morali potrošiti neko vrijeme na crtanje (ovdje sam malo pojednostavio BPMN notaciju u vezi sa upotrebom kapija da poboljšam konzistentnost dijagrama sa gornjim kodom):

Integracija BPM stila

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

class RandomPlayer

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

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

class PlayersList: ArrayList<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 se ponašati 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, pa se dijagram ne mijenja.

Sada nam je potreban test da sve to pokrenemo. Dat ću samo kod samog testa, kako ne bih zatrpao članak šablonom (u stvari, koristio sam testno okruženje kreirano ranije da testiram integraciju drugih poslovnih procesa):

testGame()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через 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;
}

Pokrenite test, pogledajte zapisnik:

izlaz konzole

Взята блокировка ключа 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že se izvući nekoliko važnih zaključaka:

  • ako su potrebni alati dostupni, programeri aplikacija mogu kreirati integracijske interakcije između aplikacija bez odvajanja od poslovne logike;
  • složenost (složenost) integracijskog zadatka koji zahtijeva inženjerske kompetencije može biti skrivena unutar okvira ako je inicijalno postavljena u arhitekturi okvira. Težina zadatka (poteškoća) se ne može sakriti, pa će rješenje teškog zadatka u kodu izgledati u skladu s tim;
  • pri razvoju logike integracije potrebno je voditi računa o eventualnoj konzistentnosti i nedostatku linearizabilnosti promjene stanja svih učesnika integracije. To nas tjera da kompliciramo logiku kako bismo je učinili neosjetljivom na redoslijed u kojem se dešavaju vanjski događaji. U našem primjeru, igrač je prisiljen sudjelovati u igri nakon što objavi svoj izlazak iz igre: drugi igrači će nastaviti da mu dodaju 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 predstavlja kompromisno rješenje u okviru odabrane arhitekture.

Dalje, hajde da pričamo o raznim suptilnostima našeg rešenja, kompromisima i drugim tačkama.

Sve poruke u jednom redu

Sve integrisane aplikacije rade sa jednom integracijskom magistralom, koja je predstavljena kao eksterni broker, jednom BPMQueue za poruke i jednom BPMTopic temom za signale (događaje). Propuštanje svih poruka kroz jedan red je samo po sebi kompromis. Na nivou poslovne logike, sada možete uvesti onoliko novih tipova poruka koliko želite bez unošenja promjena u strukturu sistema. Ovo je značajno pojednostavljenje, ali nosi određene rizike, koji nam se u kontekstu naših tipičnih zadataka nisu činili toliko značajnim.

Integracija BPM stila

Međutim, ovdje postoji jedna suptilnost: svaka aplikacija filtrira "svoje" poruke iz reda čekanja na ulazu, po imenu svoje domene. Također, domen se može specificirati u signalima, ako trebate ograničiti “opseg” signala na jednu aplikaciju. Ovo bi trebalo povećati propusni opseg magistrale, ali poslovna logika sada mora raditi sa imenima domena: obaveznim za adresiranje poruka, poželjnim za signale.

Osiguravanje pouzdanosti integracione magistrale

Pouzdanost se sastoji od nekoliko stvari:

  • Odabrani posrednik poruka je kritična komponenta arhitekture i jedina tačka kvara: mora biti dovoljno tolerantan na greške. Trebali biste koristiti samo vremenski testirane implementacije sa dobrom podrškom i velikom zajednicom;
  • potrebno je osigurati visoku dostupnost brokera poruka, za šta on mora biti fizički odvojen od integrisanih aplikacija (visoku dostupnost aplikacija sa primijenjenom poslovnom logikom je mnogo teže i skuplje obezbijediti);
  • posrednik je dužan dati garancije isporuke "barem jednom". Ovo je obavezan zahtjev za pouzdan rad integracione magistrale. Nema potrebe za garancijama na nivou "tačno jednom": poslovni procesi obično nisu osjetljivi na ponovljeno primanje poruka ili događaja, a u posebnim zadacima gdje je to važno, lakše je dodati dodatne provjere poslovnoj logici nego stalno koristiti. "skupe" " garancije;
  • slanje poruka i signala mora biti uključeno u zajedničku transakciju sa promjenom stanja poslovnih procesa i podataka domene. Poželjna opcija bi bila korištenje uzorka Transactional Outbox, ali će zahtijevati dodatnu tablicu u bazi podataka i relej. U JEE aplikacijama, ovo se može pojednostaviti korištenjem lokalnog JTA menadžera, ali veza s odabranim brokerom mora biti u mogućnosti da radi u režimu XA;
  • rukovaoci dolaznih poruka i događaja moraju raditi i sa transakcijom promjene stanja poslovnog procesa: ako se takva transakcija vrati, tada se mora otkazati i prijem poruke;
  • poruke koje nisu mogle biti isporučene zbog grešaka treba pohraniti u posebnu prodavnicu D.L.Q. (Red mrtvih pisama). Da bismo to učinili, kreirali smo zasebnu platformu mikroservisa koja takve poruke pohranjuje u svoju pohranu, indeksira ih po atributima (za brzo grupisanje i pretraživanje) i izlaže API za pregled, ponovno slanje na odredišnu adresu i brisanje poruka. Sistem administratori mogu raditi sa ovom uslugom preko svog web interfejsa;
  • u postavkama brokera, morate podesiti broj ponovnih pokušaja isporuke i kašnjenja između isporuka kako biste smanjili vjerovatnoću da poruke uđu u DLQ (gotovo je nemoguće izračunati optimalne parametre, ali možete djelovati empirijski i prilagoditi ih tokom operacija);
  • DLQ skladište treba kontinuirano nadzirati, a sistem za nadzor treba da obavijesti sistemske administratore kako bi mogli odgovoriti što je brže moguće kada se pojave neisporučene poruke. Ovo će smanjiti „zonu oštećenja“ kvara ili greške poslovne logike;
  • integracijska sabirnica mora biti neosjetljiva na privremeni nedostatak aplikacija: pretplate na teme moraju biti trajne, a ime domene aplikacije mora biti jedinstveno tako da neko drugi ne pokušava obraditi njenu poruku iz reda za vrijeme odsustva aplikacije.

Osiguravanje sigurnosti niti poslovne logike

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

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

  • pokretanje instance poslovnog procesa;
  • radnja korisnika vezana za aktivnost unutar poslovnog procesa;
  • prijem poruke ili signala na koji je instanca poslovnog procesa pretplaćena;
  • isteka tajmera postavljenog od strane instance poslovnog procesa;
  • kontrolna radnja putem API-ja (npr. prekid procesa).

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

Trajni podaci poslovnog procesa pohranjeni u relacijskoj bazi podataka su vrlo zgodna točka sinhronizacije obrade kada se koristi SELECT FOR UPDATE. Ako je jedna transakcija uspjela da dobije stanje poslovnog procesa iz baze podataka da ga promijeni, tada nijedna druga paralelna transakcija neće moći dobiti isto stanje za drugu promjenu, a nakon završetka prve transakcije, druga je garantovano primanje već promijenjenog stanja.

Koristeći pesimističke brave na strani DBMS-a, ispunjavamo sve potrebne zahtjeve ACID, a također zadržavaju mogućnost skaliranja aplikacije s poslovnom logikom povećanjem broja pokrenutih instanci.

Međutim, pesimistična zaključavanja nam prijete zastojima, što znači da SELECT FOR UPDATE i dalje treba biti ograničen na neko razumno vremensko ograničenje u slučaju zastoja na nekim eklatantnim slučajevima u poslovnoj logici.

Drugi problem je sinhronizacija početka poslovnog procesa. Iako ne postoji instanca poslovnog procesa, nema ni stanja u bazi podataka, tako da opisana metoda neće raditi. Ako želite da osigurate jedinstvenost instance poslovnog procesa u određenom opsegu, onda vam je potrebna neka vrsta sinkronizacionog objekta koji je povezan sa klasom procesa i odgovarajućim opsegom. Da bismo riješili ovaj problem, koristimo drugačiji mehanizam zaključavanja koji nam omogućava da preuzmemo zaključavanje proizvoljnog resursa određenog ključem u URI formatu putem eksterne usluge.

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

uniqueConstraint = UniqueConstraints.singleton

Dakle, dnevnik sadrži poruke o preuzimanju i otpuštanju brave odgovarajućeg ključa. Ne postoje takve poruke za druge poslovne procese: uniqueConstraint nije postavljen.

Problemi poslovnog procesa sa postojanim stanjem

Ponekad postojano stanje ne samo da pomaže, već i zaista ometa razvoj.
Problemi počinju kada je potrebno izvršiti promjene u poslovnoj logici i/ili modelu poslovnog procesa. Niti jedna takva promjena nije kompatibilna sa starim stanjem poslovnih procesa. Ako postoji mnogo "živih" instanci u bazi podataka, onda unošenje nekompatibilnih izmjena može uzrokovati mnogo problema, na koje se često susrećemo kada koristimo jBPM.

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

  1. kreirajte novi tip poslovnog procesa kako ne biste pravili nekompatibilne promjene na starom i koristite ga umjesto starog pri pokretanju novih instanci. Stare instance će nastaviti da rade "na stari način";
  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:

  • dupliranje poslovne logike u mnogim modelima poslovnih procesa, povećanje obima poslovne logike;
  • često je potreban trenutni prelazak na novu poslovnu logiku (skoro uvijek u smislu zadataka integracije);
  • programer ne zna u kom trenutku je moguće izbrisati zastarjele modele.

U praksi koristimo oba pristupa, ali smo donijeli niz odluka kako bismo si pojednostavili život:

  • u bazi podataka, trajno stanje poslovnog procesa je pohranjeno u lako čitljivom i lako obrađenom obliku: u nizu formata JSON. Ovo vam omogućava da izvršite migracije unutar aplikacije i van nje. U ekstremnim slučajevima, možete ga i podesiti pomoću ručki (posebno korisno u razvoju tokom otklanjanja grešaka);
  • integracijska poslovna logika ne koristi nazive poslovnih procesa, tako da je u svakom trenutku moguće zamijeniti implementaciju jednog od sudjelujućih procesa novim, s novim imenom (na primjer, "InitialPlayerV2"). Vezivanje se dešava kroz imena poruka i signala;
  • model procesa ima broj verzije, koji povećavamo ako napravimo nekompatibilne promjene na ovom modelu, a ovaj broj se pohranjuje zajedno sa stanjem instance procesa;
  • postojano stanje procesa se prvo čita iz baze u prikladan objektni model s kojim procedura migracije može raditi ako se promijenio broj verzije modela;
  • procedura migracije se postavlja uz poslovnu logiku i naziva se "lijenja" za svaku instancu poslovnog procesa u trenutku njegovog vraćanja iz baze podataka;
  • ako trebate brzo i sinhrono migrirati stanje svih instanci procesa, koriste se klasičnija rješenja migracije baze podataka, ali tamo morate raditi s JSON-om.

Da li mi je potreban drugi okvir za poslovne procese?

Rješenja opisana u članku omogućila su nam da značajno pojednostavimo svoje živote, proširimo raspon problema koji se rješavaju na razini razvoja aplikacija i učinimo privlačnijom ideju razdvajanja poslovne logike na mikroservise. Za to je urađeno mnogo posla, kreiran je veoma „lagan“ okvir za poslovne procese, kao i servisne komponente za rješavanje identifikovanih problema u kontekstu širokog spektra primijenjenih zadataka. Imamo želju da podijelimo ove rezultate, da razvoj zajedničkih komponenti stavimo u otvoreni pristup pod slobodnom licencom. Za to će biti potrebno malo truda i vremena. Razumijevanje potražnje za ovakvim rješenjima moglo bi nam biti dodatni poticaj. U predloženom članku vrlo je malo pažnje posvećeno mogućnostima samog okvira, ali su neke od njih vidljive iz prikazanih primjera. Ako ipak objavimo naš okvir, biće mu posvećen poseban članak. U međuvremenu, bit ćemo vam zahvalni ako ostavite malu povratnu informaciju tako što ćete odgovoriti na pitanje:

Samo registrovani korisnici mogu učestvovati u anketi. Prijavite semolim.

Da li mi je potreban drugi okvir za poslovne procese?

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

  • 12,5%zanimljivo je saznati više o vašoj implementaciji, može biti korisno2

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

  • 18,8%koristimo jedan od postojećih okvira, sve odgovara3

  • 18,8%snalaženje bez okvira3

  • 25,0%napišite svoje 4

Glasalo je 16 korisnika. Uzdržano je bilo 7 korisnika.

izvor: www.habr.com

Dodajte komentar