BPM stílus integráció

BPM stílus integráció

Helló, Habr!

Cégünk ERP-osztályú szoftvermegoldások fejlesztésére specializálódott, melynek oroszlánrészét a hatalmas üzleti logikával és dokumentumáramlással rendelkező tranzakciós rendszerek a la EDMS foglalják el. Termékeink jelenlegi verziói a JavaEE technológiákon alapulnak, de aktívan kísérletezünk mikroszolgáltatásokkal is. Az ilyen megoldások egyik legproblémásabb területe a szomszédos tartományokhoz tartozó különféle alrendszerek integrálása. Az integrációs problémák mindig is óriási fejtörést okoztak nekünk, függetlenül az általunk használt építészeti stílusoktól, technológiai halmazoktól és keretrendszerektől, de az utóbbi időben előrelépés történt az ilyen problémák megoldásában.

A figyelmedbe ajánlom cikkben az NPO Krista tapasztalatairól és építészeti kutatásairól fogok beszélni a kijelölt területen. Megnézünk egy példát egy integrációs probléma egyszerű megoldására is egy alkalmazásfejlesztő szemszögéből, és megtudjuk, mi rejtőzik e mögött az egyszerűség mögött.

Jogi nyilatkozat

A cikkben ismertetett építészeti és műszaki megoldásokat személyes tapasztalatok alapján, konkrét feladatok kapcsán javasoltam. Ezek a megoldások nem állítják, hogy univerzálisak, és nem biztos, hogy optimálisak más felhasználási feltételek mellett.

Mi köze a BPM-nek ehhez?

A kérdés megválaszolásához egy kicsit mélyebben kell elmélyednünk megoldásaink alkalmazott problémáinak sajátosságaiban. Az üzleti logika fő része tipikus tranzakciós rendszerünkben az adatok bevitele az adatbázisba felhasználói felületeken keresztül, ezen adatok manuális és automatizált ellenőrzése, valamilyen munkafolyamaton keresztül történő lebonyolítása, közzététele másik rendszerben / elemző adatbázisban / archívumban, riportok készítése . Így az ügyfelek számára a rendszer kulcsfontosságú funkciója belső üzleti folyamataik automatizálása.

A kényelem kedvéért a „dokumentum” kifejezést a kommunikációban egy olyan adathalmaz absztrakciójaként használjuk, amelyet egy közös kulcs egyesít, és amelyhez egy bizonyos munkafolyamat „kapcsolható”.
De mi a helyzet az integrációs logikával? Hiszen az integrációs feladatot a rendszer architektúrája generálja, amit NEM a megrendelő kérésére, hanem teljesen más tényezők hatására „vágnak” részekre:

  • a Conway-törvény hatálya alá tartozik;
  • a korábban más termékekhez kifejlesztett alrendszerek újrafelhasználásának eredményeként;
  • az építész belátása szerint, nem funkcionális követelmények alapján.

Nagy a kísértés az integrációs logikának a fő munkafolyamat üzleti logikájától való elkülönítésére, hogy ne szennyezzük be az üzleti logikát integrációs műtermékekkel, és megóvjuk az alkalmazásfejlesztőt attól, hogy elmélyüljön a rendszer architektúrájának jellemzőiben. Ennek a megközelítésnek számos előnye van, de a gyakorlat azt mutatja, hogy nem hatékony:

  • az integrációs problémák megoldása általában a legegyszerűbb lehetőségekre esik vissza szinkron hívások formájában a fő munkafolyamat megvalósításának korlátozott kiterjesztési pontjai miatt (a szinkron integráció hátrányait az alábbiakban tárgyaljuk);
  • az integrációs melléktermékek továbbra is behatolnak az alapvető üzleti logikába, ha egy másik alrendszertől visszajelzésre van szükség;
  • az alkalmazásfejlesztő figyelmen kívül hagyja az integrációt, és a munkafolyamat megváltoztatásával könnyen megszakíthatja azt;
  • a rendszer a felhasználó szempontjából megszűnik egységes egész lenni, észrevehetővé válnak az alrendszerek közötti „varratok”, és megjelennek a redundáns felhasználói műveletek, amelyek elindítják az adatok egyik alrendszerből a másikba való átvitelét.

Egy másik megközelítés szerint az integrációs interakciókat az alapvető üzleti logika és munkafolyamat szerves részének tekintjük. Annak elkerülése érdekében, hogy az alkalmazásfejlesztői képesítések az egekbe szökjenek, az új integrációs interakciók létrehozásának egyszerűnek és könnyednek kell lennie, minimális lehetőséggel a megoldás kiválasztására. Ezt nehezebb megtenni, mint amilyennek látszik: az eszköznek elég erősnek kell lennie ahhoz, hogy a felhasználó számára a szükséges lehetőségek széles választékát biztosítsa anélkül, hogy „lábba lőhetné magát”. Számos kérdésre kell válaszolnia egy mérnöknek az integrációs feladatok kapcsán, de amelyekre az alkalmazásfejlesztőnek nem szabad gondolnia a napi munkája során: tranzakciós határok, konzisztencia, atomitás, biztonság, skálázás, terhelés és erőforrás elosztás, útválasztás, rendezés disztribúciós és váltási kontextusok stb. Az alkalmazásfejlesztőknek elég egyszerű megoldássablonokat kell kínálnunk, amelyekben minden ilyen kérdésre a válasz már el van rejtve. Ezeknek a sablonoknak meglehetősen biztonságosaknak kell lenniük: az üzleti logika nagyon gyakran változik, ami növeli a hibalehetőségek kockázatát, a hibák költségének meglehetősen alacsony szinten kell maradnia.

De mi köze a BPM-nek ehhez? Számos lehetőség van a munkafolyamat megvalósítására...
Valóban, az üzleti folyamatok egy másik megvalósítása nagyon népszerű megoldásainkban - az állapotátmenet diagram deklaratív definíciója és a kezelők összekapcsolása az üzleti logikával az átmenetekhez. Ebben az esetben az állapot, amely meghatározza a „dokumentum” aktuális pozícióját az üzleti folyamatban, magának a „dokumentumnak” az attribútuma.

BPM stílus integráció
Így néz ki a folyamat egy projekt elején

Ennek a megvalósításnak a népszerűsége a lineáris üzleti folyamatok létrehozásának viszonylagos egyszerűségének és gyorsaságának köszönhető. Ahogy azonban a szoftverrendszerek egyre bonyolultabbá válnak, az üzleti folyamat automatizált része növekszik és egyre bonyolultabbá válik. Szükség van a folyamatok részeinek dekompozíciójára, újrafelhasználására, valamint a folyamatok elágazására, hogy az egyes ágak párhuzamosan történjenek. Ilyen körülmények között az eszköz kényelmetlenné válik, és az állapotátmenet diagram elveszti információtartalmát (az integrációs kölcsönhatások egyáltalán nem jelennek meg a diagramon).

BPM stílus integráció
Így néz ki a folyamat a követelmények pontosításának többszöri iterációja után.

A kiutat ebből a helyzetből a motor integrálása jelentette jBPM egyes, a legösszetettebb üzleti folyamatokkal rendelkező termékekbe. Rövid távon ez a megoldás némi sikert aratott: lehetővé vált összetett üzleti folyamatok megvalósítása, miközben a jelölésben egy meglehetősen informatív és releváns diagramot tartottak fenn. BPMN2.

BPM stílus integráció
Egy összetett üzleti folyamat kis része

A megoldás hosszú távon nem váltotta be a hozzá fűzött reményeket: az üzleti folyamatok vizuális eszközökkel történő létrehozásának magas munkaintenzitása nem tette lehetővé az elfogadható termelékenységi mutatók elérését, maga az eszköz pedig az egyik legellenszenvesebb lett a fejlesztők körében. A motor belső szerkezetére is panaszkodtak, ami sok „folt” és „mankó” megjelenéséhez vezetett.

A jBPM használatának fő pozitívuma az volt, hogy tudatában volt annak, milyen előnyökkel és ártalmakkal jár az üzleti folyamatpéldány saját állandó állapota. Megláttuk annak lehetőségét is, hogy folyamatszemléletű megközelítést alkalmazzunk komplex integrációs protokollok megvalósítására a különböző alkalmazások között, jeleken és üzeneteken keresztüli aszinkron interakciók segítségével. A tartós állapot jelenléte ebben döntő szerepet játszik.

A fentiek alapján megállapíthatjuk: A BPM-stílusú folyamatszemlélet lehetővé teszi a feladatok széles skálájának megoldását az egyre bonyolultabb üzleti folyamatok automatizálására, az integrációs tevékenységek harmonikus illesztésére ezekbe a folyamatokba, valamint a megvalósított folyamat megfelelő jelöléssel történő vizuális megjelenítésének lehetőségét.

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

A szinkron integráció a legegyszerűbb blokkoló hívásra utal. Az egyik alrendszer a kiszolgálóoldal szerepét tölti be, és a szükséges metódussal kiteszi az API-t. Egy másik alrendszer ügyféloldalként működik, és a megfelelő időben hívást kezdeményez, és várja az eredményt. A rendszer architektúrától függően a kliens és a szerver oldal elhelyezkedhet ugyanabban az alkalmazásban és folyamatban, vagy különbözőekben. A második esetben valamilyen RPC implementációt kell alkalmaznia, és biztosítania kell a paraméterek és a hívás eredményének rendezését.

BPM stílus integráció

Ennek az integrációs mintának meglehetősen sok hátránya van, de egyszerűsége miatt nagyon széles körben alkalmazzák a gyakorlatban. A megvalósítás gyorsasága magával ragad, és a sürgető határidők ellenére újra és újra igénybevételre kényszerít, műszaki adósságként rögzítve a megoldást. De az is előfordul, hogy a tapasztalatlan fejlesztők öntudatlanul használják, egyszerűen nem veszik észre a negatív következményeket.

Az alrendszerek összekapcsolhatóságának legnyilvánvalóbb növekedése mellett a „növekvő” és „nyúló” tranzakcióknál kevésbé nyilvánvaló problémák is vannak. Valójában, ha az üzleti logika bizonyos változtatásokat hajt végre, akkor a tranzakciók nem kerülhetők el, és a tranzakciók viszont blokkolnak bizonyos alkalmazási erőforrásokat, amelyeket ezek a változások érintenek. Vagyis amíg az egyik alrendszer nem vár választ a másiktól, addig nem tudja befejezni a tranzakciót és eltávolítani a zárakat. Ez jelentősen növeli a különféle hatások kockázatát:

  • A rendszer érzékenysége elvész, a felhasználók sokáig várnak a kérdésekre adott válaszokra;
  • a szerver általában nem válaszol a felhasználói kérésekre a túlzsúfolt szálkészlet miatt: a szálak többsége egy tranzakció által elfoglalt erőforráshoz van zárva;
  • Holtpontok kezdenek megjelenni: bekövetkezésük valószínűsége erősen függ a tranzakciók időtartamától, az üzleti logika és a tranzakcióban rejlő zárak mennyiségétől;
  • tranzakció időtúllépési hibák jelennek meg;
  • a szerver „meghibásodik” az OutOfMemory-val, ha a feladat nagy mennyiségű adat feldolgozását és megváltoztatását igényli, és a szinkron integrációk jelenléte nagyon megnehezíti a feldolgozás „könnyebb” tranzakciókra való felosztását.

Építészeti szempontból a blokkoló hívások alkalmazása az integráció során az egyes alrendszerek minősége feletti kontroll elvesztéséhez vezet: lehetetlen biztosítani az egyik alrendszer célminőségi mutatóit egy másik alrendszer minőségi mutatóitól elkülönítve. Ha az alrendszereket különböző csapatok fejlesztik, az nagy probléma.

A dolgok még érdekesebbek, ha az integrált alrendszerek különböző alkalmazásokban vannak, és mindkét oldalon szinkron változtatásokat kell végrehajtani. Hogyan biztosítható ezeknek a változtatásoknak a tranzakciós jellege?

Ha a változtatásokat külön tranzakciókban hajtják végre, akkor megbízható kivételkezelést és kompenzációt kell biztosítania, és ez teljesen kiküszöböli a szinkron integrációk fő előnyét - az egyszerűséget.

Az elosztott tranzakciók is eszembe jutnak, de ezeket nem használjuk megoldásainkban: nehéz a megbízhatóságot biztosítani.

"Saga" a tranzakciós probléma megoldásaként

A mikroszolgáltatások népszerűségének növekedésével a kereslet a Saga minta.

Ez a minta tökéletesen megoldja a hosszú tranzakciók fentebb említett problémáit, és az üzleti logika oldaláról is kibővíti a rendszer állapotkezelésének lehetőségeit: a sikertelen tranzakció utáni kompenzáció nem tudja visszaállítani a rendszert eredeti állapotába, hanem alternatív adatfeldolgozási útvonal. Ez azt is lehetővé teszi, hogy elkerülje a sikeresen befejezett adatfeldolgozási lépések megismétlését, amikor megpróbálja a folyamatot „jó” befejezni.

Érdekes módon ez a mintázat a monolitikus rendszerekben a lazán csatolt alrendszerek integrálásakor is releváns, és a hosszan tartó tranzakciók és a megfelelő erőforrás-zárak okozta negatív hatások figyelhetők meg.

A BPM-stílusú üzleti folyamatainkkal kapcsolatban a „Saga” megvalósítása nagyon egyszerűnek bizonyul: a „Saga” egyes lépései az üzleti folyamaton belüli tevékenységként adhatók meg, és az üzleti folyamat állandó állapota is. meghatározza a „Saga” belső állapotát. Vagyis nincs szükségünk semmilyen további koordinációs mechanizmusra. Mindössze egy üzenetközvetítőre van szüksége, aki „legalább egyszeri” garanciákat támogat szállításként.

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

  • az üzleti logika bonyolultabbá válik: kompenzációt kell kidolgozni;
  • el kell hagyni a teljes konzisztenciát, ami különösen érzékeny lehet a monolit rendszerekre;
  • Az architektúra kissé bonyolultabbá válik, és további szükség van egy üzenetközvetítőre;
  • további felügyeleti és adminisztrációs eszközökre lesz szükség (bár ez általában jó: a rendszerszolgáltatás minősége javul).

A monolitikus rendszerek esetében a "Sag" használatának indoklása nem olyan nyilvánvaló. A mikroszolgáltatások és más SOA esetében, ahol nagy valószínűséggel már van bróker, és a teljes konzisztenciát a projekt kezdetén feláldozzák, ennek a mintának az előnyei jelentősen meghaladják a hátrányokat, különösen akkor, ha az üzleti logikában van egy kényelmes API szint.

Az üzleti logika beágyazása mikroszolgáltatásokba

Amikor elkezdtünk kísérletezni a mikroszolgáltatásokkal, felmerült egy jogos kérdés: hova helyezzük el a domain üzleti logikáját a domain adatok fennmaradását biztosító szolgáltatással kapcsolatban?

A különféle BPMS-ek architektúráját tekintve ésszerűnek tűnhet az üzleti logika és a perzisztencia elkülönítése: platform- és tartományfüggetlen mikroszolgáltatások rétegének létrehozása, amely környezetet és konténert képez a tartományi üzleti logika végrehajtásához, és a tartományi adatok perzisztenciáját úgy kell megtervezni, egy külön réteg nagyon egyszerű és könnyű mikroszolgáltatások. Az üzleti folyamatok ebben az esetben a perzisztencia réteg szolgáltatásainak hangszerelését végzik.

BPM stílus integráció

Ennek a megközelítésnek van egy nagyon nagy előnye: tetszés szerint növelheti a platform funkcionalitását, és ettől csak a platform mikroszolgáltatásainak megfelelő rétege lesz „hízott”. Bármely tartomány üzleti folyamatai azonnal használhatják a platform új funkcióit, amint az frissítésre kerül.

Egy részletesebb tanulmány feltárta ennek a megközelítésnek a jelentős hátrányait:

  • egy platformszolgáltatás, amely egyszerre több tartomány üzleti logikáját hajtja végre, egyetlen hibapontként nagy kockázatokat rejt magában. Az üzleti logika gyakori változtatásai növelik a rendszerszintű hibákhoz vezető hibák kockázatát;
  • teljesítményproblémák: az üzleti logika keskeny és lassú felületen keresztül dolgozik az adataival:
    • az adatokat ismét sorba rendezik és átpumpálják a hálózati veremen;
    • a tartományi szolgáltatás gyakran több adatot biztosít, mint amennyi az üzleti logika feldolgozásához szükséges, mivel a szolgáltatás külső API-jának szintjén nincs elegendő kapacitás a kérések paraméterezéséhez;
    • több független üzleti logika ismételten újra kérheti ugyanazokat az adatokat feldolgozásra (ez a probléma enyhíthető az adatokat gyorsítótárazó munkamenet-összetevők hozzáadásával, de ez tovább bonyolítja az architektúrát, és problémákat okoz az adatok relevanciájával és a gyorsítótár érvénytelenítésével kapcsolatban);
  • tranzakciós problémák:
    • A platformszolgáltatás által tárolt állandó állapotú üzleti folyamatok nem konzisztensek a tartományi adatokkal, és nincs egyszerű módja ennek a probléma megoldásának;
    • domain adatblokkolás elhelyezése a tranzakción kívül: ha az aktuális adatok helyességének első ellenőrzése után a domain üzleti logikáját módosítani kell, ki kell zárni a feldolgozott adatok versengő változásának lehetőségét. A külső adatblokkolás segíthet a probléma megoldásában, de egy ilyen megoldás további kockázatokkal jár, és csökkenti a rendszer általános megbízhatóságát;
  • további nehézségek a frissítés során: bizonyos esetekben a perzisztencia szolgáltatást és az üzleti logikát szinkronban vagy szigorú sorrendben kell frissíteni.

Végül vissza kellett térnünk az alapokhoz: a tartományadatokat és a tartományi üzleti logikát egyetlen mikroszolgáltatásba foglalni. Ez a megközelítés leegyszerűsíti a mikroszolgáltatásnak a rendszer szerves összetevőjeként való felfogását, és nem veti fel a fenti problémákat. Ezt sem adják ingyen:

  • 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 üzleti folyamatok részeként történő biztosításához) és az API platformszolgáltatásokhoz; nagyobb figyelmet igényel az API módosításai, az előre és visszafelé kompatibilitás;
  • további futásidejű könyvtárak hozzáadása szükséges az üzleti logika működésének biztosításához minden ilyen mikroszolgáltatás részeként, és ez új követelményeket vet fel az ilyen könyvtárakkal szemben: könnyűség és minimális tranzitív függőségek;
  • Az üzleti logika fejlesztőinek figyelniük kell a könyvtári verziókat: ha egy mikroszolgáltatás hosszú ideig nincs véglegesítve, akkor valószínűleg a könyvtárak elavult verzióját fogja tartalmazni. Ez váratlan akadály lehet egy új szolgáltatás hozzáadása előtt, és szükség lehet egy ilyen szolgáltatás régi üzleti logikájának áttelepítésére a könyvtárak új verzióira, ha a verziók között nem kompatibilis változások történtek.

BPM stílus integráció

A platformszolgáltatások egy rétege is jelen van egy ilyen architektúrában, de ez a réteg már nem képez tárolót a tartományi üzleti logika végrehajtásához, hanem csak a környezetét, amely kiegészítő „platform” funkciókat biztosít. Egy ilyen rétegre nemcsak a tartományi mikroszolgáltatások könnyű jellegének megőrzéséhez, hanem a felügyelet központosításához is szükség van.

Például az üzleti folyamatokban végzett felhasználói tevékenységek feladatokat generálnak. A feladatokkal végzett munka során azonban a felhasználónak látnia kell a feladatokat az összes tartományból az általános listában, ami azt jelenti, hogy léteznie kell egy megfelelő platformfeladat regisztrációs szolgáltatásnak, amely mentes a tartomány üzleti logikájától. Az üzleti logika beágyazottságának fenntartása ilyen kontextusban meglehetősen problematikus, és ez ennek az architektúrának egy másik kompromisszuma.

Üzleti folyamatok integrációja az alkalmazásfejlesztő szemével

Mint fentebb említettük, az alkalmazásfejlesztőnek elvonatkoztatnia kell a több alkalmazás interakciójának megvalósításának műszaki és mérnöki jellemzőitől, hogy jó fejlesztési termelékenységre számíthasson.

Próbáljunk meg megoldani egy meglehetősen nehéz integrációs problémát, amelyet kifejezetten a cikkhez találtak ki. Ez egy „játék” feladat lesz, három alkalmazásból, ahol mindegyik meghatároz egy bizonyos domain nevet: „app1”, „app2”, „app3”.

Az egyes alkalmazásokon belül olyan üzleti folyamatok indulnak el, amelyek elkezdenek „labdázni” az integrációs buszon keresztül. A „Ball” nevű üzenetek labdaként működnek.

Játékszabályok:

  • az első játékos a kezdeményező. Meghívja a többi játékost a játékba, elindítja a játékot és bármikor befejezheti;
  • a többi játékos bejelenti részvételét a játékban, „megismeri” egymást és az első játékost;
  • a labda átvétele után a játékos kiválaszt egy másik résztvevő játékost, és átadja neki a labdát. A rendszer megszámolja az adások teljes számát;
  • Minden játékosnak van "energiája", amely az adott játékos minden egyes labdapasszával csökken. Amikor az energia elfogy, a játékos kilép a játékból, és bejelenti lemondását;
  • ha a játékos magára marad, azonnal bejelenti távozását;
  • Amikor minden játékos kiesik, az első játékos befejezettnek nyilvánítja a játékot. Ha korán elhagyja a játékot, továbbra is követnie kell a játékot, hogy befejezze azt.

Ennek a problémának a megoldására az üzleti folyamatokhoz használható DSL-ünket fogom használni, ami lehetővé teszi, hogy a Kotlin-beli logikát tömören, minimális feldolgozás mellett leírjuk.

Az első játékos (más néven a játék kezdeményezője) üzleti folyamata az app1 alkalmazásban fog működni:

osztályú 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}")
}

A fenti kód az üzleti logika végrehajtásán túl egy üzleti folyamat objektummodelljét is előállíthatja, amely diagram formájában megjeleníthető. Még nem valósítottuk meg a megjelenítőt, így egy kis időt kellett szánnunk a rajzolásra (itt kissé leegyszerűsítettem a kapuk használatára vonatkozó BPMN jelölést, hogy javítsam a diagram konzisztenciáját az alábbi kóddal):

BPM stílus integráció

Az app2 tartalmazza a másik játékos üzleti folyamatát:

osztályú 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}")
}

Diagram:

BPM stílus integráció

Az app3 alkalmazásban egy kicsit más viselkedésű játékost készítünk: ahelyett, hogy véletlenszerűen választaná ki a következő játékost, a körmérkőzéses algoritmus szerint fog cselekedni:

osztályú 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}")
}

Egyébként a játékos viselkedése nem tér el az előzőtől, így a diagram nem változik.

Most egy tesztre van szükségünk, hogy mindezt lefuttassuk. Csak magának a tesztnek a kódját adom meg, hogy ne zsúfolja el a cikket egy kazánnal (sőt, a korábban létrehozott tesztkörnyezetet használtam más üzleti folyamatok integrációjának tesztelésére):

tesztJáték()

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

Futtassuk le a tesztet, és nézzük meg a naplót:

konzol kimenet

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

Mindebből számos fontos következtetést vonhatunk le:

  • a szükséges eszközökkel az alkalmazásfejlesztők az üzleti logika megszakítása nélkül hozhatnak létre integrációs interakciókat az alkalmazások között;
  • a mérnöki kompetenciákat igénylő integrációs feladat összetettsége elrejthető a kereten belül, ha ez kezdetben szerepel a keretrendszer architektúrájában. Egy probléma nehézségét nem lehet elrejteni, így egy nehéz probléma kódbeli megoldása így fog kinézni;
  • Az integrációs logika kialakítása során feltétlenül figyelembe kell venni az esetleges következetességet és az integráció valamennyi résztvevőjének állapotában bekövetkezett változások linearizálhatóságának hiányát. Ez arra kényszerít bennünket, hogy bonyolítsuk a logikát, hogy érzéketlenné tegyük a külső események bekövetkezésének sorrendjét. Példánkban a játékos arra kényszerül, hogy részt vegyen a játékban, miután kijelentette, hogy kilép a játékból: a többi játékos tovább passzolja neki a labdát, amíg a kilépésére vonatkozó információ el nem éri és azt minden résztvevő feldolgozza. Ez a logika nem következik a játékszabályokból, és kompromisszumos megoldás a választott architektúra keretein belül.

Ezután a megoldásunk különféle bonyodalmairól, a kompromisszumokról és egyéb pontokról fogunk beszélni.

Minden üzenet egy sorban van

Minden integrált alkalmazás egyetlen integrációs busszal működik, amely külső közvetítő, egy BPMQueue üzenetek és egy BPMTopic téma a jelzések (események) formájában jelenik meg. Az összes üzenet egy sorba helyezése önmagában is kompromisszum. Az üzleti logika szintjén mostantól tetszőleges számú új üzenettípust vezethet be anélkül, hogy módosítaná a rendszer szerkezetét. Ez jelentős leegyszerűsítés, de bizonyos kockázatokkal jár, amelyek a jellemző feladataink keretében számunkra nem tűntek olyan jelentősnek.

BPM stílus integráció

Van azonban itt egy finomság: minden alkalmazás kiszűri „saját” üzeneteit a bejárati sorból, a domain neve alapján. A tartomány jelekben is megadható, ha a jel „láthatósági körét” egyetlen alkalmazásra kell korlátozni. Ez növeli a busz átviteli sebességét, de az üzleti logikának mostantól tartománynevekkel kell működnie: üzenetek címezésére - kötelező, jelzésekre - kívánatos.

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

A megbízhatóság több pontból áll:

  • A kiválasztott üzenetközvetítő az architektúra kritikus eleme, és egyetlen hibapont: kellően hibatűrőnek kell lennie. Csak jól bevált implementációkat használjon, megfelelő támogatással és nagy közösséggel;
  • biztosítani kell az üzenetközvetítő magas rendelkezésre állását, amihez azt fizikailag el kell különíteni az integrált alkalmazásoktól (az alkalmazott üzleti logikával rendelkező alkalmazások magas rendelkezésre állását sokkal nehezebb és költségesebb biztosítani);
  • az alkusz köteles „legalább egyszeri” szállítási garanciát vállalni. Ez kötelező követelmény az integrációs busz megbízható működéséhez. Nincs szükség „pontosan egyszer” szintű garanciákra: az üzleti folyamatok általában nem érzékenyek az üzenetek vagy események ismétlődő érkezésére, és speciális feladatoknál, ahol ez fontos, egyszerűbb további ellenőrzéseket hozzáadni az üzlethez. logikát, mintsem állandóan „drága” garanciákat alkalmazni;
  • Az üzenetek és jelek küldésének egy átfogó tranzakcióban kell részt vennie az üzleti folyamatok és a tartományi adatok állapotának változásaival. Az előnyben részesített lehetőség a minta használata lenne Tranzakciós kimenő üzenetek, de ehhez szükség lesz egy további táblára az adatbázisban és egy átjátszóra. A JEE alkalmazásokban ez egyszerűsíthető egy helyi JTA menedzser használatával, de a kiválasztott brókerrel való kapcsolatnak működnie kell XA;
  • a bejövő üzenetek és események kezelőinek olyan tranzakciókkal is dolgozniuk kell, amelyek megváltoztatják az üzleti folyamat állapotát: ha egy ilyen tranzakciót visszagörgetnek, akkor az üzenet fogadását törölni kell;
  • a hiba miatt nem kézbesíthető üzeneteket külön tárolóban kell tárolni D.L.Q. (Holt levelek sora). Ebből a célból külön platform mikroszolgáltatást hoztunk létre, amely az ilyen üzeneteket a tárhelyén tárolja, attribútumok szerint indexeli (a gyors csoportosítás és keresés érdekében), valamint egy API-t tesz közzé az üzenetek megtekintésére, a célcímre történő újraküldésére és az üzenetek törlésére. A rendszergazdák webes felületükön keresztül dolgozhatnak ezzel a szolgáltatással;
  • a bróker beállításaiban módosítani kell a kézbesítési újrapróbálkozások számát és a kézbesítések közötti késések számát, hogy csökkentse annak valószínűségét, hogy az üzenetek bekerüljenek a DLQ-ba (az optimális paraméterek kiszámítása szinte lehetetlen, de empirikusan lehet cselekedni és működés közben módosítani );
  • A DLQ tárolót folyamatosan figyelni kell, és a felügyeleti rendszernek figyelmeztetnie kell a rendszergazdákat, hogy kézbesítetlen üzenetek esetén a lehető leggyorsabban reagálhassanak. Ez csökkenti a hiba vagy üzleti logikai hiba „érintett területét”;
  • az integrációs busznak érzéketlennek kell lennie az alkalmazások átmeneti hiányára: egy témakörre való előfizetésnek tartósnak kell lennie, és az alkalmazás domain nevének egyedinek kell lennie, hogy amíg az alkalmazás nincs jelen, valaki más ne próbálja meg feldolgozni az üzeneteit a sorban.

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

Egy üzleti folyamat ugyanazon példánya egyszerre több üzenetet és eseményt is fogadhat, amelyek feldolgozása párhuzamosan indul el. Ugyanakkor egy alkalmazásfejlesztő számára mindennek egyszerűnek és szálbiztosnak kell lennie.

Egy folyamat üzleti logikája egyenként dolgoz fel minden olyan külső eseményt, amely az adott üzleti folyamatot érinti. Ilyen események lehetnek:

  • üzleti folyamatpéldány elindítása;
  • üzleti folyamaton belüli tevékenységhez kapcsolódó felhasználói tevékenység;
  • olyan üzenet vagy jel fogadása, amelyre egy üzleti folyamatpéldány előfizetett;
  • üzleti folyamatpéldány által beállított időzítő kiváltása;
  • vezérlési műveletet API-n keresztül (például folyamat megszakítása).

Minden ilyen esemény megváltoztathatja az üzleti folyamatpéldány állapotát: egyes tevékenységek véget érhetnek, mások elkezdődhetnek, és a perzisztens tulajdonságok értékei megváltozhatnak. Bármely tevékenység bezárása az alábbi tevékenységek közül egy vagy több aktiválását eredményezheti. Ezek pedig abbahagyhatják a várakozást más eseményekre, vagy ha nincs szükségük további adatokra, akkor ugyanabban a tranzakcióban teljesíthetnek. A tranzakció lezárása előtt az üzleti folyamat új állapota mentésre kerül az adatbázisba, ahol megvárja a következő külső esemény bekövetkezését.

A relációs adatbázisban tárolt állandó üzleti folyamatadatok nagyon kényelmes pontot jelentenek a feldolgozás szinkronizálására, ha a SELECT FOR UPDATE funkciót használja. Ha egy tranzakciónak sikerült megszereznie egy üzleti folyamat állapotát a módosítási alapból, akkor párhuzamosan egyetlen másik tranzakció sem lesz képes ugyanazt az állapotot megszerezni egy másik változtatáshoz, és az első tranzakció befejezése után a második garantáltan megkapja a már megváltozott állapotot.

DBMS oldalon pesszimista zárolásokkal minden szükséges követelményt teljesítünk SAV, és megőrzi az alkalmazás üzleti logikával történő méretezésének lehetőségét a futó példányok számának növelésével.

A pesszimista zárolások azonban holtpontokkal fenyegetnek bennünket, ami azt jelenti, hogy a SELECT FOR UPDATE funkciót továbbra is ésszerű időtúllépésre kell korlátozni arra az esetre, ha az üzleti logika néhány kirívó esete holtpontra kerülne.

Egy másik probléma az üzleti folyamat kezdetének szinkronizálása. Noha nincs üzleti folyamat példánya, az adatbázisban nincs állapot, így a leírt módszer nem fog működni. Ha egy üzleti folyamatpéldány egyediségét kell biztosítania egy adott hatókörben, akkor szüksége lesz valamilyen szinkronizálási objektumra a folyamatosztályhoz és a megfelelő hatókörhöz. A probléma megoldására egy másik zárolási mechanizmust használunk, amely lehetővé teszi, hogy egy külső szolgáltatáson keresztül zároljunk egy tetszőleges, URI formátumú kulccsal megadott erőforrást.

Példáinkban az InitialPlayer üzleti folyamat deklarációt tartalmaz

uniqueConstraint = UniqueConstraints.singleton

Ezért a napló üzeneteket tartalmaz a megfelelő kulcs zárjának felvételéről és feloldásáról. Más üzleti folyamatokhoz nincsenek ilyen üzenetek: az egyedi korlát nincs beállítva.

Perzisztens állapotú üzleti folyamatok problémái

Néha a tartós állapot nemcsak segít, hanem valóban gátolja is a fejlődést.
A problémák akkor kezdődnek, amikor változtatni kell az üzleti logikán és/vagy az üzleti folyamatmodellben. Nem minden ilyen változás kompatibilis az üzleti folyamatok régi állapotával. Ha sok élő példány van az adatbázisban, akkor az inkompatibilis változtatások sok gondot okozhatnak, amivel gyakran találkoztunk a jBPM használatakor.

A változtatások mélységétől függően kétféleképpen járhat el:

  1. hozzon létre egy új üzleti folyamattípust, hogy ne hajtson végre inkompatibilis módosításokat a régiben, és használja azt a régi helyett az új példányok indításakor. A régi példányok továbbra is „mint korábban” működnek;
  2. az üzleti folyamatok állandó állapotának áttelepítése az üzleti logika frissítésekor.

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

  • az üzleti logika megkettőzése számos üzleti folyamatmodellben, növelve az üzleti logika mennyiségét;
  • Gyakran azonnali átállás szükséges az új üzleti logikára (az integrációs feladatok tekintetében - szinte mindig);
  • a fejlesztő nem tudja, hogy az elavult modelleket mikor lehet törölni.

A gyakorlatban mindkét megközelítést alkalmazzuk, de számos döntést hoztunk életünk megkönnyítése érdekében:

  • Az adatbázisban egy üzleti folyamat állandó állapota könnyen olvasható és könnyen feldolgozható formában tárolódik: JSON formátumú karakterláncban. Ez lehetővé teszi az áttelepítések végrehajtását az alkalmazáson belül és kívül is. Végső megoldásként manuálisan is kijavíthatja (különösen hasznos a fejlesztés során a hibakeresés során);
  • az integrációs üzleti logika nem használja az üzleti folyamatok neveit, így bármikor le lehet cserélni az egyik részt vevő folyamat megvalósítását egy új névvel (például „InitialPlayerV2”). A kötés üzenet- és jelneveken keresztül történik;
  • a folyamatmodellnek van egy verziószáma, amelyet növelünk, ha nem kompatibilis változtatásokat hajtunk végre ezen a modellen, és ez a szám a folyamatpéldány állapotával együtt mentésre kerül;
  • a folyamat állandó állapotát először az adatbázisból olvassa be egy kényelmes objektummodellbe, amellyel a migrációs eljárás működhet, ha a modell verziószáma megváltozott;
  • az áttelepítési eljárás az üzleti logika mellé kerül, és „lustának” nevezik az üzleti folyamat minden egyes példányánál az adatbázisból való visszaállításkor;
  • Ha az összes folyamatpéldány állapotát gyorsan és szinkronosan kell áttelepíteni, akkor klasszikusabb adatbázis-áttelepítési megoldásokat használnak, de JSON-nal kell dolgozni.

Szüksége van egy másik keretrendszerre az üzleti folyamatokhoz?

A cikkben ismertetett megoldások lehetővé tették számunkra, hogy jelentősen leegyszerűsítsük életünket, kibővítsük az alkalmazásfejlesztési szinten megoldandó problémák körét, és vonzóbbá tegyük az üzleti logika mikroszolgáltatásokra való szétválasztásának gondolatát. Ennek elérése érdekében rengeteg munkát végeztek, egy nagyon „könnyű” keretrendszert hoztak létre az üzleti folyamatokhoz, valamint olyan szolgáltatási komponenseket, amelyek az azonosított problémákat az alkalmazási problémák széles körének összefüggésében megoldják. Szeretnénk megosztani ezeket az eredményeket, és a közös komponensek fejlesztését ingyenes licenc alatt nyílt hozzáférésűvé tenni. Ez némi erőfeszítést és időt igényel. Az ilyen megoldások iránti kereslet megértése további ösztönzést jelenthet számunkra. A javasolt cikkben nagyon kevés figyelmet fordítanak magának a keretrendszernek a képességeire, de ezek egy része látható a bemutatott példákból. Ha mégis közzétesszük a keretünket, külön cikket fogunk szentelni neki. Addig is hálásak lennénk, ha visszajelzést adnának a következő kérdés megválaszolásával:

A felmérésben csak regisztrált felhasználók vehetnek részt. Bejelentkezés, kérem.

Szüksége van egy másik keretrendszerre az üzleti folyamatokhoz?

  • 18,8%Igen, már régóta keresek ilyesmit

  • 12,5%Szeretnék többet megtudni a megvalósításáról, hasznos lehet2

  • 6,2%A meglévő keretrendszerek egyikét használjuk, de gondolkodunk az1 cserén

  • 18,8%A meglévő keretrendszerek egyikét használjuk, minden rendben3

  • 18,8%keret nélkül gazdálkodunk3

  • 25,0%írd meg a tiédet 4

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

Forrás: will.com

Hozzászólás