BPM stílusú integráció

BPM stílusú integráció

Üdvözöljük, Habr!

Cégünk ERP szoftvermegoldások fejlesztésére specializálódott, amelyek zöme tranzakciós rendszerekből áll, hatalmas üzleti logikával és dokumentumkezeléssel, mint a SED. Modern termékeink JavaEE technológiákra épülnek, de aktívan kísérletezünk mikroservice-ekkel is. Az ilyen megoldások egyik legproblémásabb része a különböző alrendszerek integrálása, amelyek kapcsolódó domainekhez tartoznak. Az integrációs feladatok mindig óriási fejtörést okoztak nekünk, függetlenül az alkalmazott architekturális stílusoktól, technológiai stack-ekből és keretrendszerektől, de az utóbbi időben előrelépés látszik ezen feladatok megoldásában.

A javasolt cikkben a Krista NPO tapasztalatait és architekturális felfedezéseit fogom bemutatni a megnevezett területen. Példaként megvizsgálunk egy egyszerű integrációs feladat megoldását a gyakorlati fejlesztő nézőpontjából, és kiderítjük, mi rejlik a látszólagos egyszerűség mögött.

Megjegyzés

A cikkben leírt architekturális és technikai megoldásokat saját tapasztalataim alapján, konkrét feladatok kontextusában ajánlom. Ezek a megoldások nem igényelnek univerzalitást, és lehet, hogy nem optimálisak más használati feltételek mellett.

Mi köze van ehhez a BPM-hez?

A kérdés megválaszolásához egy kicsit el kell mélyednünk a megoldásaink alkalmazásának sajátosságaiba. A tipikus tranzakciós rendszerünk üzleti logikájának fő része az adatok bevitele az adatbázisba felhasználói felületeken keresztül, ezeken az adatok manuális és automatizált ellenőrzése, azok egyfajta workflow-n való áthaladása, publikálás egy másik rendszerbe / analitikai bázisba / archívumba, valamint jelentések készítése. Így a rendszer kulcsfontosságú funkciója a megrendelők számára a belső üzleti folyamataik automatizálása.

A kényelmes kommunikáció érdekében a "dokumentum" kifejezést mint adatcsoport absztrakciót használjuk, amelyeket közös kulccsal kapcsolhatunk be egy adott workflow-ba.
De mi a helyzet az integrációs logikával? A rendszer architektúrája, amely "darabokban van", nem a megrendelői igények miatt, hanem más tényezők hatására hozza létre az integrációs feladatot:

  • Conway-törvény hatása alatt;
  • másodlagos rendszerek újrahasználatának eredményeként, amelyeket korábban más termékekhez fejlesztettek;
  • az építész döntése alapján, figyelembe véve a nem funkcionális követelményeket.

Nagy a kísértés, hogy az integrációs logikát elválasszuk a fő munkafolyamat üzleti logikájától, hogy ne szennyezzük be az üzleti logikát integrációs artefaktumokkal, és megóvjuk az alkalmazásfejlesztőt attól, hogy belemerüljön a rendszer architekturális tájának sajátosságaiba. Ennek a megközelítésnek számos előnye van, azonban a gyakorlat azt mutatja, hogy hatástalan:

  • az integrációs feladatok megoldása általában a legegyszerűbb lehetőségekre korlátozódik, mint például szinkron hívások, a fő munkafolyamat végrehajtásában a bővítési pontok korlátozottsága miatt (a szinkron integráció hátrányairól - kissé lentebb);
  • az integrációs artefaktumok ugyanúgy behatolnak a fő üzleti logikába, amikor visszajelzés szükséges egy másik alrendszertől;
  • az alkalmazásfejlesztő figyelmen kívül hagyja az integrációt, és könnyen megszegheti azt, ha módosítja a munkafolyamatot;
  • a rendszer a felhasználó szempontjából már nem egységes, láthatóvá válnak a "varrások" az alrendszerek között, felesleges felhasználói műveletek jelennek meg, amelyek adatátvitelt kezdeményeznek az egyes alrendszerek között.

A másik megközelítés az integrációs interakciók alapvető üzleti logikájának és munkafolyamatának elválaszthatatlan részének tekintése. Ahhoz, hogy az alkalmazásfejlesztők képesítési követelményei ne szöktek az egeket, az új integrációs interakciók létrehozásának egyszerűnek és zökkenőmentesnek kell lennie, minimális döntési lehetőségekkel a megoldási mód kiválasztásában. Ezt nehezebb megvalósítani, mint amilyennek tűnik: az eszköznek elég erősnek kell lennie ahhoz, hogy a felhasználónak elegendő alkalmazási lehetőséget kínáljon, miközben megakadályozza, hogy "lábba lője magát". Számos kérdés merül fel, amelyekre az mérnöknek válaszolnia kell az integrációs feladatok kontextusában, de amelyekkel az alkalmazásfejlesztőnek a napi munkájában nem kell foglalkoznia: tranzakciós határok, konzisztencia, atomaritás, biztonság, skálázhatóság, terhelés- és erőforrás-elosztás, útválasztás, marshaling, kontextusok terjesztése és váltása stb. Az alkalmazásfejlesztőknek elegendő egyszerű megoldási sablonokat kell kínálni, amelyek már tartalmazzák az ilyen kérdésekre adott válaszokat. Ezeknek a sablonoknak kellően biztonságosnak kell lenniük: az üzleti logika nagyon gyakran változik, ami növeli a hibák bevezetésének kockázatát, a hibák ára pedig elég alacsonyan kell, hogy maradjon.

De mi köze van a BPM-hez? Számos lehetőség van a munkafolyamat megvalósítására...
Valóban, a megoldásainkban nagyon népszerű egy alternatív üzleti folyamatvégrehajtás – az állapotátmeneti diagram deklaratív megadásával és az állapotátmenetekhez kapcsolódó üzleti logikát kezelő programozók integrálásával. Ezenkívül az állapot, amely meghatározza a „dokumentum” aktuális helyzetét az üzleti folyamatban, a „dokumentum” attribútuma.

BPM stílusú integráció
Így néz ki a folyamat a projekt kezdetén.

E megoldás népszerűsége a lineáris üzleti folyamatok viszonylag egyszerű és gyors létrehozásának köszönhető. Azonban a programrendszerek folyamatos összetetté válásával az üzleti folyamat automatizált része is bővül és bonyolultabbá válik. Felmerül a szükséglet a dekompozícióra, a folyamatok egyes részeinek újrahasznosítására, valamint a folyamatok ágazására, hogy minden ágazat párhuzamosan fusson. Ilyen körülmények között az eszköz kényelmetlenné válik, és az állapotátmenetek diagramja elveszíti informatív értékét (az integrációs kölcsönhatások egyáltalán nem tükröződnek a diagramon).

BPM stílusú integráció
Így néz ki a folyamat néhány iteráció után a követelmények pontosításában.

A helyzet megoldása a motor integrálása lett. jBPM bizonyos termékekben a legbonyolultabb üzleti folyamatokkal. Rövid távon ez a megoldás bizonyos sikereket hozott: lehetőség nyílt bonyolult üzleti folyamatok megvalósítására, miközben a diagram elegendően informativ és aktuális maradt a BPMN2.

BPM stílusú integráció
Bonyolult üzleti folyamat kis hányada

Hosszú távon azonban a megoldás nem felelt meg az elvárásoknak: a vizuális eszközökön keresztül történő üzleti folyamatok létrehozásának nagy munkaigénye nem tette lehetővé a megfelelő termelékenységi mutatók elérését, és magát az eszközt az egyik legkevésbé kedvelt fejlesztői eszközzé tette. Az engine belső felépítésére is voltak panaszok, amelyek számos "javítást" és "problémákat" eredményeztek.

A jBPM alkalmazásának fő pozitívumai közé tartozik a saját állandó állapot meglétével járó hasznok és hátrányok felismerése az üzleti folyamat példányában. Ezen kívül lehetőséget láttunk a folyamatmegközelítés alkalmazására bonyolult integrációs protokollok megvalósítására különböző alkalmazások között, aszinkron kölcsönhatások révén jelek és üzenetek segítségével. Az állandó állapot megléte ebben kulcsszerepet játszik.

A fentiek alapján levonható a következtetés: a BPM-stílusú folyamatmegközelítés lehetővé teszi számunkra, hogy széles spektrumú feladatokat oldjunk meg az egyre bonyolultabb üzleti folyamatok automatizálása terén, harmonikusan beépítve az integrációs tevékenységeket ezen folyamatokba, és fenntartva a lehetőséget a megvalósított folyamat vizuális ábrázolására a megfelelő notációban.

A szinkron hívások hátrányai mint integrációs minta

A szinkron integráció alatt a legegyszerűbb blokkoló hívást értjük. Az egyik alrendszer szerveroldali szerepet tölt be, és API-t biztosít a megfelelő módszerrel. A másik alrendszer kliensoldali szerepet játszik, és a megfelelő időben hívást végez, várva az eredményre. A rendszer architektúrájától függően a kliens- és szerveroldalak lehetnek egy alkalmazásban és folyamatban, vagy különböző helyeken. Az utóbbi esetben szükséges egy RPC megvalósítás alkalmazása és a paraméterek és a hívás eredményeinek marshalálása.

BPM stílusú integráció

Ez az integrációs minta számos hátránnyal bíró, de gyakorlatban széles körben használt megoldás a egyszerűsége miatt. A megvalósítás gyorsasága vonzó, és újra és újra arra késztet minket, hogy alkalmazzuk a "tüzes" határidők alatt, ezzel technikai adósságot halmozva fel. De az is előfordul, hogy tapasztalatlan fejlesztők tudatosan alkalmazzák, egyszerűen nem járva utána a negatív következményeknek.

Az egyértelmű szubsisztencia összekapcsolásán kívül vannak kevésbé nyilvánvaló problémák a tranzakciók "széttartásával" és "hosszabbításával". Valóban, ha az üzleti logika módosításokat végez, akkor szükség van tranzakciókra, és a tranzakciók blokkálják a bizonyos alkalmazási erőforrásokat, amelyeket ezek a módosítások érintenek. Vagyis ameddig az egyik alrendszer meg nem kapja a választ a másiktól, nem tudja befejezni a tranzakciót, és feloldani a zárolásokat. Ez jelentősen növeli a különböző hatások megjelenésének kockázatát:

  • a rendszer reagálási ideje csökken, a felhasználók hosszú ideig várnak a válaszokra;
  • a szerver teljesen leáll a felhasználói kérelmekre a szálak medencéjének túlterheltsége miatt: a többségi szálak "megrekedtek" a tranzakció által blokkolt erőforráson;
  • halott zárolások kezdődnek megjelenni: a megjelenésük valószínűsége jelentősen függ a tranzakciók hosszától, az érintett üzleti logika mennyiségétől és a zárolásoktól;
  • tranzakciós időkorlátok hibái jelentkeznek;
  • a szerver "összeomlik" OutOfMemory miatt, ha a feladat nagy mennyiségű adat feldolgozását és módosítását igényli, és a szinkron integrációk jelentősen megnehezítik a feldolgozás "könnyebb" tranzációkra való bontását.

Architektonikai szempontból a blokkoló hívások alkalmazása integráció során a különböző alrendszerek minőségének elvesztéséhez vezet: lehetetlen biztosítani egy alrendszer célkitűzéseinek minőségét egy másik alrendszer minőségétől való eltérés nélkül. Ha az alrendszereket különböző csapatok fejlesztik, akkor ez nagy problémát jelent.

Minden még érdekesebbé válik, ha az integrált alrendszerek különböző alkalmazásokban találhatók, és szinkron módosításokat kell végezni mindkét oldalról. Hogyan biztosítható a módosítások tranzakciós jellege?

Ha a módosításokat külön tranzakciókban végzik el, akkor megfelelő hibakezelést és kompenzációt kell biztosítani, ami teljesen eltünteti a szinkron integrációk lényeges előnyét – a egyszerűséget.

Eszembe jutnak a megosztott tranzakciók, de mi nem használjuk ezeket a megoldásainkban: nehéz biztosítani a megbízhatóságot.

A «Saga» mint tranzakciós probléma megoldás

A mikroszolgáltatások népszerűségének növekedésével egyre nagyobb igény mutatkozik Saga Minta.

Ez a minta kiválóan megoldja a hosszú tranzakciókkal kapcsolatos fenti problémákat, és bővíti a rendszer állapotkezelésének lehetőségeit az üzleti logika szempontjából: egy sikertelen tranzakció utáni kompenzáció nem vonja vissza a rendszert az eredeti állapotába, hanem alternatív adatfeldolgozási útvonalat biztosít. Ez lehetővé teszi továbbá, hogy a sikeresen befejezett lépéseket ne ismételjük meg az adatok feldolgozási folyamatának „jó” befejezéséig.

Érdekes módon ez a minta a monolitikus rendszerekben is releváns, ha gyengén kapcsolódó alrendszerek integrációjáról van szó, és megfigyelhetők a hosszú tranzakciók és a megfelelő erőforrászárak által okozott negatív hatások.

A BPM stílusú üzleti folyamataikra vonatkozóan a „Ságák” implementálása nagyon egyszerűnek bizonyul: a „Saga” egyes lépései belső üzleti folyamatok aktivitásaiként definiálhatók, és az üzleti folyamat perzisztens állapota az alrendszer „Saga” belső állapotát is meghatározza. Tehát nem szükséges egyéb koordináló mechanizmusokat alkalmaznunk. Csak egy üzenetközvetítőre van szükségünk, amely támogatja az „at least once” garanciákat mint szállítmányozási módot.

De ennek a megoldásnak is megvan a maga „ára”:

  • üzleti logika egyre bonyolultabbá válik: a kártérítéseket ki kell dolgozni;
  • fel kell adni a teljes konzisztenciát, ami különösen érzékeny lehet monolitikus rendszerek esetében;
  • kicsit bonyolultabbá válik az architektúra, megjelenik egy további igény üzenetközvetítőre;
  • további monitorozási és adminisztrációs eszközökre lesz szükség (bár összességében ez akár jónak is mondható: a rendszer szolgáltatásának minősége nőni fog).

Monolitikus rendszerek esetén a 'Saga' használatának indokoltsága nem olyan egyértelmű. Mikroszolgáltatások és más SOA esetén, ahol valószínűleg már van üzenetközvetítő, és a teljes konzisztenciát már a projekt kezdetén feláldozták, ennek a sablonnak a használata jelentősen meghaladhatja a hátrányait, különösen, ha van kényelmes API a üzleti logika szintjén.

Üzleti logika enkapszulálása mikroszolgáltatásokban

Amikor elkezdtünk kísérletezni a mikroszolgáltatásokkal, ésszerű kérdés merült fel: hová helyezzük a domain üzleti logikát a domain adatok tartósítását biztosító szolgáltatáshoz képest?

Ha különböző BPMS architektúrákra nézünk, ésszerűnek tűnhet az üzleti logika elválasztása a tartósítástól: létrehozni egy platformfüggetlen mikroszolgáltatás réteget, amely környezetet és konténert képez a domain üzleti logika végrehajtásához, míg a domain adatok tartósítása egy nagyon egyszerű és könnyű mikroszolgáltatásokból álló külön réteggel történik. Az üzleti folyamatok ebben az esetben a tartósítást biztosító szolgáltatások orkestrálását végzik.

BPM stílusú integráció

Ennek a megközelítésnek van egy nagyon nagy előnye: a platform funkcionalitását szabadon lehet bővíteni, és ez csak a megfelelő platform mikroszolgáltatásokat fogja 'elhízni'. Az üzleti folyamatok bármely domainből azonnal lehetőséget kapnak, hogy használják az új funkcionalitást a platformon, amint az frissül.

A részletesebb kidolgozás jelentős hiányosságokat tárt fel:

  • a platformszolgáltatás, amely számos domain üzleti logikáját végrehajtja, nagy kockázatot hordoz, mint egyetlen hibalehetőség. A gyakori üzleti logikai változások növelik a hibák kockázatát, amelyek kudarcokhoz vezetnek, ami az egész rendszeren elterjedhet;
  • teljesítményproblémák: az üzleti logika saját adataival egy szűk és lassú interfészen keresztül dolgozik:
    • az adatok többször is át lesznek paraméterezve és feldolgozva a hálózati stack-en keresztül;
    • a domain szolgáltatás gyakran több adatot ad vissza, mint amire a biznisz logikának szüksége van a feldolgozáshoz, a külső API szolgáltatás kérésének paraméterezési lehetőségeinek hiánya miatt;
    • több független üzleti logikai rész ismételten lekérheti ugyanazokat az adatokat a feldolgozáshoz (a probléma enyhítésére hozzáadhatók session komponensek, amelyek gyorsítják az adatokat, de ez tovább bonyolítja az architektúrát és adatfrissítési problémákat okoz a cache érvénytelenítése miatt);
  • tranzakciós problémák:
    • a platformalapú szolgáltatás által kezelt tartós állapotú üzleti folyamatok eltéréseket mutatnak a domain adatokkal, és egyszerű megoldásokat nem lehet látni erre a problémára;
    • a domain adatok zárolásának kiemelése a tranzakcióból: ha a domain üzleti logikának változtatásokat kell végrehajtania, először is ellenőriznie kell az aktuális adatok helyességét, ki kell zárnia az adatkezelések versenyhelyzetét. A külső adatzárolás segíthet a feladat megoldásában, de ilyen megoldás további kockázatokat hoz és csökkenti a rendszer össz可靠性ét;
  • további bonyodalmak az aktualizálás során: bizonyos esetekben a tartóssági és az üzleti logika szolgáltatását szinkronban vagy szigorú sorrendben kell frissíteni.

Végül vissza kellett térni a gyökerekhez: az domain adatokat és az üzleti logikát egy mikroszolgáltatásba kellett kapszulázni. Ez a megközelítés egyszerűsíti a mikroszolgáltatás egységes komponensként való érzékelését a rendszerben, és nem okozza az előbb említett problémákat. Ez sem ingyenes:

  • API szabványosítás szükséges az üzleti logikával való interakcióhoz (különösen a felhasználói tevékenységek biztosítása érdekében az üzleti folyamatokon belül) és az API platformszolgáltatásokhoz; figyelmesebben kell kezelni az API változásait, a közvetlen és fordított kompatibilitást;
  • további runtime könyvtárak hozzáadása szükséges az üzleti logika működésének biztosításához ezekben a mikroszolgáltatásokban, és ez új követelményeket támaszt a könyvtárakkal szemben: könnyedség és minimális transzitiv függőségek;
  • A fejlesztőknek figyelniük kell a könyvtárak verzióira: ha egy mikroszolgáltatást régóta nem frissítettek, akkor abban valószínűleg elavult könyvtárverziók találhatók. Ez váratlan akadályt jelenthet egy új funkció hozzáadásánál, és megkövetelheti a régi üzleti logika migrálását az új könyvtárverziókra, ha a verziók között inkompatibilis változások történtek.

BPM stílusú integráció

A platformszolgáltatások rétege ebben az architektúrában szintén jelen van, de ez a réteg már nem a domén üzleti logika végrehajtásához szükséges konténer, hanem csupán annak környezete, amely segéd „platform” funkciókat biztosít. Erre a rétegre nem csupán a domén mikroszolgáltatások könnyűsúlyának megőrzése érdekében van szükség, hanem a központi irányítás fenntartásához is.

Például a felhasználói aktivitások az üzleti folyamatokban feladatokat generálnak. Azonban a felhasználónak a feladatokkal dolgozva látnia kell a feladatokat az összes doménben egy általános listában, tehát szükséges egy megfelelő platformszolgáltatás a feladatok nyilvántartásához, amely tisztában van a domén üzleti logikával. Az üzleti logika kapszulázása ilyen kontextusban meglehetősen problémás, és ez újabb kompromisszumot jelent az adott architektúrában.

Üzleti folyamatok integrációja alkalmazásfejlesztők szempontjából

Ahogyan már említve lett, az alkalmazásfejlesztőnek el kell vonatkoznia a több alkalmazás közötti interakció technikai és mérnöki részleteitől, hogy a fejlesztési produktivitásra lehetősége legyen.

Próbáljuk megoldani egy meglehetősen bonyolult integrációs feladatot, amelyet kifejezetten a cikkhez találtak ki. Ez egy „játékszerű” feladat lesz három alkalmazás részvételével, ahol mindegyik egy meghatározott domén nevet képvisel: „app1”, „app2”, „app3”.

Minden alkalmazáson belül üzleti folyamatok indulnak el, amelyek elkezdik „passzolni a labdát” az integrációs buszon. A „labda” szerepét üzenetek fogják betölteni, amelyek neve „Ball”.

A játék szabályai:

  • az első játékos – a kezdeményező. Ő meghívja a többi játékost a játékba, elkezdi a játékot, és bármikor befejezheti;
  • a többi játékos bejelenti részvételét a játékban, „megismerkedik” egymással és az első játékossal;
  • a játékba fogadva a játékos választ egy másik résztvevő játékost, és átadja neki a labdát. Számon tartjuk az összes átadást;
  • Minden játékosnak van „energiája”, amely csökken, ahogy a labdát átadja. Amikor az energia elfogy, a játékos kilép a játékból, bejelentve távozását;
  • ha a játékos egyedül marad, azonnal bejelenti távozását;
  • amikor minden játékos kilép, az első játékos bejelenti a játék befejezését. Ha ő korábban kilépett, akkor figyelemmel kíséri a játékot, hogy befejezhesse azt.

E feladat megoldásához a mi DSL-ünket fogom használni az üzleti folyamatok számára, amely lehetővé teszi a logika tömör leírását Kotlin nyelven, minimális boilerplate-tel.

Az app1 alkalmazásban az első játékos (ő az játék iniciátora) üzleti folyamata fog működni:

class InitialPlayer

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

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

class PlayersList : ArrayList()

// Ez a folyamat példányának osztálya: encapsulálja a belső állapotát
class InitialPlayer : ProcessImpl(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Ez a folyamatmodell deklarációja: egyszer hozzák létre, és minden
// a megfelelő osztályú folyamat példány használja
val initialPlayerModel = processModel(name = "InitialPlayer",
                                                     version = 1) {

    // A szabályok szerint az első játékos a játék kezdeményezője és egyedüli kell, hogy legyen
    uniqueConstraint = UniqueConstraints.singleton

    // Bejelentjük a tevékenységeket, amelyekből a üzleti folyamat áll
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "${players.size} játékos csatlakozott. Kezdjük el?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... csatlakozott a játékos: ${signal.data} ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... a játékos ${signal.data} kilépett ...")
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("Ball") {
        shotCounter = it
    }

    // Most felépítjük a folyamat grafikonját a meghatározott tevékenységekből
    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)
    }

    // További kezelőket adunk a tevékenységeinkhez a naplózáshoz
    sendNewGameSignal.onExit { println("Játsszunk!") }
    sendStopGameSignal.onExit { println("Megáll!") }
    sendPlayerOut.onExit { println("$playerName: Kiléptem!") }
}

private fun MessageSendInstance.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Lépés ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

A vállalati logika végrehajtása mellett a bemutatott kód képes visszaadni a üzleti folyamat objektummodeljét, amely vizuálisan ábrázolható diagramként. A vizualizálót még nem valósítottuk meg, ezért egy kis időt kellett töltenünk rajzolással (itt egy kicsit leegyszerűsítettem a BPMN jelölést a kapuk használatát illetően, hogy javítsam a diagram és a bemutatott kód összhangját):

BPM stílusú integráció

Az app2 alkalmazás egy másik szereplő üzleti folyamatát fogja tartalmazni:

class RandomPlayer

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

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

class PlayersList: ArrayList()

class RandomPlayer : ProcessImpl(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(name = "RandomPlayer", 
                                                   version = 1) {

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

Diagram:

BPM stílusú integráció

Az app3 alkalmazásban a játékos viselkedését kicsit megváltoztatjuk: a következő játékos véletlenszerű kiválasztása helyett round-robin algoritmus szerint fog működni:

class 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()

class RoundRobinPlayer : ProcessImpl(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(
        name = "RoundRobinPlayer", 
        version = 1) {

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

A játékos viselkedése egyébként nem különbözik az előzőtől, így a diagram nem változik.

Most szükség van egy tesztre, hogy mindezt elindítsuk. Csak a teszt kódját hozom, hogy ne terheljem meg a cikket sablon szöveggel (valójában a korábban más üzleti folyamatok integrációs tesztelésére létrehozott tesztkörnyezetet használtam):

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");
    // Most egy kicsit várni kell, amíg a játékosok "megismerkednek" egymással.
    // Várni a sleep-el - rossz megoldás, de a legegyszerűbb.
    // Ne csináld ezt komoly tesztekben!
    Thread.sleep(1000);
    // Elindítjuk a játékot, lezárva a felhasználói aktivitást
    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 playerParams(String name, int energy) {
    Map params = new HashMap();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Elindítjuk a tesztet, nézzük a naplót:

konzol kimenet

A zár lezárva: lock://app1/process/InitialPlayer
Játék készen áll!
A zár feloldva: lock://app1/process/InitialPlayer
Player2: Itt vagyok!
Player3: Itt vagyok!
Player4: Itt vagyok!
Player5: Itt vagyok!
... csatlakozás a Player2-hez ...
... csatlakozás a Player4-hez ...
... csatlakozás a Player3-hoz ...
... csatlakozás a Player5-höz ...
1. lépés: Player1 >>> Player3
2. lépés: Player3 >>> Player5
3. lépés: Player5 >>> Player3
4. lépés: Player3 >>> Player4
5. lépés: Player4 >>> Player3
6. lépés: Player3 >>> Player4
7. lépés: Player4 >>> Player5
8. lépés: Player5 >>> Player2
9. lépés: Player2 >>> Player5
10. lépés: Player5 >>> Player4
11. lépés: Player4 >>> Player2
12. lépés: Player2 >>> Player4
13. lépés: Player4 >>> Player1
14. lépés: Player1 >>> Player4
15. lépés: Player4 >>> Player3
16. lépés: Player3 >>> Player1
17. lépés: Player1 >>> Player2
18. lépés: Player2 >>> Player3
19. lépés: Player3 >>> Player1
20. lépés: Player1 >>> Player5
21. lépés: Player5 >>> Player1
22. lépés: Player1 >>> Player2
23. lépés: Player2 >>> Player4
24. lépés: Player4 >>> Player5
25. lépés: Player5 >>> Player3
26. lépés: Player3 >>> Player4
27. lépés: Player4 >>> Player2
28. lépés: Player2 >>> Player5
29. lépés: Player5 >>> Player2
30. lépés: Player2 >>> Player1
31. lépés: Player1 >>> Player3
32. lépés: Player3 >>> Player4
33. lépés: Player4 >>> Player1
34. lépés: Player1 >>> Player3
35. lépés: Player3 >>> Player4
36. lépés: Player4 >>> Player3
37. lépés: Player3 >>> Player2
38. lépés: Player2 >>> Player5
39. lépés: Player5 >>> Player4
40. lépés: Player4 >>> Player5
41. lépés: Player5 >>> Player1
42. lépés: Player1 >>> Player5
43. lépés: Player5 >>> Player3
44. lépés: Player3 >>> Player5
45. lépés: Player5 >>> Player2
46. lépés: Player2 >>> Player3
47. lépés: Player3 >>> Player2
48. lépés: Player2 >>> Player5
49. lépés: Player5 >>> Player4
50. lépés: Player4 >>> Player2
51. lépés: Player2 >>> Player5
52. lépés: Player5 >>> Player1
53. lépés: Player1 >>> Player5
54. lépés: Player5 >>> Player3
55. lépés: Player3 >>> Player5
56. lépés: Player5 >>> Player2
57. lépés: Player2 >>> Player1
58. lépés: Player1 >>> Player4
59. lépés: Player4 >>> Player1
60. lépés: Player1 >>> Player4
61. lépés: Player4 >>> Player3
62. lépés: Player3 >>> Player2
63. lépés: Player2 >>> Player5
64. lépés: Player5 >>> Player4
65. lépés: Player4 >>> Player5
66. lépés: Player5 >>> Player1
67. lépés: Player1 >>> Player5
68. lépés: Player5 >>> Player3
69. lépés: Player3 >>> Player4
70. lépés: Player4 >>> Player2
71. lépés: Player2 >>> Player5
72. lépés: Player5 >>> Player2
73. lépés: Player2 >>> Player1
74. lépés: Player1 >>> Player4
75. lépés: Player4 >>> Player1
76. lépés: Player1 >>> Player2
77. lépés: Player2 >>> Player5
78. lépés: Player5 >>> Player4
79. lépés: Player4 >>> Player3
80. lépés: Player3 >>> Player1
81. lépés: Player1 >>> Player5
82. lépés: Player5 >>> Player1
83. lépés: Player1 >>> Player4
84. lépés: Player4 >>> Player5
85. lépés: Player5 >>> Player3
86. lépés: Player3 >>> Player5
87. lépés: Player5 >>> Player2
88. lépés: Player2 >>> Player3
Player2: Ki vagyok!
89. lépés: Player3 >>> Player4
... a Player2 kiesett ...
90. lépés: Player4 >>> Player1
91. lépés: Player1 >>> Player3
92. lépés: Player3 >>> Player1
93. lépés: Player1 >>> Player4
94. lépés: Player4 >>> Player3
95. lépés: Player3 >>> Player5
96. lépés: Player5 >>> Player1
97. lépés: Player1 >>> Player5
98. lépés: Player5 >>> Player3
99. lépés: Player3 >>> Player5
100. lépés: Player5 >>> Player4
101. lépés: Player4 >>> Player5
Player4: Ki vagyok!
... a Player4 kiesett ...
102. lépés: Player5 >>> Player1
103. lépés: Player1 >>> Player3
104. lépés: Player3 >>> Player1
105. lépés: Player1 >>> Player3
106. lépés: Player3 >>> Player5
107. lépés: Player5 >>> Player3
108. lépés: Player3 >>> Player1
109. lépés: Player1 >>> Player3
110. lépés: Player3 >>> Player5
111. lépés: Player5 >>> Player1
112. lépés: Player1 >>> Player3
113. lépés: Player3 >>> Player5
114. lépés: Player5 >>> Player3
115. lépés: Player3 >>> Player1
116. lépés: Player1 >>> Player3
117. lépés: Player3 >>> Player5
118. lépés: Player5 >>> Player1
119. lépés: Player1 >>> Player3
120. lépés: Player3 >>> Player5
121. lépés: Player5 >>> Player3
Player5: Ki vagyok!
... a Player5 kiesett ...
122. lépés: Player3 >>> Player5
123. lépés: Player5 >>> Player1
Player5: Ki vagyok!
124. lépés: Player1 >>> Player3
... a Player5 kiesett ...
125. lépés: Player3 >>> Player1
126. lépés: Player1 >>> Player3
Player1: Ki vagyok!
... a Player1 kiesett ...
127. lépés: Player3 >>> Player3
Player3: Ki vagyok!
Lépj le!
128. lépés: Player3 >>> Player3
... a Player3 kiesett ...
Player3: Ki vagyok!
Megállj!
129. lépés: Player3 >>> Player3
Player3: Ki vagyok!

Ebből néhány fontos következtetést lehet levonni:

  • a megfelelő eszközök birtokában az alkalmazásfejlesztők üzleti logikától függetlenül hozhatnak létre integrációs interakciókat;
  • az integrációs feladat bonyolultsága, amely mérnöki kompetenciákat igényel, elrejtető a keretrendszeren belül, ha azt eredetileg a keretrendszer architektúrájában fektették le. A feladat nehézségét azonban nem lehet elrejteni, így a nehéz feladat megoldása a kódban ennek megfelelően fog kinézni;
  • az integrációs logika fejlesztése során必 考虑 a végső konzisztenciát és a résztvevők állapotváltozásának linearizálhatatlanságát. Ez a logika bonyolítására kényszerít, hogy érzéketlenné váljon a külső események bekövetkezésének sorrendjével szemben. Példánkban a játékosnak részt kell vennie a játékban, már miután bejelentette kilépését: más játékosok továbbra is passzolni fogják neki a labdát, amíg az információ a kilépéséről el nem jut és el nem kerül minden résztvevőhöz. Ez a logika nem a játék szabályaiból ered, hanem kompromisszumos megoldás a választott architektúra keretein belül.

Most a megoldásunk különböző finomságairól, kompromisszumairól és egyéb részleteiről fogunk beszélni.

Minden üzenet – egy sorban

Minden integrált alkalmazás egy közös integrációs buszon működik, amely külső brókerként jelenik meg, egy BPMQueue nevű üzenetsorra és egy BPMTopic nevű topikra (események) vonatkozik. Minden üzenet egy soron való átkonvertálása önmagában is kompromisszumot jelent. Az üzleti logika szintjén most annyi új üzenet típust vehetünk be, amennyit csak akarunk, anélkül, hogy változtatnánk a rendszer szerkezetén. Ez jelentős egyszerűsítést jelent, de bizonyos kockázatokat is hordoz magában, amelyek a szokásos feladataink kontextusában nem tűntek olyan jelentősnek.

BPM stílusú integráció

Egy fontos részlet azonban van: minden alkalmazás a saját domain neve alapján szűri a "saját" üzeneteit a bejövő sorból. A domain név a jelzésekben is megadható, ha szükséges korlátozni a jelzés "láthatóságát" egyetlen alkalmazásra. Ennek növelnie kell a busz áteresztőképességét, de az üzleti logikának mostantól domain nevek szerint kell eljárnia: üzenetek címzésére – kötelező, jelzések esetén – ajánlott.

Az integrációs busz megbízhatóságának biztosítása

A megbízhatóság több tényezőből áll:

  • a kiválasztott üzenetközvetítő – kritikus fontosságú komponens az architektúrában és egyetlen hibaforrás: elég megbízhatónak kell lennie. Csak megbízható implementációkat célszerű használni, jó támogatással és nagy közösséggel;
  • biztosítani kell a üzenetközvetítő magas rendelkezésre állását, amihez fizikailag el kell választani az integrált alkalmazásoktól (az alkalmazások magas rendelkezésre állásának biztosítása sokkal bonyolultabb és költségesebb);
  • a közvetítőnek garantálnia kell az "at least once" üzenetküldés garanciáját. Ez kötelező feltétel az integrációs busz megbízható működéséhez. Az "exactly once" garanciákra nincs szükség: az üzleti folyamatok általában nem érzékenyek az üzenetek vagy események megismételt beérkezésére, és külön feladatokban, ahol ez fontos, egyszerűbb plusz ellenőrzést beépíteni az üzleti logikába, mint állandóan "drága" garanciákat használni;
  • az üzenetek és jelzések küldését be kell vonni az üzleti folyamatok állapotának és domain adataik módosításának költségvetett tranzikciójába. Az előnyben részesített megoldás a Transactional Outbox, de ez további táblázat létrehozását és retrécreket igényel. JEE alkalmazásokban ezt a problémát a helyi JTA menedzser használatával egyszerűsíthetjük, de a kiválasztott közvetítőhöz való csatlakozásnak képesnek kell lennie működni XA;
  • a bejövő üzenetek és események feldolgozóinak is részt kell vennie az üzleti folyamat állapotának módosításának tranzikciójában: ha ilyen tranzikció visszaáll, akkor az üzenet átvétele is vissza kell álljon;
  • azokat az üzeneteket, amelyeket nem sikerült kézbesíteni hibák miatt, külön tárolóba kell helyezni DLQ (Holt levél sor). A célunkkal egy külön platformmikroszervizt hoztunk létre, amely ilyen üzeneteket tárol a saját adattárolójában, attribútumok szerint indexeli őket (gyors csoportosítás és keresés érdekében), és API-t biztosít a megtekintésükhöz, újbóli elküldésükhöz a címzett felé, valamint az üzenetek törléséhez. A rendszer adminisztrátorai a webes felületen keresztül dolgozhatnak e szolgáltatással;
  • a broker beállításaiban a kiszállítási kísérletek számát és a kiszállítások közötti késleltetést kell beállítani, hogy csökkentsük az üzenetek DLQ-ba kerülésének valószínűségét (az optimális paraméterek kiszámítása gyakorlatilag lehetetlen, de empirikus alapon működhetünk és azokat az üzemeltetés során finomíthatjuk);
  • a DLQ tárolóját folyamatosan monitorozni kell, és a monitorozó rendszernek értesítenie kell a rendszer adminisztrátorait, hogy a nem kézbesített üzenetek megjelenésekor minél gyorsabban reagáljanak. Ez csökkenti a fellépő hiba vagy üzleti logika problémájának 'hatókörét';
  • az integrációs busznak érzéketlennek kell lennie az alkalmazások ideiglenes hiányára: a topik előfizetéseknek tartósnak kell lenniük, és az alkalmazás domainnevének egyedinek kell lennie, hogy az alkalmazás hiánya alatt az üzeneteket senki más ne próbálja meg feldolgozni a sorból.

Az üzleti logika szálbiztonságának biztosítása

Azonos üzleti folyamat példányhoz egyszerre több üzenet és esemény is érkezhet, amelyek párhuzamosan indulnak el. Ugyanakkor a fejlesztők számára mindennek egyszerűnek és szálbiztosnak kell lennie.

Az üzleti folyamat logikája minden külső eseményt, amely hatással van erre az üzleti folyamatra, külön-külön dolgoz fel. Ilyen események lehetnek:

  • az üzleti folyamat példányának elindítása;
  • a felhasználó tevékenysége, amely a folyamaton belüli aktivitással kapcsolatos;
  • üzenet vagy jel érkezése, amelyre az üzleti folyamat példánya fel van iratkozva;
  • az üzleti folyamat példánya által beállított időzítő aktiválása;
  • irányító hatások API-n keresztül (például a folyamat vészleállítása).

Minden ilyen esemény megváltoztathatja az üzleti folyamat példányának állapotát: egyes tevékenységek befejeződhetnek, míg mások elkezdődhetnek, és a perzisztens attribútumok értékei is megváltozhatnak. Bármely tevékenység lezárása egy vagy több következő tevékenység aktiválásához vezethet. Ezek viszont várakozhatnak más eseményekre, vagy ha nincs szükségük további adatokra, ugyanabban a tranzakcióban befejeződhetnek. A tranzakció lezárása előtt az üzleti folyamat új állapotát az adatbázisba mentjük, ahol a következő külső esemény bekövetkezésére vár.

A perzisztens üzleti folyamat adatai, amelyek a relációs adatbázisba vannak mentve, nagyon kényelmes szinkronizálási pontot jelentenek a feldolgozás során, ha SELECT FOR UPDATE parancsot használunk. Ha egy tranzakciónak sikerült megszereznie az üzleti folyamat állapotát a változtatás érdekében, akkor egy másik tranzakció párhuzamosan nem kaphatja meg ugyanezt az állapotot más változtatásra, és az első tranzakció befejezése után a második garantáltan már a megváltozott állapotot fogja megkapni.

A lehetséges korlátozások alkalmazása a DBMS oldalán biztosítja, hogy minden szükséges követelményt teljesítsünk ACID, ugyanakkor megtartja a lehetőséget az alkalmazás üzleti logikájának skálázására az elindított példányok számának növelésével.

Ugyanakkor a pesszimista zárolások kockázatokat hordoznak a holtpontokkal kapcsolatban, ezért a SELECT FOR UPDATE parancsot érdemes egy ésszerű időkorlát alá helyezni, hogy elkerüljük a holtpontokat a súlyos esetekben.

Egy másik probléma az üzleti folyamat elindításának szinkronizálása. Amíg nincs az üzleti folyamatnak példánya, nincs az adatbázisban állapot sem, ezért a leírt módszer nem alkalmazható. Ha a példány egyediségét egy adott körben biztosítani kell, akkor szükség lesz egy szinkronizáló objektumra, amely összekapcsolódik a folyamat osztályával és az adott körrel. Ennek a problémának a megoldására egy másik zárolási mechanizmust alkalmazunk, amely lehetővé teszi bármilyen erőforrás zárolását, amelyet URI formátumban határozunk meg, külső szolgáltatáson keresztül.

Példáinkban az üzleti folyamat InitialPlayer tartalmaz egy nyilatkozatot

uniqueConstraint = UniqueConstraints.singleton

Ezért a naplóban megtalálhatók a releváns kulcs zárának feloldásával és megszerzésével kapcsolatos üzenetek. Más üzleti folyamatok esetében ilyen üzenetek nincsenek: uniqueConstraint nincs megadva.

Üzleti folyamatok problémái tartós állapotban

Néha a tartós állapot nemcsak segít, hanem nagyon is hátr way-t jelent a fejlesztés során.
A problémák akkor kezdődnek, amikor változtatásokat kell végezni az üzleti logikában és/vagy az üzleti folyamat modelljében. Nem minden ilyen változtatás kompatibilis a régi üzleti folyamatok állapotával. Ha az adatbázisban sok "élő" példány található, akkor az inkompatibilis változtatások végrehajtása rengeteg kellemetlenséget okozhat, amivel gyakran szembesültünk a jBPM használata során.

A változtatások mélységétől függően két utat lehet követni:

  1. új üzleti folyamat típust hozunk létre, hogy ne kelljen inkompatibilis változtatásokat végrehajtanunk a régin, és ezt használjuk a régi helyett új példányok indításakor. A régi példányok a "régi módon" fognak működni;
  2. migráljuk a tartós állapotot az üzleti logika frissítésekor.

Az első út egyszerűbb, de megvannak a maga korlátai és hátrányai, például:

  • az üzleti logika duplikálása sok üzleti folyamat modellben, az üzleti logika volumenének növekedése;
  • gyakran szükségek az új üzleti logikára való azonnali áttérés (az integrációs feladatok esetében – szinte mindig);
  • a fejlesztő nem tudja, mikor lehet törölni a elavult modelleket.

A gyakorlatban mindkét megközelítést alkalmazzuk, de számos döntést hoztunk, hogy megkönnyítsük az életünket:

  • az adatbázisban az üzleti folyamat tartós állapota könnyen olvasható és könnyen feldolgozható formában van tárolva: JSON formátumú sorban. Ez lehetővé teszi a migrációk végrehajtását az alkalmazáson belül és kívül egyaránt. Végső esetben manuálisan is módosítható (különösen hasznos a fejlesztés során a hibakeresés alatt);
  • az integrációs üzleti logika nem használja az üzleti folyamatok neveit, hogy bármikor lecserélhessük az egyik részt vevő folyamat megvalósítását egy újra, új névvel (például „InitialPlayerV2”). Az összekapcsolás üzenetek és jelek nevein keresztül történik;
  • A folyamatmodell verziószámmal rendelkezik, amelyet növelünk, ha inkompatibilis változásokat hajtunk végre ebben a modellben, és ez a szám a folyamatpéldány állapotával együtt megmarad.
  • A folyamat tartós állapota a databázisból először kényelmes objektummodellé olvasható ki, amellyel a migrációs eljárás dolgozhat, ha a modell verziószáma megváltozott.
  • A migrációs eljárás a üzleti logikával közel helyezkedik el, és 'lustán' hívják meg a folyamat üzleti esetének minden példányához, amikor az adatbázisból helyreállítják.
  • Ha a folyamat összes példányának állapotát gyorsan és szinkron módon kell migrálni, klasszikusabb adatbázis-migráló megoldásokat alkalmaznak, de ott JSON-nal kell dolgozni.

Szükség van-e még egy üzleti folyamat keretrendszerre?

A cikkben ismertetett megoldások lehetővé tették számunkra, hogy jelentősen megkönnyítsük az életünket, bővítsük a kérdések körét, amelyeket alkalmazásfejlesztési szinten megoldunk, vonzóbbá tegyük az üzleti logika mikroszolgáltatásokba való kiemelésének ötleteit. Ehhez sok munkát végeztünk, létrehoztunk egy nagyon 'könnyű' keretrendszert az üzleti folyamatokhoz, valamint segédkomponenseket a problémák megoldására a széles alkalmazási feladatok kontextusában. Szeretnénk megosztani ezeket az eredményeket, és a közös komponensek fejlesztését nyílt hozzáférésűvé tenni szabad licenc alatt. Ez bizonyos erőfeszítéseket és időt igényel. Az ilyen megoldások iránti kereslet megértése további ösztönzést jelentene számunkra. A javasolt cikkben nagyon kevés figyelmet szentelnek a keretrendszer lehetőségeinek, de néhány közvetett példa látható. Ha mégis közzétesszük a keretrendszerünket, külön cikk lesz neki szentelve. De egyelőre hálásak leszünk, ha hagy egy rövid visszajelzést, válaszolva a következő kérdésre:

Csak a regisztrált felhasználók vehetnek részt a szavazáson. Kérem, jelentkezzen be., kérem.

Szükség van-e még egy üzleti folyamat keretrendszerre?

  • 18,8%Igen, régóta keresünk valami hasonlót.

  • 12,5%Érdekes lenne többet megtudni a megvalósításáról, hasznos lehet.

  • 6,2%Használunk egy meglévő keretrendszert, de gondolkodunk a cserén.

  • 18,8%Használunk egy meglévő keretrendszert, minden rendben van.

  • 18,8%Keretrendszer nélkül is boldogulunk.

  • 25,0%Sajátot írunk.

16 felhasználó szavazott. 7 felhasználó tartózkodott.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster