BPM-stiili integreerimine

BPM-stiili integreerimine

Tere, Habr!

Meie ettevõte on spetsialiseerunud ERP-klassi tarkvaralahenduste arendamisele, milles lõviosa hõivavad tohutu äriloogika ja töövooga a la EDMS tehingusüsteemid. Meie toodete kaasaegsed versioonid põhinevad JavaEE tehnoloogiatel, kuid katsetame aktiivselt ka mikroteenustega. Selliste lahenduste üks probleemsemaid valdkondi on erinevate külgnevate domeenidega seotud alamsüsteemide integreerimine. Integratsiooniülesanded on meile alati tohutut peavalu valmistanud, olenemata kasutatavatest arhitektuuristiilidest, tehnoloogiapakkidest ja raamistikest, kuid viimasel ajal on selliste probleemide lahendamisel edusamme tehtud.

Teie tähelepanu alla juhitud artiklis räägin MTÜ Krista kogemustest ja arhitektuursetest uuringutest selleks ettenähtud piirkonnas. Vaatleme ka näidet integratsiooniprobleemi lihtsast lahendusest rakenduste arendaja vaatevinklist ja uurime, mis selle lihtsuse taga on.

Vastutusest loobumine

Artiklis kirjeldatud arhitektuursed ja tehnilised lahendused olen pakutud isiklikule kogemusele tuginedes konkreetsete ülesannete kontekstis. Need lahendused ei pretendeeri universaalsusele ega pruugi olla optimaalsed teistes kasutustingimustes.

Mis BPM-il sellega pistmist on?

Sellele küsimusele vastamiseks peame veidi süvenema meie lahenduste rakendusprobleemide eripäradesse. Äriloogika põhiosa meie tüüpilises tehingusüsteemis on andmete sisestamine andmebaasi kasutajaliideste kaudu, nende andmete käsitsi ja automaatne kontrollimine, mingi töövoo läbimine, teise süsteemi / analüütilisse andmebaasi / arhiivi avaldamine, aruannete genereerimine. Seega on süsteemi põhifunktsioon klientide jaoks nende sisemiste äriprotsesside automatiseerimine.

Mugavuse huvides kasutame mõistet "dokument" suhtluses mingi andmekogumi abstraktsioonina, mida ühendab ühine võti, millele saab "kinnitada" konkreetse töövoo.
Aga kuidas on lood integratsiooniloogikaga? Lõppkokkuvõttes genereerib integreerimise ülesande süsteemi arhitektuur, mis "saagitakse" osadeks MITTE kliendi soovil, vaid täiesti erinevate tegurite mõjul:

  • Conway seaduse mõjul;
  • varem muude toodete jaoks välja töötatud alamsüsteemide taaskasutamise tulemusena;
  • nagu arhitekt otsustas, lähtudes mittefunktsionaalsetest nõuetest.

On suur kiusatus eraldada integratsiooniloogika põhitöövoo äriloogikast, et mitte reostada äriloogikat integratsiooniartefaktidega ja säästa rakenduste arendajat süsteemi arhitektuurilise maastiku iseärasustesse süvenemisest. Sellel lähenemisviisil on mitmeid eeliseid, kuid praktika näitab selle ebaefektiivsust:

  • integratsiooniprobleemide lahendamine libiseb reeglina alla kõige lihtsamate valikuteni sünkroonkõnede näol, kuna laienduspunktid on põhitöövoo realiseerimisel piiratud (sünkroonse integreerimise puudustest lähemalt allpool);
  • integratsiooni artefaktid tungivad endiselt peamisse äriloogikasse, kui on vaja tagasisidet teisest alamsüsteemist;
  • rakenduse arendaja ignoreerib integratsiooni ja võib selle töövoogu muutes kergesti katkestada;
  • süsteem lakkab olemast ühtne tervik kasutaja seisukohalt, alamsüsteemidevahelised "õmblused" muutuvad märgatavaks, ilmnevad üleliigsed kasutajatoimingud, mis algatavad andmete ülekandmise ühest alamsüsteemist teise.

Teine lähenemisviis on käsitleda integratsiooni interaktsioone põhilise äriloogika ja töövoo lahutamatu osana. Selleks, et rakenduste arendajate oskuste nõuded ei tõuseks hüppeliselt, tuleks uute integreerimisinteraktsioonide loomine toimuda lihtsalt ja loomulikult ning lahenduse valikul on minimaalsed võimalused. See on keerulisem, kui pealtnäha paistab: tööriist peab olema piisavalt võimas, et pakkuda kasutajale selle kasutamiseks vajalikke erinevaid võimalusi ja samal ajal mitte lasta endale jalga lasta. On palju küsimusi, millele insener peab integratsiooniülesannete kontekstis vastama, kuid millele rakenduste arendaja ei peaks oma igapäevatöös mõtlema: tehingupiirid, järjepidevus, atomaalsus, turvalisus, skaleerimine, koormuse ja ressursside jaotus, marsruutimine, jaotus, levitamise ja vahetamise kontekstid jne. Rakenduste arendajatele on vaja pakkuda üsna lihtsaid otsustusmalle, milles on vastused kõikidele sellistele küsimustele juba peidus. Need mustrid peaksid olema piisavalt turvalised: äriloogika muutub väga sageli, mis suurendab vigade sissetoomise riski, vigade maksumus peaks jääma üsna madalale tasemele.

Aga ikkagi, mis BPM-il sellega pistmist on? Töövoo rakendamiseks on palju võimalusi ...
Tõepoolest, meie lahendustes on väga populaarne veel üks äriprotsesside teostus - oleku ülemineku diagrammi deklaratiivse seadistamise ja äriloogikaga töötlejate ühendamise kaudu üleminekutega. Samal ajal on olek, mis määrab "dokumendi" hetkepositsiooni äriprotsessis, "dokumendi" enda atribuudiks.

BPM-stiili integreerimine
Nii näeb protsess välja projekti alguses

Sellise teostuse populaarsus on tingitud lineaarsete äriprotsesside loomise suhtelisest lihtsusest ja kiirusest. Kuna aga tarkvarasüsteemid muutuvad keerukamaks, kasvab ja muutub keerukamaks äriprotsessi automatiseeritud osa. Vaja on lagundamist, protsesside osade taaskasutamist, samuti protsesside harutamist, et iga haru toimuks paralleelselt. Sellistel tingimustel muutub tööriist ebamugavaks ja oleku ülemineku diagramm kaotab oma teabesisu (integratsiooni interaktsioonid ei kajastu diagrammil üldse).

BPM-stiili integreerimine
Selline näeb protsess välja pärast mitut nõuete selgitamist

Väljapääs sellest olukorrast oli mootori integreerimine jBPM mõnesse kõige keerukamate äriprotsessidega toodetesse. Lühiajalises perspektiivis oli sellel lahendusel mõningane edu: sai võimalikuks keerukate äriprotsesside juurutamine, säilitades märkmetes üsna informatiivse ja ajakohase diagrammi. BPMN2.

BPM-stiili integreerimine
Väike osa keerulisest äriprotsessist

Pikemas perspektiivis ei vastanud lahendus ootustele: visuaalsete tööriistade abil äriprotsesside loomise suur töömahukus ei võimaldanud saavutada vastuvõetavaid tootlikkuse näitajaid ning tööriist ise muutus arendajate seas üheks ebameeldivamaks. Samuti esitati kaebusi mootori sisemise struktuuri kohta, mis tõi kaasa paljude “plaastrite” ja “karkude” ilmumise.

jBPM-i kasutamise peamine positiivne aspekt oli äriprotsessi eksemplari jaoks oma püsiva oleku eeliste ja kahjude mõistmine. Samuti nägime võimalust kasutada protsessipõhist lähenemist keerukate integratsiooniprotokollide rakendamiseks erinevate rakenduste vahel, kasutades signaalide ja sõnumite kaudu asünkroonset interaktsiooni. Püsiva seisundi olemasolu mängib selles otsustavat rolli.

Ülaltoodu põhjal võime järeldada: BPM-stiilis protsessikäsitlus võimaldab lahendada väga erinevaid ülesandeid üha keerukamate äriprotsesside automatiseerimiseks, integreerimistegevused nendesse protsessidesse harmooniliselt sobitada ning säilitada võimaluse realiseeritud protsessi visuaalselt sobivas notatsioonis kuvada.

Sünkroonkõnede kui integratsioonimustri miinused

Sünkroonne integreerimine viitab lihtsaimale blokeerivale kõnele. Üks alamsüsteem toimib serveri poolena ja paljastab API soovitud meetodiga. Teine alamsüsteem toimib kliendi poolena ja helistab õigel ajal tulemuse ootusega. Olenevalt süsteemi arhitektuurist saab kliendi- ja serveripoolt majutada kas samas rakenduses ja protsessis või erinevates. Teisel juhul peate rakendama RPC-d ja pakkuma parameetrite ja kõne tulemuse järjestamist.

BPM-stiili integreerimine

Sellisel integreerimismustril on üsna suur hulk puudusi, kuid praktikas kasutatakse seda oma lihtsuse tõttu väga laialdaselt. Teostamise kiirus köidab ja paneb seda ikka ja jälle rakendama "põlevate" tähtaegade tingimustes, kirjutades lahenduse tehnilisse võlga. Kuid juhtub ka, et kogenematud arendajad kasutavad seda alateadlikult, lihtsalt ei mõista negatiivseid tagajärgi.

Lisaks alamsüsteemide ühenduvuse kõige ilmsemale suurenemisele on vähem ilmseid probleeme "levitavate" ja "venitavate" tehingutega. Tõepoolest, kui äriloogika teeb mingeid muudatusi, on tehingud hädavajalikud ja tehingud omakorda lukustavad teatud rakendusressursid, mida need muudatused mõjutavad. See tähendab, et kuni üks alamsüsteem ei oota teiselt vastust, ei saa see tehingut lõpule viia ega lukke vabastada. See suurendab märkimisväärselt mitmesuguste tagajärgede riski:

  • süsteemi reageerimisvõime kaob, kasutajad ootavad päringutele vastuseid kaua;
  • server lõpetab üldiselt kasutajate päringutele vastamise ületäitunud lõimekogumi tõttu: enamik lõimedest "seisab" tehingu poolt hõivatud ressursi lukus;
  • hakkavad tekkima ummikseisud: nende tekkimise tõenäosus sõltub tugevalt tehingute kestusest, äriloogika ja tehinguga seotud lukkude mahust;
  • ilmuvad tehingu ajalõpu aegumise vead;
  • server "kukkub" OutOfMemoryle, kui ülesanne nõuab suurte andmemahtude töötlemist ja muutmist ning sünkroonsete integratsioonide olemasolu muudab töötlemise "kergemateks" tehinguteks jagamise väga keeruliseks.

Arhitektuurilisest vaatenurgast viib blokeerimiskõnede kasutamine integreerimise ajal üksikute alamsüsteemide kvaliteedikontrolli kadumiseni: ühe allsüsteemi kvaliteedieesmärke on võimatu tagada eraldi teise allsüsteemi kvaliteedieesmärkidest. Kui alamsüsteeme arendavad erinevad meeskonnad, on see suur probleem.

Asi läheb veelgi huvitavamaks, kui integreeritavad alamsüsteemid on erinevates rakendustes ja mõlemal poolel on vaja teha sünkroonseid muudatusi. Kuidas muuta need muudatused tehinguliseks?

Kui muudatusi tehakse eraldi tehingutes, siis tuleb tagada robustne erandite käsitlemine ja kompenseerimine ning see välistab täielikult sünkroonsete integratsioonide peamise eelise - lihtsuse.

Meenuvad ka hajutatud tehingud, kuid me ei kasuta neid oma lahendustes: usaldusväärsust on raske tagada.

"Saaga" kui lahendus tehingute probleemile

Mikroteenuste populaarsuse kasvuga suureneb nõudlus Saaga muster.

See muster lahendab suurepäraselt ülaltoodud pikkade tehingute probleemid ja avardab ka võimalusi süsteemi oleku juhtimiseks äriloogika poolelt: ebaõnnestunud tehingu järgselt makstav hüvitis ei pruugi süsteemi algolekusse tagasi viia, vaid pakkuda alternatiivi. andmetöötluse marsruut. Samuti võimaldab see mitte korrata edukalt lõpetatud andmetöötlusetappe, kui proovite protsessi "hea" lõpuni viia.

Huvitav on see, et monoliitsetes süsteemides on see muster asjakohane ka lõdvalt seotud alamsüsteemide integreerimisel ning pikad tehingud ja vastavad ressursilukud põhjustavad negatiivseid mõjusid.

Mis puutub meie BPM-stiilis äriprotsessidesse, siis on Sagade juurutamine väga lihtne: Sagade üksikuid samme saab määrata tegevusteks äriprotsessi sees ning äriprotsessi püsiv olek määrab muuhulgas , Saagade sisemine olek. See tähendab, et me ei vaja täiendavat koordineerimismehhanismi. Kõik, mida vajate, on sõnumivahendaja, millel on transpordiks "vähemalt ühekordne" garantii.

Kuid sellisel lahendusel on ka oma "hind":

  • äriloogika muutub keerulisemaks: tuleb välja töötada hüvitis;
  • tuleb loobuda täielikust järjepidevusest, mis võib olla eriti tundlik monoliitsete süsteemide puhul;
  • arhitektuur muutub veidi keerulisemaks, tekib täiendav vajadus sõnumivahendaja järele;
  • vaja on täiendavaid jälgimis- ja haldustööriistu (kuigi üldiselt on see isegi hea: süsteemiteenuse kvaliteet tõuseb).

Monoliitsete süsteemide puhul pole "Sags" kasutamise õigustus nii ilmne. Mikroteenuste ja muude SOA-de puhul, kus suure tõenäosusega on maakler juba olemas ja projekti alguses ohverdati täielik järjepidevus, võivad selle mustri kasutamise eelised oluliselt üles kaaluda puudused, eriti kui rakenduses on mugav API äriloogika tase.

Äriloogika kapseldamine mikroteenustesse

Kui hakkasime mikroteenustega katsetama, tekkis mõistlik küsimus: kuhu panna domeeni äriloogika seoses domeeniandmete püsivust tagava teenusega?

Erinevate BPMS-ide arhitektuuri vaadates võib tunduda mõistlik eraldada äriloogika püsivusest: luua platvormist ja domeenist sõltumatute mikroteenuste kiht, mis moodustavad domeeni äriloogika täitmiseks keskkonna ja konteineri, ning korraldada domeeni andmete püsivus eraldiseisvana. kiht väga lihtsaid ja kergeid mikroteenuseid. Äriprotsessid korraldavad sel juhul püsivuskihi teenuseid.

BPM-stiili integreerimine

Sellel lähenemisel on väga suur pluss: platvormi funktsionaalsust saad suurendada nii palju kui soovid ja sellest “paksub” vaid vastav platvormi mikroteenuste kiht. Mis tahes domeeni äriprotsessid saavad kohe võimaluse kasutada platvormi uut funktsionaalsust kohe pärast selle värskendamist.

Üksikasjalikum uuring näitas selle lähenemisviisi olulisi puudusi:

  • platvormteenus, mis rakendab korraga paljude domeenide äriloogikat, kannab endas suuri riske ühe tõrkepunktina. Äriloogika sagedased muudatused suurendavad vigade ohtu, mis põhjustavad kogu süsteemi tõrkeid;
  • jõudlusprobleemid: äriloogika töötab oma andmetega kitsa ja aeglase liidese kaudu:
    • andmed järjestatakse ja pumbatakse uuesti läbi võrgupinu;
    • domeeniteenus tagastab sageli rohkem andmeid, kui äriloogika töötlemiseks nõuab, kuna teenuse välise API tasemel on ebapiisavad päringu parameetrite määramise võimalused;
    • mitmed sõltumatud äriloogika osad võivad samu andmeid korduvalt töötlemiseks uuesti taotleda (saate seda probleemi leevendada, lisades andmeid vahemällu salvestavaid seansi ube, kuid see muudab arhitektuuri veelgi keerulisemaks ning tekitab andmete värskuse ja vahemälu kehtetuks tunnistamise probleeme);
  • tehinguga seotud probleemid:
    • platvormiteenuse salvestatud püsiva olekuga äriprotsessid ei ole domeeniandmetega kooskõlas ja selle probleemi lahendamiseks pole lihtsaid viise;
    • domeeniandmete lukustuse teisaldamine tehingust välja: kui domeeni äriloogikas on vaja teha muudatusi, tuleb pärast esmast tegelike andmete õigsuse kontrollimist välistada töödeldavate andmete konkurentsimuutuse võimalus. Andmete väline blokeerimine võib aidata probleemi lahendada, kuid selline lahendus toob endaga kaasa lisariske ja vähendab süsteemi üldist töökindlust;
  • täiendavad komplikatsioonid värskendamisel: mõnel juhul peate uuendama püsivusteenust ja äriloogikat sünkroonselt või ranges järjekorras.

Lõpuks pidin minema tagasi põhitõdede juurde: kapseldama domeeni andmed ja domeeni äriloogika ühte mikroteenusesse. Selline lähenemine lihtsustab mikroteenuse kui süsteemi lahutamatu komponendi tajumist ega tekita ülaltoodud probleeme. See pole ka tasuta:

  • API standardimine on vajalik interaktsiooniks äriloogikaga (eelkõige kasutajate tegevuste pakkumiseks äriprotsesside osana) ja API platvormiteenustega; API muudatustele, edasi- ja tagasiühilduvusele on vaja rohkem tähelepanu pöörata;
  • iga sellise mikroteenuse osana on vaja lisada täiendavaid käitusaegseid teeke, et tagada äriloogika toimimine, ja see toob kaasa uued nõuded sellistele teekidele: kergus ja minimaalne transitiivne sõltuvus;
  • äriloogika arendajad peavad jälgima teegi versioone: kui mikroteenust pole pikka aega lõpetatud, sisaldab see tõenäoliselt teekide aegunud versioone. See võib olla ootamatu takistus uue funktsiooni lisamisel ja võib nõuda sellise teenuse vana äriloogika üleviimist teekide uutele versioonidele, kui versioonide vahel toimusid kokkusobimatud muudatused.

BPM-stiili integreerimine

Sellises arhitektuuris on ka platvormiteenuste kiht, kuid see kiht ei moodusta enam konteinerit domeeni äriloogika täitmiseks, vaid ainult selle keskkonda, pakkudes "platvormi" abifunktsioone. Sellist kihti pole vaja mitte ainult domeeni mikroteenuste kerguse säilitamiseks, vaid ka halduse tsentraliseerimiseks.

Näiteks kasutajate tegevused äriprotsessides genereerivad ülesandeid. Ülesannetega töötades peab kasutaja aga nägema ülesandeid kõigist domeenidest üldnimekirjas, mis tähendab, et seal peab olema vastav, domeeni äriloogikast puhastatud ülesannete registreerimisplatvormi teenus. Äriloogika kapseldamine selles kontekstis on üsna problemaatiline ja see on veel üks selle arhitektuuri kompromiss.

Äriprotsesside integreerimine rakenduste arendaja pilgu läbi

Nagu eespool juba mainitud, peab rakenduste arendaja olema abstraheeritud mitme rakenduse koostoime rakendamise tehnilistest ja insenertehnilistest omadustest, et saaks loota heale arendustootlikkusele.

Proovime lahendada üsna keerulise integreerimisprobleemi, mis on spetsiaalselt artikli jaoks leiutatud. See on "mängu" ülesanne, mis hõlmab kolme rakendust, kus igaüks neist määratleb mõne domeeninime: "app1", "app2", "app3".

Iga rakenduse sees käivitatakse äriprotsessid, mis hakkavad integratsioonisiini kaudu "palli mängima". Kirjad nimega "Pall" toimivad pallina.

Mängu reeglid:

  • esimene mängija on algataja. Ta kutsub teisi mängijaid mängu, alustab mängu ja võib selle igal ajal lõpetada;
  • teised mängijad deklareerivad oma osalemist mängus, "tutvuvad" üksteise ja esimese mängijaga;
  • pärast palli saamist valib mängija teise osaleva mängija ja söödab palli talle. Loendatakse läbimiste koguarv;
  • igal mängijal on "energia", mis väheneb selle mängija iga palli sööduga. Kui energia saab otsa, eemaldatakse mängija mängust, teatades oma pensionile jäämisest;
  • kui mängija jäetakse üksi, teatab ta kohe oma lahkumisest;
  • kui kõik mängijad langevad välja, kuulutab esimene mängija mängu lõpu. Kui ta lahkus mängust varem, siis tuleb mängu lõpuleviimiseks jälgida.

Selle probleemi lahendamiseks kasutan meie äriprotsesside jaoks mõeldud DSL-i, mis võimaldab kirjeldada Kotlini loogikat kompaktselt ja minimaalselt.

Rakenduses app1 töötab esimese mängija (ta on ka mängu algataja) äriprotsess:

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

Lisaks äriloogika täitmisele võib ülaltoodud kood luua äriprotsessi objektmudeli, mida saab diagrammina visualiseerida. Me ei ole visualiseerijat veel juurutanud, seega pidime natuke aega kulutama joonistamisele (siinkohal lihtsustasin veidi BPMN-i tähistust väravate kasutamise kohta, et parandada diagrammi kooskõla ülaltoodud koodiga):

BPM-stiili integreerimine

app2 sisaldab teise mängija äriprotsessi:

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

Diagramm:

BPM-stiili integreerimine

Rakenduses app3 muudame mängija pisut teistsuguse käitumisega: selle asemel, et juhuslikult järgmist mängijat valida, tegutseb ta ring-robin algoritmi järgi:

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

Vastasel juhul ei erine mängija käitumine eelmisest, seega diagramm ei muutu.

Nüüd vajame selle kõige käivitamiseks testi. Annan ainult testi enda koodi, et artiklit mitte risustada (tegelikult kasutasin teiste äriprotsesside integreerimise testimiseks varem loodud testkeskkonda):

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;
}

Käivitage test, vaadake logi:

konsooli väljund

Взята блокировка ключа 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!

Sellest kõigest saab teha mitmeid olulisi järeldusi:

  • vajalike tööriistade olemasolul saavad rakenduste arendajad luua rakenduste vahel integratsiooniinteraktsioone ilma äriloogikast lahti murdmata;
  • inseneripädevusi nõudva integreerimisülesande keerukus (keerukus) võib peituda raamistiku sees, kui see on algselt raamistiku arhitektuuris sätestatud. Ülesande raskusastet (raskusastet) ei saa varjata, seega näeb keerulise ülesande lahendus koodis välja vastavalt;
  • lõimumisloogika väljatöötamisel tuleb arvestada lõpuks kõigi lõimumisel osalejate olekumuutuse järjepidevuse ja lineariseeritavuse puudumisega. See sunnib meid loogikat keerulisemaks muutma, et muuta see väliste sündmuste toimumise järjekorra suhtes tundetuks. Meie näites on mängija sunnitud mängust osa võtma pärast seda, kui ta on teatanud oma mängust lahkumisest: teised mängijad jätkavad talle palli söötmist, kuni teave tema väljumise kohta jõuab ja kõik osalejad seda töötlevad. Selline loogika ei tulene mängureeglitest ja on kompromisslahendus valitud arhitektuuri raames.

Järgmisena räägime meie lahenduse erinevatest nüanssidest, kompromissidest ja muudest punktidest.

Kõik kirjad ühes järjekorras

Kõik integreeritud rakendused töötavad ühe integratsioonisiiniga, mis on esindatud välise maaklerina, ühe BPMQueue'iga sõnumite jaoks ja ühe BPMTopici teemaga signaalide (sündmuste) jaoks. Kõigi sõnumite ühe järjekorra kaudu saatmine on iseenesest kompromiss. Äriloogika tasandil saate nüüd kasutusele võtta nii palju uut tüüpi sõnumeid, kui soovite, ilma süsteemi struktuuri muutmata. See on märkimisväärne lihtsustus, kuid sellega kaasnevad teatud riskid, mis meie tüüpiliste ülesannete kontekstis ei tundunud meile nii olulised.

BPM-stiili integreerimine

Siin on aga üks nüanss: iga rakendus filtreerib oma domeeni nime järgi sissepääsu järjekorras olevast "oma" sõnumid. Samuti saab signaalides määrata domeeni, kui on vaja piirata signaali “ulatust” ühe rakendusega. See peaks suurendama siini ribalaiust, kuid äriloogika peab nüüd toimima domeeninimedega: sõnumite adresseerimisel kohustuslik, signaalide puhul soovitav.

Integratsioonisiini töökindluse tagamine

Usaldusväärsus koosneb mitmest asjast:

  • Valitud sõnumivahendaja on arhitektuuri kriitiline komponent ja üksainus tõrkepunkt: see peab olema piisavalt tõrketaluv. Peaksite kasutama ainult ajaproovitud rakendusi, millel on hea tugi ja suur kogukond;
  • on vaja tagada sõnumivahendaja kõrge kättesaadavus, mille jaoks tuleb see integreeritud rakendustest füüsiliselt eraldada (rakendusliku äriloogikaga rakenduste kõrget kättesaadavust on palju keerulisem ja kulukam pakkuda);
  • maakler on kohustatud andma "vähemalt ühekordse" tarnetagatise. See on kohustuslik nõue integreerimissiini usaldusväärseks tööks. "Täpselt üks kord" tasemel garantiisid pole vaja: äriprotsessid ei ole enamasti tundlikud sõnumite või sündmuste korduva saabumise suhtes ning eriülesannetes, kus see on oluline, on lihtsam lisada äriloogikasse täiendavaid kontrolle kui pidevalt kasutada. pigem "kallid" " garantiid;
  • sõnumite ja signaalide saatmine peab olema kaasatud ühisesse tehingusse äriprotsesside ja domeeniandmete oleku muutumisega. Eelistatud variant oleks kasutada mustrit Tehingu väljundkaust, kuid see nõuab täiendavat tabelit andmebaasis ja releed. JEE rakendustes saab seda lihtsustada kohaliku JTA halduri abil, kuid ühendus valitud maakleriga peab töötama režiimis XA;
  • äriprotsessi oleku muutmise tehinguga peavad töötama ka sissetulevate sõnumite ja sündmuste käitlejad: kui selline tehing tagasi keeratakse, siis tuleb tühistada ka teate vastuvõtmine;
  • sõnumid, mida ei saanud vigade tõttu kohale toimetada, tuleks hoida eraldi poes D.L.Q. (Surnud kirjade järjekord). Selleks lõime eraldi platvormi mikroteenuse, mis salvestab sellised sõnumid oma salvestusruumi, indekseerib need atribuutide järgi (kiireks rühmitamiseks ja otsimiseks) ning avab API vaatamiseks, sihtaadressile uuesti saatmiseks ja sõnumite kustutamiseks. Süsteemiadministraatorid saavad selle teenusega töötada oma veebiliidese kaudu;
  • maakleri seadetes tuleb reguleerida tarnekorduste arvu ja tarnetevahelisi viivitusi, et vähendada tõenäosust, et teated jõuavad DLQ-sse (optimaalseid parameetreid on peaaegu võimatu välja arvutada, kuid saate tegutseda empiiriliselt ja neid reguleerida operatsioon);
  • DLQ-poodi tuleks pidevalt jälgida ja seiresüsteem peaks teavitama süsteemiadministraatoreid, et nad saaksid edastamata sõnumite ilmnemisel võimalikult kiiresti reageerida. See vähendab tõrke või äriloogika vea "kahjustustsooni";
  • integratsioonisiin peab olema tundetu rakenduste ajutise puudumise suhtes: teematellimused peavad olema vastupidavad ja rakenduse domeeninimi peab olema kordumatu, et keegi teine ​​ei üritaks rakenduse puudumisel selle sõnumit järjekorrast töödelda.

Äriloogika lõime ohutuse tagamine

Äriprotsessi sama eksemplar võib korraga vastu võtta mitu sõnumit ja sündmust, mille töötlemine algab paralleelselt. Samas peaks rakenduste arendaja jaoks kõik olema lihtne ja lõimekindel.

Protsessi äriloogika töötleb iga välist sündmust, mis seda äriprotsessi mõjutab, eraldi. Need sündmused võivad olla:

  • äriprotsessi eksemplari käivitamine;
  • äriprotsessis toimuva tegevusega seotud kasutaja toiming;
  • sõnumi või signaali vastuvõtmine, millele äriprotsessi eksemplar on tellitud;
  • äriprotsessi eksemplari seatud taimeri aegumine;
  • juhttoimingud API kaudu (nt protsessi katkestamine).

Iga selline sündmus võib muuta äriprotsessi eksemplari olekut: mõned tegevused võivad lõppeda ja teised alata, püsivate omaduste väärtused võivad muutuda. Mis tahes tegevuse sulgemine võib kaasa tuua ühe või mitme järgmise tegevuse aktiveerimise. Need omakorda võivad lõpetada teiste sündmuste ootamise või kui nad täiendavaid andmeid ei vaja, saavad nad sooritada sama tehingu. Enne tehingu sulgemist salvestatakse äriprotsessi uus olek andmebaasi, kus see jääb ootama järgmist välist sündmust.

Relatsiooniandmebaasi salvestatud püsivad äriprotsesside andmed on väga mugav töötlemise sünkroonimispunkt, kui kasutate valikut SELECT FOR UPDATE. Kui ühel tehingul õnnestus saada andmebaasist äriprotsessi olek selle muutmiseks, siis ükski teine ​​paralleelne tehing ei saa teise muudatuse jaoks sama seisundit ja pärast esimese tehingu sooritamist on teine. garanteeritud juba muutunud oleku saamine.

Kasutades DBMS-i poolel pessimistlikke lukke, täidame kõik vajalikud nõuded ACID, ja säilitavad ka võimaluse skaleerida rakendust äriloogikaga, suurendades töötavate eksemplaride arvu.

Pessimistlikud lukud ähvardavad meid aga ummikseisudega, mis tähendab, et SELECT FOR UPDATE peaks siiski piirduma mõne mõistliku ajalõpuga juhuks, kui mõnel äriloogikas silmatorkaval juhul ummikusse sattuda.

Teine probleem on äriprotsessi alguse sünkroonimine. Kuigi äriprotsessi eksemplari pole, pole ka andmebaasis olekut, seega kirjeldatud meetod ei tööta. Kui soovite tagada äriprotsessi eksemplari unikaalsust konkreetses ulatuses, on teil vaja protsessiklassi ja vastava ulatusega seotud mingit sünkroonimisobjekti. Selle probleemi lahendamiseks kasutame teistsugust lukustusmehhanismi, mis võimaldab meil välisteenuse kaudu lukustada URI-vormingus võtmega määratud suvalise ressursi.

Meie näidetes sisaldab InitialPlayeri äriprotsess deklaratsiooni

uniqueConstraint = UniqueConstraints.singleton

Seetõttu sisaldab logi teateid vastava võtme luku võtmise ja vabastamise kohta. Teiste äriprotsesside jaoks selliseid sõnumeid pole: unikaalne piirang pole määratud.

Püsiva olekuga seotud äriprotsesside probleemid

Vahel püsiv seisund mitte ainult ei aita, vaid ka tõesti takistab arengut.
Probleemid algavad siis, kui on vaja teha muudatusi äriloogikas ja/või äriprotsessi mudelis. Ükski selline muudatus ei sobi kokku äriprotsesside vana olekuga. Kui andmebaasis on palju "reaalajas" eksemplare, võib kokkusobimatute muudatuste tegemine põhjustada palju probleeme, millega jBPM-i kasutamisel sageli kokku puutusime.

Olenevalt muutuse sügavusest saate tegutseda kahel viisil.

  1. looge uus äriprotsessi tüüp, et mitte teha kokkusobimatuid muudatusi vanasse, ja kasutage seda uute eksemplaride käivitamisel vana asemel. Vanad eksemplarid jätkavad tööd "vanal viisil";
  2. äriloogika värskendamisel äriprotsesside püsivat seisundit migreerida.

Esimene viis on lihtsam, kuid sellel on oma piirangud ja puudused, näiteks:

  • äriloogika dubleerimine paljudes äriprotsessimudelites, äriloogika mahu suurenemine;
  • sageli on vaja kohest üleminekut uuele äriloogikale (peaaegu alati integreerimisülesannete osas);
  • arendaja ei tea, mis hetkel on võimalik aegunud mudeleid kustutada.

Praktikas kasutame mõlemat lähenemisviisi, kuid oleme oma elu lihtsustamiseks teinud mitmeid otsuseid:

  • andmebaasis salvestatakse äriprotsessi püsiv olek kergesti loetaval ja hõlpsasti töödeldaval kujul: JSON-vormingus stringis. See võimaldab teil migreeruda nii rakenduse sees kui ka väljaspool. Äärmuslikel juhtudel saate seda ka käepidemetega näpistada (eriti kasulik arenduses silumise ajal);
  • integratsiooni äriloogika ei kasuta äriprotsesside nimetusi, nii et igal ajal on võimalik ühe osaleva protsessi juurutamine asendada uuega, uue nimega (näiteks "InitialPlayerV2"). Sidumine toimub sõnumite ja signaalide nimede kaudu;
  • protsessimudelil on versiooninumber, mida me suurendame, kui teeme selles mudelis mitteühilduvaid muudatusi, ja see number salvestatakse koos protsessi eksemplari olekuga;
  • protsessi püsiv olek loetakse esmalt baasist mugavasse objektmudelisse, millega saab migratsiooniprotseduur töötada, kui mudeli versiooninumber on muutunud;
  • migratsiooniprotseduur asetatakse äriloogika kõrvale ja seda nimetatakse iga äriprotsessi eksemplari jaoks "laisaks" selle andmebaasist taastamise ajal;
  • kui on vaja kiiresti ja sünkroonselt migreerida kõigi protsessieksemplaride olekut, kasutatakse klassikalisemaid andmebaasi migratsiooni lahendusi, kuid seal tuleb töötada JSON-iga.

Kas ma vajan äriprotsesside jaoks teist raamistikku?

Artiklis kirjeldatud lahendused võimaldasid meil oluliselt lihtsustada oma elu, laiendada rakenduste arenduse tasemel lahendatavate probleemide ringi ja muuta äriloogika mikroteenusteks eraldamise ideed atraktiivsemaks. Selleks on tehtud palju tööd, loodud väga “kerge” äriprotsesside raamistik, samuti teenusekomponendid tuvastatud probleemide lahendamiseks paljude rakenduslike ülesannete kontekstis. Meil on soov neid tulemusi jagada, tuua ühiskomponentide arendus vaba litsentsi alla avatud juurdepääsuks. See nõuab pingutust ja aega. Selliste lahenduste nõudluse mõistmine võiks olla meile lisastiimuliks. Kavandatavas artiklis pööratakse väga vähe tähelepanu raamistiku enda võimalustele, kuid mõned neist on esitatud näidete põhjal nähtavad. Kui me siiski oma raamistiku avaldame, pühendatakse sellele eraldi artikkel. Seniks oleme tänulikud, kui jätate veidi tagasisidet, vastates küsimusele:

Küsitluses saavad osaleda ainult registreerunud kasutajad. Logi sissepalun.

Kas ma vajan äriprotsesside jaoks teist raamistikku?

  • 18,8%Jah, ma olen midagi sellist juba pikka aega otsinud.

  • 12,5%huvitav on teie rakenduse kohta lisateavet, see võib olla kasulik2

  • 6,2%kasutame ühte olemasolevatest raamistikest, kuid mõtleme selle väljavahetamisele1

  • 18,8%kasutame üht olemasolevatest raamistikest, kõik sobib3

  • 18,8%toimetulek ilma raamistikuta3

  • 25,0%kirjuta oma 4

16 kasutajat hääletas. 7 kasutajat jäi erapooletuks.

Allikas: www.habr.com

Lisa kommentaar