BPM stiliaus integracija

BPM stiliaus integracija

Sveiki, Habr!

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.

BPM stiliaus integracija
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).

BPM stiliaus integracija
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.

BPM stiliaus integracija
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ą.

BPM stiliaus integracija

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ą.

„Saga“ kaip sandorių problemos sprendimas

Augant mikropaslaugų populiarumui, didėja paklausa Sagos raštas.

Š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.

BPM stiliaus integracija

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ų.

BPM stiliaus integracija

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):

BPM stiliaus integracija

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:

BPM stiliaus integracija

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

Atlikite testą, pažiūrėkite į žurnalą:

konsolės išvestis

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

Iš viso to galima padaryti keletą svarbių išvadų:

  • 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.

BPM stiliaus integracija

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:

  1. 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“;
  2. 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

  • 18,8%susidoroti be rėmų3

  • 25,0%parašyk savo 4

Balsavo 16 vartotojų. 7 vartotojai susilaikė.

Šaltinis: www.habr.com

Добавить комментарий