Mūsų įmonė specializuojasi kuriant ERP klasės programinės įrangos sprendimus, kuriuose liūto dalį užima transakcinės sistemos, turinčios didžiulę verslo logiką ir darbo eigą a la EDMS. Šiuolaikinės mūsų produktų versijos yra pagrįstos JavaEE technologijomis, tačiau aktyviai eksperimentuojame ir su mikropaslaugomis. Viena iš problemiškiausių tokių sprendimų sričių yra įvairių posistemių, susijusių su gretimais domenais, integravimas. Integravimo užduotys mums visada kėlė didžiulį galvos skausmą, nepaisant mūsų naudojamų architektūrinių stilių, technologijų krūvų ir karkasų, tačiau pastaruoju metu buvo padaryta pažanga sprendžiant tokias problemas.
Jūsų dėmesiui skirtame straipsnyje kalbėsiu apie NPO Krista patirtį ir architektūrinius tyrimus nurodytoje teritorijoje. Taip pat apsvarstysime paprasto integravimo problemos sprendimo pavyzdį programų kūrėjo požiūriu ir išsiaiškinsime, kas slypi už šio paprastumo.
Atsakomybės apribojimas
Straipsnyje aprašytus architektūrinius ir techninius sprendimus siūlau remdamasis asmenine patirtimi konkrečių užduočių kontekste. Šie sprendimai nepretenduoja į universalumą ir gali būti neoptimalūs kitomis naudojimo sąlygomis.
Ką su tuo turi BPM?
Norėdami atsakyti į šį klausimą, turime šiek tiek įsigilinti į mūsų sprendimų taikomų problemų specifiką. Pagrindinė verslo logikos dalis mūsų tipinėje operacijų sistemoje yra duomenų įvedimas į duomenų bazę per vartotojo sąsajas, šių duomenų rankinis ir automatinis tikrinimas, perdavimas per tam tikrą darbo eigą, publikavimas į kitą sistemą / analitinę duomenų bazę / archyvą, ataskaitų generavimas. Taigi pagrindinė sistemos funkcija klientams yra jų vidinių verslo procesų automatizavimas.
Patogumo dėlei terminą „dokumentas“ komunikacijoje vartojame kaip tam tikrą duomenų rinkinio abstrakciją, jungiamą bendru raktu, prie kurios galima „prijungti“ konkrečią darbo eigą.
Bet kaip su integravimo logika? Juk integravimo užduotį generuoja sistemos architektūra, kuri „supjaustoma“ į dalis NE kliento pageidavimu, o veikiama visiškai skirtingų veiksnių:
veikiant Conway įstatymui;
dėl pakartotinio posistemių, anksčiau sukurtų kitiems gaminiams, naudojimo;
kaip nusprendė architektas, remiantis nefunkciniais reikalavimais.
Kyla didžiulė pagunda atskirti integravimo logiką nuo pagrindinės darbo eigos verslo logikos, kad nebūtų užteršta verslo logika integravimo artefaktais ir programos kūrėjui nereikėtų gilintis į sistemos architektūrinio kraštovaizdžio ypatumus. Šis metodas turi daug privalumų, tačiau praktika rodo jo neefektyvumą:
integravimo problemų sprendimas dažniausiai nuslysta iki paprasčiausių variantų sinchroninių skambučių pavidalu dėl ribotų išplėtimo taškų įgyvendinant pagrindinę darbo eigą (apie sinchroninės integracijos trūkumus plačiau žemiau);
integracijos artefaktai vis tiek įsiskverbia į pagrindinę verslo logiką, kai reikalingas grįžtamasis ryšys iš kito posistemio;
programos kūrėjas ignoruoja integraciją ir gali lengvai ją nutraukti pakeisdamas darbo eigą;
sistema nustoja būti viena visuma vartotojo požiūriu, pastebimos „siūlės“ tarp posistemių, atsiranda perteklinės vartotojo operacijos, kurios inicijuoja duomenų perdavimą iš vieno posistemio į kitą.
Kitas būdas – integracijos sąveiką laikyti neatsiejama pagrindinės verslo logikos ir darbo eigos dalimi. Kad programų kūrėjų įgūdžių reikalavimai nepadidėtų, naujų integravimo sąveikų kūrimas turėtų būti atliekamas lengvai ir natūraliai, naudojant minimalias sprendimo pasirinkimo galimybes. Tai padaryti yra sunkiau, nei atrodo: įrankis turi būti pakankamai galingas, kad vartotojui suteiktų reikiamą jo naudojimo galimybių įvairovę ir tuo pačiu neleistų sau šaudyti į koją. Yra daug klausimų, į kuriuos inžinierius turi atsakyti atlikdamas integravimo užduotis, tačiau apie kuriuos programų kūrėjas neturėtų galvoti savo kasdieniame darbe: operacijų ribos, nuoseklumas, atomiškumas, saugumas, mastelio keitimas, apkrovos ir išteklių paskirstymas, maršruto parinkimas, paskirstymas, platinimo ir perjungimo kontekstai ir tt Programų kūrėjams būtina pasiūlyti gana paprastus sprendimų šablonus, kuriuose atsakymai į visus tokius klausimus jau paslėpti. Šie modeliai turėtų būti pakankamai saugūs: verslo logika labai dažnai keičiasi, todėl didėja klaidų įvedimo rizika, klaidų kaina turėtų išlikti gana žema.
Bet vis dėlto, ką BPM turi bendro su tuo? Yra daug darbo eigos įgyvendinimo variantų ...
Išties mūsų sprendimuose labai populiarus dar vienas verslo procesų įgyvendinimas – per deklaratyvų būsenos perėjimo diagramos nustatymą ir verslo logikos tvarkyklių prijungimą prie perėjimų. Tuo pačiu būsena, kuri lemia esamą „dokumento“ padėtį verslo procese, yra paties „dokumento“ atributas.
Taip procesas atrodo projekto pradžioje
Tokio diegimo populiarumą lemia santykinis linijinių verslo procesų kūrimo paprastumas ir greitis. Tačiau programinės įrangos sistemoms tampant sudėtingesnėms, automatizuota verslo proceso dalis auga ir tampa sudėtingesnė. Reikia išskaidyti, pakartotinai panaudoti procesų dalis, taip pat šakės procesus, kad kiekviena šaka būtų vykdoma lygiagrečiai. Tokiomis sąlygomis įrankis tampa nepatogus, o būsenų perėjimo diagrama praranda informacinį turinį (integracijos sąveikos diagramoje visiškai neatsispindi).
Taip atrodo procesas po kelių reikalavimų išsiaiškinimo iteracijų
Išeitis iš šios situacijos buvo variklio integravimas jBPM kai kuriuos produktus su sudėtingiausiais verslo procesais. Per trumpą laiką šis sprendimas turėjo tam tikrą pasisekimą: tapo įmanoma įgyvendinti sudėtingus verslo procesus, išlaikant gana informatyvią ir naujausią schemą. BPMN2.
Maža sudėtingo verslo proceso dalis
Ilgainiui sprendimas nepateisino lūkesčių: didelis darbo intensyvumas kuriant verslo procesus vaizdiniais įrankiais neleido pasiekti priimtinų produktyvumo rodiklių, o pats įrankis tapo vienu nemėgstamiausių tarp kūrėjų. Taip pat buvo skundų dėl vidinės variklio struktūros, dėl kurios atsirado daugybė „lopų“ ir „ramentų“.
Pagrindinis teigiamas jBPM naudojimo aspektas buvo savo nuolatinės būsenos verslo proceso atveju naudos ir žalos suvokimas. Mes taip pat matėme galimybę naudoti proceso metodą, kad būtų galima įgyvendinti sudėtingus integravimo protokolus tarp skirtingų programų, naudojant asinchroninę sąveiką per signalus ir pranešimus. Atkaklios būsenos buvimas čia vaidina lemiamą vaidmenį.
Remdamiesi tuo, kas išdėstyta pirmiau, galime padaryti tokias išvadas: Proceso metodas BPM stiliumi leidžia išspręsti daugybę užduočių, skirtų vis sudėtingesniems verslo procesams automatizuoti, harmoningai į šiuos procesus suderinti integravimo veiklą ir išlaikyti galimybę vizualiai atvaizduoti įgyvendintą procesą tinkamu užrašu.
Sinchroninių skambučių kaip integravimo modelio trūkumai
Sinchroninis integravimas reiškia paprasčiausią blokavimo skambutį. Viena posistemė veikia kaip serverio pusė ir pateikia API norimu metodu. Kitas posistemis veikia kaip kliento pusė ir tinkamu metu skambina tikėdamasis rezultato. Priklausomai nuo sistemos architektūros, kliento ir serverio pusės gali būti talpinamos toje pačioje programoje ir procese arba skirtingose. Antruoju atveju turite pritaikyti tam tikrą RPC diegimą ir pateikti parametrų bei skambučio rezultato suskirstymą.
Toks integravimo modelis turi gana daug trūkumų, tačiau dėl savo paprastumo jis labai plačiai naudojamas praktikoje. Įgyvendinimo greitis sužavi ir verčia jį taikyti vėl ir vėl „deginančių“ terminų sąlygomis, rašant sprendimą į techninę skolą. Tačiau pasitaiko ir taip, kad nepatyrę kūrėjai tuo naudojasi nesąmoningai, tiesiog nesuvokdami neigiamų pasekmių.
Be akivaizdžiausio posistemių jungiamumo padidėjimo, yra ir mažiau akivaizdžių problemų, susijusių su „išplitimo“ ir „tempimo“ operacijomis. Iš tiesų, jei verslo logika daro kokius nors pakeitimus, tada operacijos yra būtinos, o operacijos savo ruožtu užrakina tam tikrus programų išteklius, kuriuos paveikė šie pakeitimai. Tai yra, kol vienas posistemis nelauks atsakymo iš kito, jis negalės užbaigti operacijos ir atleisti užraktų. Tai žymiai padidina įvairių pasekmių riziką:
prarandamas sistemos reagavimas, vartotojai ilgai laukia atsakymų į užklausas;
serveris paprastai nustoja reaguoti į vartotojų užklausas dėl perpildyto gijų telkinio: dauguma gijų „stovi“ ant operacijos užimto resurso užrakto;
ima ryškėti aklavietės: jų atsiradimo tikimybė labai priklauso nuo sandorių trukmės, verslo logikos ir sandoryje dalyvaujančių spynų kiekio;
atsiranda operacijos laiko pabaigos klaidos;
serveris „nukrenta“ į „OutOfMemory“, jei atliekant užduotį reikia apdoroti ir pakeisti didelius duomenų kiekius, o sinchroninių integracijų buvimas labai apsunkina apdorojimo skaidymą į „lengvesnes“ operacijas.
Architektūriniu požiūriu blokavimo skambučių naudojimas integracijos metu lemia atskirų posistemių kokybės kontrolės praradimą: neįmanoma užtikrinti vieno posistemio kokybės tikslų atskirai nuo kito posistemio kokybės tikslų. Jei posistemes kuria skirtingos komandos, tai yra didelė problema.
Viskas tampa dar įdomiau, jei integruojamos posistemės yra skirtingose programose ir abiejose pusėse reikia atlikti sinchroninius pakeitimus. Kaip šiuos pakeitimus padaryti sandoriais?
Jei pakeitimai atliekami atskirose operacijose, reikės užtikrinti patikimą išimčių tvarkymą ir kompensavimą, o tai visiškai pašalina pagrindinį sinchroninių integracijų pranašumą – paprastumą.
Į galvą ateina ir paskirstyti sandoriai, tačiau savo sprendimuose jų nenaudojame: sunku užtikrinti patikimumą.
Šis modelis puikiai išsprendžia minėtas ilgų operacijų problemas, taip pat išplečia sistemos būsenos valdymo galimybes iš verslo logikos pusės: kompensacija po nesėkmingos operacijos gali ne grąžinti sistemos į pradinę būseną, o suteikti alternatyvą. duomenų apdorojimo maršrutas. Tai taip pat leidžia nekartoti sėkmingai atliktų duomenų apdorojimo veiksmų, kai bandote užbaigti procesą iki „geros“ pabaigos.
Įdomu tai, kad monolitinėse sistemose šis modelis taip pat aktualus, kai kalbama apie laisvai susietų posistemių integravimą, ir yra neigiamų padarinių, kuriuos sukelia ilgos operacijos ir atitinkami išteklių užraktai.
Kalbant apie mūsų verslo procesus BPM stiliumi, Sagas įgyvendinti yra labai paprasta: atskiri Sagos žingsniai gali būti nustatyti kaip veiklos verslo procese, o nuolatinė verslo proceso būsena, be kita ko, lemia. , Sagų vidinė būsena. Tai yra, mums nereikia jokio papildomo koordinavimo mechanizmo. Viskas, ko jums reikia, yra žinučių brokeris, turintis „bent kartą“ garantijas kaip transportą.
Tačiau šis sprendimas taip pat turi savo „kainą“:
verslo logika tampa sudėtingesnė: reikia išsiaiškinti kompensaciją;
reikės atsisakyti visiško nuoseklumo, kuris gali būti ypač jautrus monolitinėms sistemoms;
architektūra tampa šiek tiek sudėtingesnė, atsiranda papildomas pranešimų brokerio poreikis;
reikės papildomų stebėjimo ir administravimo priemonių (nors apskritai tai netgi gerai: pakils sistemos aptarnavimo kokybė).
Monolitinėms sistemoms „Sags“ naudojimo pagrindimas nėra toks akivaizdus. Mikropaslaugoms ir kitiems SOA, kur greičiausiai jau yra tarpininkas ir projekto pradžioje buvo paaukotas visiškas nuoseklumas, šio modelio naudojimo nauda gali gerokai nusverti trūkumus, ypač jei yra patogi API verslo logikos lygis.
Verslo logikos įterpimas į mikropaslaugas
Kai pradėjome eksperimentuoti su mikropaslaugomis, iškilo pagrįstas klausimas: kur dėti domeno verslo logiką, susijusią su paslauga, kuri užtikrina domeno duomenų išlikimą?
Žvelgiant į įvairių BPMS architektūrą, gali atrodyti tikslinga atskirti verslo logiką nuo patvarumo: sukurti nuo platformos ir domeno nepriklausomų mikropaslaugų sluoksnį, kuris sudaro aplinką ir konteinerį domeno verslo logikai vykdyti, ir sutvarkyti domeno duomenų išlikimą kaip atskirą. labai paprastų ir lengvų mikropaslaugų sluoksnis. Verslo procesai šiuo atveju organizuoja atkaklumo sluoksnio paslaugas.
Toks požiūris turi labai didelį pliusą: platformos funkcionalumą galite didinti kiek tik norite, ir nuo to „pastorės“ tik atitinkamas platformos mikropaslaugų sluoksnis. Verslo procesai iš bet kurio domeno iš karto gauna galimybę naudotis naujomis platformos funkcijomis, kai tik ji atnaujinama.
Išsamesnis tyrimas atskleidė reikšmingus šio metodo trūkumus:
Platformos paslauga, kuri vienu metu vykdo daugelio domenų verslo logiką, kelia didelę riziką kaip vienas nesėkmės taškas. Dažnas verslo logikos keitimas padidina klaidų riziką, sukeliančią visos sistemos gedimus;
našumo problemos: verslo logika dirba su savo duomenimis per siaurą ir lėtą sąsają:
duomenys vėl bus suskirstyti ir perpumpuoti per tinklo stulpą;
domeno paslauga dažnai grąžins daugiau duomenų, nei verslo logika reikalauja apdoroti, dėl nepakankamų užklausų parametravimo galimybių paslaugos išorinės API lygyje;
kelios nepriklausomos verslo logikos dalys gali pakartotinai prašyti apdoroti tuos pačius duomenis (šią problemą galite sušvelninti pridėdami seansų pupelių, kurios talpina duomenis, tačiau tai dar labiau apsunkina architektūrą ir sukuria duomenų šviežumo bei talpyklos negaliojimo problemų);
sandorių problemos:
verslo procesai su nuolatine būsena, saugomi platformos paslaugos, nesuderinami su domeno duomenimis, todėl nėra lengvų būdų išspręsti šią problemą;
domeno duomenų užrakto pašalinimas iš operacijos: jei reikia keisti domeno verslo logiką, pirmiausia patikrinus faktinių duomenų teisingumą, būtina atmesti konkurencinio tvarkomų duomenų pakeitimo galimybę. Išorinis duomenų blokavimas gali padėti išspręsti problemą, tačiau toks sprendimas kelia papildomą riziką ir mažina bendrą sistemos patikimumą;
papildomos komplikacijos atnaujinant: kai kuriais atvejais reikia sinchroniškai arba griežta seka atnaujinti atkaklumo paslaugą ir verslo logiką.
Galų gale turėjau grįžti prie pagrindų: domeno duomenis ir domeno verslo logiką sujungti į vieną mikropaslaugą. Šis požiūris supaprastina mikropaslaugos kaip neatskiriamos sistemos sudedamosios dalies suvokimą ir nesukelia minėtų problemų. Tai taip pat nėra nemokama:
API standartizavimas reikalingas sąveikai su verslo logika (ypač norint teikti vartotojo veiklą kaip verslo procesų dalį) ir API platformos paslaugomis; atidesnis dėmesys API pakeitimams, reikalingas suderinamumas pirmyn ir atgal;
reikalaujama pridėti papildomų vykdymo bibliotekų, kad būtų užtikrintas verslo logikos veikimas kaip kiekvienos tokios mikropaslaugos dalis, ir dėl to tokioms bibliotekoms keliami nauji reikalavimai: lengvumas ir minimalios pereinamosios priklausomybės;
verslo logikos kūrėjai turi sekti bibliotekos versijas: jei mikropaslauga ilgą laiką nebuvo baigta, greičiausiai joje bus pasenusios bibliotekų versijos. Tai gali būti netikėta kliūtis pridedant naują funkciją ir gali prireikti seną tokios paslaugos verslo logiką perkelti į naujas bibliotekų versijas, jei tarp versijų buvo nesuderinamų pakeitimų.
Tokioje architektūroje yra ir platformos paslaugų sluoksnis, tačiau šis sluoksnis nebesudaro konteinerį domeno verslo logikai vykdyti, o tik jo aplinką, teikiančią pagalbines „platformines“ funkcijas. Toks sluoksnis reikalingas ne tik norint išlaikyti domeno mikropaslaugų lengvumą, bet ir centralizuoti valdymą.
Pavyzdžiui, vartotojo veikla verslo procesuose generuoja užduotis. Tačiau dirbdamas su užduotimis vartotojas turi matyti užduotis iš visų domenų bendrame sąraše, o tai reiškia, kad turi būti atitinkama užduočių registravimo platformos paslauga, išvalyta nuo domeno verslo logikos. Šiame kontekste išlaikyti verslo logikos kapsuliavimą yra gana problematiška, ir tai yra dar vienas šios architektūros kompromisas.
Verslo procesų integravimas programų kūrėjo akimis
Kaip jau minėta aukščiau, programų kūrėjas turi būti abstrahuotas nuo kelių programų sąveikos įgyvendinimo techninių ir inžinerinių ypatybių, kad galėtų pasikliauti geru kūrimo produktyvumu.
Pabandykime išspręsti gana sudėtingą integravimo problemą, specialiai sugalvotą straipsniui. Tai bus „žaidimo“ užduotis, apimanti tris programas, kurių kiekviena apibrėžia tam tikrą domeno pavadinimą: „app1“, „app2“, „app3“.
Kiekvienoje programoje paleidžiami verslo procesai, kurie pradeda „žaisti kamuoliu“ per integravimo magistralę. Žinutės pavadinimu „Kamuolis“ veiks kaip rutulys.
Žaidimo taisyklės:
pirmasis žaidėjas yra iniciatorius. Jis kviečia kitus žaidėjus į žaidimą, pradeda žaidimą ir gali bet kada jį užbaigti;
kiti žaidėjai deklaruoja savo dalyvavimą žaidime, „susipažįsta“ vieni su kitais ir pirmuoju žaidėju;
gavęs kamuolį žaidėjas pasirenka kitą dalyvaujantį žaidėją ir perduoda kamuolį jam. Skaičiuojamas bendras praėjimų skaičius;
kiekvienas žaidėjas turi „energiją“, kuri mažėja kiekvienu to žaidėjo kamuolio perdavimu. Kai energija baigiasi, žaidėjas pašalinamas iš žaidimo, pranešant apie pasitraukimą;
jei žaidėjas paliekamas vienas, jis nedelsdamas pareiškia apie savo išvykimą;
kai visi žaidėjai pašalinami, pirmasis žaidėjas paskelbia žaidimo pabaigą. Jei jis paliko žaidimą anksčiau, tada belieka sekti žaidimą, kad jį užbaigtumėte.
Norėdami išspręsti šią problemą, naudosiu mūsų DSL verslo procesams, kuris leidžia kompaktiškai apibūdinti Kotlino logiką su minimaliomis sąnaudomis.
App1 programoje veiks pirmojo žaidėjo (jis taip pat yra žaidimo iniciatorius) verslo procesas:
klasės 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}")
}
Be verslo logikos vykdymo, aukščiau pateiktas kodas gali sukurti objektinį verslo proceso modelį, kurį galima vizualizuoti kaip diagramą. Mes dar neįdiegėme vizualizatoriaus, todėl turėjome šiek tiek laiko skirti piešimui (čia šiek tiek supaprastinau BPMN žymėjimą dėl vartų naudojimo, kad pagerinčiau diagramos nuoseklumą su aukščiau nurodytu kodu):
app2 apims kito žaidėjo verslo procesą:
klasės 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}")
}
Diagrama:
„App3“ programoje žaidėją padarysime šiek tiek kitokiu elgesiu: užuot atsitiktinai pasirinkę kitą žaidėją, jis veiks pagal „round-robin“ algoritmą:
klasės „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}")
}
Priešingu atveju žaidėjo elgesys nesiskiria nuo ankstesnio, todėl diagrama nesikeičia.
Dabar mums reikia testo, kad galėtume viską atlikti. Pateiksiu tik paties testo kodą, kad neužgriozdinčiau straipsnio (tiesą sakant, kitų verslo procesų integracijai išbandyti naudojau anksčiau sukurtą testavimo aplinką):
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;
}
jei yra reikiamų įrankių, programų kūrėjai gali kurti integravimo sąveikas tarp taikomųjų programų neatsiribodami nuo verslo logikos;
integravimo užduoties, kuriai reikia inžinerinių kompetencijų, sudėtingumas (sudėtingumas) gali būti paslėptas sistemos viduje, jei tai iš pradžių nustatyta sistemos architektūroje. Užduoties sunkumas (sunkumas) negali būti paslėptas, todėl sudėtingos užduoties sprendimas kode atrodys atitinkamai;
kuriant integracijos logiką, būtina atsižvelgti į visų integracijos dalyvių būsenos kaitos nuoseklumą ir netiesiarizavimo trūkumą. Tai verčia mus apsunkinti logiką, kad ji nebūtų jautri išorinių įvykių eigai. Mūsų pavyzdyje žaidėjas yra priverstas dalyvauti žaidime po to, kai praneša apie savo pasitraukimą iš žaidimo: kiti žaidėjai ir toliau perduos jam kamuolį, kol pasieks informacija apie jo išėjimą ir ją apdoros visi dalyviai. Ši logika neišplaukia iš žaidimo taisyklių ir yra kompromisinis sprendimas pasirinktos architektūros rėmuose.
Toliau pakalbėkime apie įvairias mūsų sprendimo subtilybes, kompromisus ir kitus dalykus.
Visi pranešimai vienoje eilėje
Visos integruotos programos veikia su viena integravimo magistrale, kuri pateikiama kaip išorinis tarpininkas, viena BPMQueue pranešimams ir viena BPMTopic tema signalams (įvykiams). Visų pranešimų perdavimas per vieną eilę savaime yra kompromisas. Verslo logikos lygiu dabar galite pristatyti tiek naujų tipų pranešimų, kiek norite, nekeisdami sistemos struktūros. Tai reikšmingas supaprastinimas, tačiau susijęs su tam tikra rizika, kuri, vykdant įprastas užduotis, mums atrodė ne tokia reikšminga.
Tačiau čia yra viena subtilybė: kiekviena programa filtruoja „savo“ pranešimus iš eilės prie įėjimo pagal savo domeno pavadinimą. Taip pat signaluose galima nurodyti domeną, jei reikia apriboti signalo „apimtį“ iki vienos programos. Tai turėtų padidinti magistralės pralaidumą, tačiau verslo logika dabar turi veikti su domenų vardais: privaloma adresuoti pranešimus, pageidautina signalams.
Integravimo magistralės patikimumo užtikrinimas
Patikimumas susideda iš kelių dalykų:
Pasirinktas pranešimų tarpininkas yra esminis architektūros komponentas ir vienintelis gedimo taškas: jis turi būti pakankamai atsparus gedimams. Turėtumėte naudoti tik laiko patikrintus diegimus, turinčius gerą palaikymą ir didelę bendruomenę;
būtina užtikrinti aukštą pranešimų brokerio pasiekiamumą, kuriam jis turi būti fiziškai atskirtas nuo integruotų programų (aukštą programų su taikomą verslo logiką pasiekiamumą užtikrinti daug sunkiau ir brangiau);
brokeris įpareigotas „bent kartą“ pateikti pristatymo garantijas. Tai yra privalomas patikimo integravimo magistralės veikimo reikalavimas. „Tiksliai vieną kartą“ lygio garantijų nereikia: verslo procesai dažniausiai nėra jautrūs pasikartojantiems pranešimų ar įvykių atėjimui, o atliekant specialias užduotis, kur tai svarbu, verslo logiką lengviau įtraukti papildomus patikrinimus, nei nuolat naudoti. "brangios" " garantijos;
pranešimų ir signalų siuntimas turi būti įtrauktas į bendrą operaciją, pasikeitus verslo procesų ir domeno duomenų būsenai. Pageidautina būtų naudoti modelį Operacijų siuntimo dėžutė, tačiau tam reikės papildomos lentelės duomenų bazėje ir relės. JEE programose tai galima supaprastinti naudojant vietinį JTA valdytoją, tačiau ryšys su pasirinktu brokeriu turi veikti režimu XA;
gaunamų pranešimų ir įvykių tvarkytojai taip pat turi dirbti su verslo proceso būsenos keitimo operacija: jei tokia operacija atšaukiama, tada pranešimo gavimas taip pat turi būti atšauktas;
pranešimai, kurių nepavyko pristatyti dėl klaidų, turėtų būti saugomi atskiroje parduotuvėje D.L.Q. (Mirusių laiškų eilė). Norėdami tai padaryti, sukūrėme atskirą platformos mikropaslaugą, kuri saugo tokius pranešimus savo saugykloje, indeksuoja juos pagal atributus (greitam grupavimui ir paieškai) ir atskleidžia API, kad būtų galima peržiūrėti, iš naujo siųsti į paskirties adresą ir ištrinti pranešimus. Sistemos administratoriai gali dirbti su šia paslauga per savo žiniatinklio sąsają;
brokerio nustatymuose reikia koreguoti pristatymo bandymų skaičių ir vėlavimus tarp pristatymų, kad sumažintumėte tikimybę, kad pranešimai pateks į DLQ (optimalių parametrų apskaičiuoti beveik neįmanoma, tačiau galite veikti empiriškai ir koreguoti juos per operacija);
DLQ parduotuvė turėtų būti nuolat stebima, o stebėjimo sistema turėtų pranešti sistemos administratoriams, kad jie galėtų kuo greičiau reaguoti, kai atsiranda nepristatytų pranešimų. Tai sumažins gedimo ar verslo logikos klaidos „žalos zoną“;
integravimo magistralė turi būti nejautri laikinam programų nebuvimui: temų prenumeratos turi būti patvarios, o programos domeno pavadinimas turi būti unikalus, kad kas nors kitas nebandytų apdoroti jos pranešimo iš eilės, kai programos nėra.
Verslo logikos gijų saugumo užtikrinimas
Tas pats verslo proceso egzempliorius vienu metu gali gauti keletą pranešimų ir įvykių, kurių apdorojimas prasidės lygiagrečiai. Tuo pačiu metu programų kūrėjui viskas turėtų būti paprasta ir saugi.
Proceso verslo logika apdoroja kiekvieną išorinį įvykį, kuris veikia šį verslo procesą atskirai. Šie įvykiai gali būti:
verslo proceso egzemplioriaus paleidimas;
vartotojo veiksmas, susijęs su veikla verslo procese;
pranešimo ar signalo, kuriam prenumeruojamas verslo proceso egzempliorius, gavimas;
verslo proceso egzemplioriaus nustatyto laikmačio galiojimo pabaiga;
valdymo veiksmas per API (pvz., proceso nutraukimas).
Kiekvienas toks įvykis gali pakeisti verslo proceso egzemplioriaus būseną: kai kurios veiklos gali baigtis, kitos prasidėti, gali pasikeisti nuolatinių savybių reikšmės. Uždarius bet kokią veiklą gali būti suaktyvinta viena ar daugiau toliau nurodytų veiklų. Tie, savo ruožtu, gali nustoti laukti kitų įvykių arba, jei jiems nereikia jokių papildomų duomenų, jie gali atlikti tą pačią operaciją. Prieš uždarant operaciją, nauja verslo proceso būsena išsaugoma duomenų bazėje, kur ji lauks kito išorinio įvykio.
Nuolatiniai verslo procesų duomenys, saugomi reliacinėje duomenų bazėje, yra labai patogus apdorojimo sinchronizavimo taškas naudojant SELECT FOR UPDATE. Jei vienai operacijai pavyko gauti verslo proceso būseną iš duomenų bazės, kad ją pakeistų, tai jokia kita operacija lygiagrečiai negalės gauti tokios pačios būsenos kitam pakeitimui, o atlikus pirmą operaciją, antrasis garantuotai gaus jau pasikeitusią būseną.
Naudodami pesimistinius užraktus DBVS pusėje, įvykdome visus būtinus reikalavimus RŪGŠTIS, taip pat išsaugoma galimybė pritaikyti programos mastelį pagal verslo logiką, padidinant vykdomų egzempliorių skaičių.
Tačiau pesimistiniai užraktai mums gresia aklavietėmis, o tai reiškia, kad SELECT FOR UPDATE vis tiek turėtų būti apribotas iki tam tikro protingo skirtojo laiko, jei kai kuriais žiauriais verslo logikos atvejais aklavietėje atsirastų aklavietė.
Kita problema – verslo proceso pradžios sinchronizavimas. Nors nėra verslo proceso egzemplioriaus, duomenų bazėje taip pat nėra būsenos, todėl aprašytas metodas neveiks. Jei norite užtikrinti verslo proceso egzemplioriaus unikalumą tam tikroje srityje, jums reikia tam tikro sinchronizavimo objekto, susieto su proceso klase ir atitinkama apimtimi. Norėdami išspręsti šią problemą, naudojame kitą užrakinimo mechanizmą, kuris leidžia užrakinti savavališką šaltinį, nurodytą raktu URI formatu per išorinę paslaugą.
Mūsų pavyzdžiuose „InitialPlayer“ verslo procese yra deklaracija
uniqueConstraint = UniqueConstraints.singleton
Todėl žurnale yra pranešimai apie atitinkamo rakto užrakto paėmimą ir atleidimą. Kituose verslo procesuose tokių pranešimų nėra: unikalus apribojimas nenustatytas.
Verslo procesų problemos su nuolatine būsena
Kartais atkakli būsena ne tik padeda, bet ir tikrai trukdo vystytis.
Problemos prasideda, kai reikia pakeisti verslo logiką ir (arba) verslo procesų modelį. Nenustatyta, kad joks toks pakeitimas būtų suderinamas su sena verslo procesų būsena. Jei duomenų bazėje yra daug „gyvų“ egzempliorių, nesuderinami pakeitimai gali sukelti daug problemų, su kuriomis dažnai susidurdavome naudodami jBPM.
Priklausomai nuo pokyčių gylio, galite veikti dviem būdais:
sukurkite naują verslo proceso tipą, kad neatliktumėte nesuderinamų senojo pakeitimų, ir naudokite jį vietoj senojo pradėdami naujus egzempliorius. Seni egzemplioriai ir toliau veiks „senu būdu“;
atnaujinant verslo logiką perkelti nuolatinę verslo procesų būseną.
Pirmasis būdas yra paprastesnis, tačiau turi savo apribojimų ir trūkumų, pavyzdžiui:
verslo logikos dubliavimas daugelyje verslo procesų modelių, verslo logikos apimties didinimas;
dažnai reikalingas momentinis perėjimas prie naujos verslo logikos (beveik visada kalbant apie integravimo užduotis);
kūrėjas nežino, kada galima ištrinti pasenusius modelius.
Praktiškai mes naudojame abu būdus, tačiau priėmėme keletą sprendimų, kad supaprastintume savo gyvenimą:
duomenų bazėje nuolatinė verslo proceso būsena saugoma lengvai skaitoma ir lengvai apdorojama forma: JSON formato eilutėje. Tai leidžia atlikti perkėlimą tiek programoje, tiek išorėje. Ypatingais atvejais taip pat galite jį pakoreguoti rankenėlėmis (ypač naudinga kuriant derinant);
integravimo verslo logika nenaudoja verslo procesų pavadinimų, todėl bet kuriuo metu būtų galima pakeisti vieno iš dalyvaujančių procesų įgyvendinimą nauju, nauju pavadinimu (pvz., „InitialPlayerV2“). Susiejimas vyksta per pranešimų ir signalų pavadinimus;
proceso modelis turi versijos numerį, kurį padidiname, jei atliekame nesuderinamus šio modelio pakeitimus, ir šis skaičius išsaugomas kartu su proceso egzemplioriaus būsena;
nuolatinė proceso būsena pirmiausia nuskaitoma iš bazės į patogų objekto modelį, su kuriuo gali dirbti perkėlimo procedūra, jei pasikeitė modelio versijos numeris;
perkėlimo procedūra yra šalia verslo logikos ir yra vadinama "tinginiu" kiekvienam verslo proceso egzemplioriui jo atkūrimo iš duomenų bazės metu;
jei reikia greitai ir sinchroniškai perkelti visų procesų egzempliorių būseną, naudojami daugiau klasikinių duomenų bazių perkėlimo sprendimų, tačiau ten turite dirbti su JSON.
Ar man reikia kitos verslo procesų sistemos?
Straipsnyje aprašyti sprendimai leido mums gerokai supaprastinti gyvenimą, išplėsti programų kūrimo lygmeniu sprendžiamų problemų spektrą ir padaryti patrauklesnę idėją verslo logiką atskirti į mikropaslaugas. Tam buvo atliktas didelis darbas, sukurta labai „lengva“ verslo procesų sistema, taip pat paslaugų komponentai identifikuotoms problemoms spręsti įvairių taikomų užduočių kontekste. Mes norime pasidalinti šiais rezultatais, kad bendrų komponentų kūrimas būtų atvira prieiga pagal nemokamą licenciją. Tam reikės šiek tiek pastangų ir laiko. Tokių sprendimų paklausos supratimas mums galėtų būti papildoma paskata. Siūlomame straipsnyje labai mažai dėmesio skiriama paties karkaso galimybėms, tačiau kai kurios jų matomos iš pateiktų pavyzdžių. Jei vis dėlto paskelbsime savo sistemą, jai bus skirtas atskiras straipsnis. Tuo tarpu būsime dėkingi, jei paliksite šiek tiek atsiliepimų atsakydami į klausimą:
Apklausoje gali dalyvauti tik registruoti vartotojai. Prisijungti, Prašau.
Ar man reikia kitos verslo procesų sistemos?
18,8%Taip, aš ilgai ieškojau kažko panašaus.
12,5%įdomu sužinoti daugiau apie jūsų įgyvendinimą, tai gali būti naudinga2
6,2%naudojame vieną iš esamų karkasų, bet galvojame apie jo pakeitimą1
18,8%naudojame vieną iš esamų karkasų, viskas tinka3