BPM stil integration

BPM stil integration

Hej! Habr!

Vårt företag är specialiserat på utveckling av ERP-klassade mjukvarulösningar, vars lejonpart upptas av transaktionssystem med en enorm mängd affärslogik och dokumentflöde a la EDMS. Nuvarande versioner av våra produkter är baserade på JavaEE-teknologier, men vi experimenterar också aktivt med mikrotjänster. Ett av de mest problematiska områdena för sådana lösningar är integrationen av olika delsystem som tillhör angränsande domäner. Integrationsproblem har alltid gett oss en enorm huvudvärk, oavsett vilka arkitektoniska stilar, teknikstaplar och ramverk vi använder, men på senare tid har det skett framsteg med att lösa sådana problem.

I artikeln jag uppmärksammar dig kommer jag att prata om den erfarenhet och den arkitekturforskning som NPO Krista har inom det utpekade området. Vi kommer också att titta på ett exempel på en enkel lösning på ett integrationsproblem från en applikationsutvecklares synvinkel och ta reda på vad som döljer sig bakom denna enkelhet.

varning

De arkitektoniska och tekniska lösningar som beskrivs i artikeln föreslås av mig baserat på personlig erfarenhet i samband med specifika uppgifter. Dessa lösningar gör inte anspråk på att vara universella och kanske inte är optimala under andra användningsförhållanden.

Vad har BPM med det att göra?

För att besvara denna fråga måste vi gräva lite djupare in i detaljerna för de tillämpade problemen i våra lösningar. Huvuddelen av affärslogiken i vårt typiska transaktionssystem är att mata in data i databasen via användargränssnitt, manuell och automatiserad verifiering av denna data, utföra den genom något arbetsflöde, publicera den till ett annat system / analytisk databas / arkiv, generera rapporter . Därför är systemets nyckelfunktion för kunderna automatiseringen av deras interna affärsprocesser.

För enkelhetens skull använder vi termen "dokument" i kommunikation som en abstraktion av en uppsättning data förenad av en gemensam nyckel till vilken ett visst arbetsflöde kan "länkas".
Men hur är det med integrationslogiken? När allt kommer omkring genereras integrationsuppgiften av systemets arkitektur, som är "skuren" i delar INTE på kundens begäran, utan under påverkan av helt andra faktorer:

  • omfattas av Conways lag;
  • som ett resultat av återanvändning av delsystem som tidigare utvecklats för andra produkter;
  • efter arkitektens gottfinnande, utifrån icke-funktionella krav.

Det finns en stor frestelse att separera integrationslogik från affärslogiken i huvudarbetsflödet, för att inte förorena affärslogiken med integrationsartefakter och rädda applikationsutvecklaren från behovet av att fördjupa sig i funktionerna i systemets arkitektoniska landskap. Detta tillvägagångssätt har ett antal fördelar, men praktiken visar dess ineffektivitet:

  • att lösa integrationsproblem faller vanligtvis tillbaka till de enklaste alternativen i form av synkrona samtal på grund av de begränsade förlängningspunkterna i implementeringen av huvudarbetsflödet (nackdelarna med synkron integration diskuteras nedan);
  • integrationsartefakter penetrerar fortfarande kärnverksamhetens logik när återkoppling från ett annat delsystem krävs;
  • applikationsutvecklaren ignorerar integrationen och kan enkelt bryta den genom att ändra arbetsflödet;
  • systemet upphör att vara en helhet ur användarens synvinkel, "sömmar" mellan delsystem blir märkbara och redundanta användaroperationer uppstår, vilket initierar överföringen av data från ett delsystem till ett annat.

Ett annat tillvägagångssätt är att betrakta integrationsinteraktioner som en integrerad del av kärnverksamhetens logik och arbetsflöde. För att förhindra att applikationsutvecklarens kvalifikationer skjuter i höjden bör det vara enkelt och enkelt att skapa nya integrationsinteraktioner, med minimala möjligheter att välja en lösning. Detta är svårare att göra än det verkar: verktyget måste vara tillräckligt kraftfullt för att förse användaren med den mängd olika alternativ som krävs för dess användning, utan att tillåta honom att "skjuta sig själv i foten." Det finns många frågor som en ingenjör måste svara på i samband med integrationsuppgifter, men som en applikationsutvecklare inte bör tänka på i sitt dagliga arbete: transaktionsgränser, konsekvens, atomicitet, säkerhet, skalning, last- och resursfördelning, routing, marshaling, distribution och växlingskontexter etc. Det är nödvändigt att erbjuda applikationsutvecklare ganska enkla lösningsmallar där svaren på alla sådana frågor redan är gömda. Dessa mallar måste vara ganska säkra: affärslogiken ändras väldigt ofta, vilket ökar risken för att fel introduceras, kostnaden för fel måste förbli på en ganska låg nivå.

Men vad har BPM med det att göra? Det finns många alternativ för att implementera arbetsflöde...
En annan implementering av affärsprocesser är faktiskt mycket populär i våra lösningar - genom den deklarativa definitionen av ett tillståndsövergångsdiagram och kopplingen av hanterare med affärslogik för övergångar. I det här fallet är tillståndet som bestämmer den aktuella positionen för "dokumentet" i affärsprocessen ett attribut för själva "dokumentet".

BPM stil integration
Så här ser processen ut i början av ett projekt

Populariteten för denna implementering beror på den relativa enkelheten och snabbheten att skapa linjära affärsprocesser. Men eftersom mjukvarusystemen ständigt blir mer komplexa, växer den automatiserade delen av affärsprocessen och blir mer komplex. Det finns ett behov av nedbrytning, återanvändning av delar av processer, samt förgreningsprocesser så att varje gren exekveras parallellt. Under sådana förhållanden blir verktyget obekvämt och tillståndsövergångsdiagrammet förlorar sitt informationsinnehåll (integreringsinteraktioner återspeglas inte alls i diagrammet).

BPM stil integration
Så här ser processen ut efter flera iterationer av kravförtydligande.

Vägen ut ur denna situation var integrationen av motorn jBPM i vissa produkter med de mest komplexa affärsprocesserna. På kort sikt hade denna lösning viss framgång: det blev möjligt att implementera komplexa affärsprocesser samtidigt som ett ganska informativt och relevant diagram i notationen bibehölls BPMN2.

BPM stil integration
En liten del av en komplex affärsprocess

På lång sikt levde lösningen inte upp till förväntningarna: den höga arbetsintensiteten för att skapa affärsprocesser med visuella verktyg gjorde det inte möjligt att uppnå acceptabla produktivitetsindikatorer, och själva verktyget blev ett av de mest ogillade bland utvecklarna. Det fanns också klagomål på motorns inre struktur, vilket ledde till uppkomsten av många "lappar" och "kryckor".

Den främsta positiva aspekten med att använda jBPM var medvetenheten om fördelarna och nackdelarna med att ha en affärsprocessinstanss eget ihållande tillstånd. Vi såg också möjligheten att använda en processmetod för att implementera komplexa integrationsprotokoll mellan olika applikationer med hjälp av asynkron interaktion genom signaler och meddelanden. Förekomsten av ett ihållande tillstånd spelar en avgörande roll i detta.

Baserat på ovanstående kan vi dra slutsatsen: Processansatsen i BPM-stilen tillåter oss att lösa ett brett spektrum av uppgifter för att automatisera allt mer komplexa affärsprocesser, harmoniskt passa in integrationsaktiviteter i dessa processer och bibehålla förmågan att visuellt visa den implementerade processen i en lämplig notation.

Nackdelar med synkrona samtal som integrationsmönster

Synkron integration avser det enklaste blockerande samtalet. Ett delsystem fungerar som serversidan och exponerar API:t med den metod som krävs. Ett annat delsystem fungerar som klientsidan och ringer vid rätt tidpunkt och väntar på resultatet. Beroende på systemarkitekturen kan klient- och serversidan placeras antingen i samma applikation och process, eller i olika. I det andra fallet måste du tillämpa en viss RPC-implementering och tillhandahålla rangering av parametrarna och resultatet av anropet.

BPM stil integration

Detta integrationsmönster har en ganska stor uppsättning av nackdelar, men det används mycket i praktiken på grund av sin enkelhet. Implementeringshastigheten fängslar och tvingar dig att använda den om och om igen inför pressande deadlines, och registrera lösningen som teknisk skuld. Men det händer också att oerfarna utvecklare använder det omedvetet, helt enkelt inte inser de negativa konsekvenserna.

Förutom den mest uppenbara ökningen av delsystems anslutningsmöjligheter finns det också mindre uppenbara problem med att "växa" och "sträcka ut" transaktioner. Faktum är att om affärslogiken gör vissa ändringar, kan transaktioner inte undvikas, och transaktioner blockerar i sin tur vissa applikationsresurser som påverkas av dessa ändringar. Det vill säga, tills ett delsystem väntar på ett svar från det andra, kommer det inte att kunna slutföra transaktionen och ta bort låsen. Detta ökar avsevärt risken för en mängd olika effekter:

  • Systemets lyhördhet går förlorad, användare väntar länge på svar på frågor;
  • servern slutar vanligtvis att svara på användarförfrågningar på grund av en överfull trådpool: majoriteten av trådarna är låsta på en resurs som upptas av en transaktion;
  • Dödlägen börjar dyka upp: sannolikheten för att de inträffar beror starkt på transaktionernas varaktighet, mängden affärslogik och låsningar som är involverade i transaktionen;
  • transaktions timeout-fel visas;
  • servern "misslyckas" med OutOfMemory om uppgiften kräver bearbetning och ändring av stora mängder data, och närvaron av synkrona integrationer gör det mycket svårt att dela upp bearbetningen i "lättare" transaktioner.

Ur en arkitektonisk synvinkel leder användningen av blockerande samtal under integration till en förlust av kontroll över kvaliteten på enskilda delsystem: det är omöjligt att säkerställa målkvalitetsindikatorerna för ett delsystem isolerat från kvalitetsindikatorerna för ett annat delsystem. Om delsystem utvecklas av olika team är detta ett stort problem.

Saker och ting blir ännu mer intressanta om delsystemen som integreras finns i olika applikationer och du behöver göra synkrona förändringar på båda sidor. Hur säkerställer man transaktionaliteten av dessa förändringar?

Om ändringar görs i separata transaktioner måste du tillhandahålla pålitlig undantagshantering och kompensation, och detta eliminerar helt den största fördelen med synkrona integrationer - enkelhet.

Distribuerade transaktioner kommer också att tänka på, men vi använder dem inte i våra lösningar: det är svårt att säkerställa tillförlitlighet.

"Saga" som en lösning på transaktionsproblemet

Med den växande populariteten för mikrotjänster, efterfrågan på Saga mönster.

Detta mönster löser perfekt de ovan nämnda problemen med långa transaktioner och utökar också möjligheterna att hantera systemets tillstånd från affärslogikens sida: kompensation efter en misslyckad transaktion kanske inte rullar tillbaka systemet till dess ursprungliga tillstånd, utan ger en alternativ databehandlingsväg. Detta låter dig också undvika att upprepa framgångsrikt genomförda databearbetningssteg när du försöker få processen till ett "bra" slut.

Intressant nog är detta mönster även relevant i monolitiska system när det gäller integrationen av löst kopplade delsystem och negativa effekter som orsakas av långvariga transaktioner och motsvarande resurslås observeras.

I förhållande till våra affärsprocesser i BPM-stil, visar det sig vara mycket enkelt att implementera "Saga": individuella steg i "Saga" kan specificeras som aktiviteter inom affärsprocessen, och det ihållande tillståndet i affärsprocessen också bestämmer det interna tillståndet för "Saga". Det vill säga att vi inte kräver någon ytterligare samordningsmekanism. Allt du behöver är en meddelandeförmedlare som stödjer "minst en gång"-garantier som transport.

Men den här lösningen har också sitt eget "pris":

  • affärslogik blir mer komplex: kompensation måste utarbetas;
  • det kommer att vara nödvändigt att överge full konsistens, vilket kan vara särskilt känsligt för monolitiska system;
  • Arkitekturen blir lite mer komplicerad och ett ytterligare behov av en meddelandeförmedlare dyker upp;
  • ytterligare övervaknings- och administrationsverktyg kommer att krävas (även om detta i allmänhet är bra: kvaliteten på systemtjänsten kommer att öka).

För monolitiska system är motiveringen för att använda "Sag" inte så uppenbar. För mikrotjänster och annan SOA, där det med största sannolikhet redan finns en mäklare och full konsistens offras i början av projektet, kan fördelarna med att använda detta mönster avsevärt överväga nackdelarna, särskilt om det finns ett bekvämt API vid affärslogiken nivå.

Inkapslar affärslogik i mikrotjänster

När vi började experimentera med mikrotjänster uppstod en rimlig fråga: var ska man placera domänens affärslogik i förhållande till tjänsten som säkerställer att domändata finns kvar?

När man tittar på arkitekturen för olika BPMS, kan det tyckas rimligt att skilja affärslogik från persistens: skapa ett lager av plattforms- och domänoberoende mikrotjänster som bildar en miljö och behållare för exekvering av domänens affärslogik, och utforma beständigheten hos domändata som ett separat lager av mycket enkla och lätta mikrotjänster. Affärsprocesser i det här fallet utför orkestrering av tjänsterna i persistensskiktet.

BPM stil integration

Detta tillvägagångssätt har en mycket stor fördel: du kan öka plattformens funktionalitet så mycket du vill, och endast motsvarande lager av plattformsmikrotjänster kommer att bli "fett" av detta. Affärsprocesser från vilken domän som helst kan omedelbart använda plattformens nya funktionalitet så snart den uppdateras.

En mer detaljerad studie avslöjade betydande nackdelar med detta tillvägagångssätt:

  • en plattformstjänst som exekverar affärslogiken för många domäner samtidigt medför stora risker som en enda punkt av misslyckande. Frekventa ändringar av affärslogik ökar risken för fel som leder till systemomfattande fel;
  • prestandaproblem: affärslogik arbetar med sina data genom ett smalt och långsamt gränssnitt:
    • data kommer återigen att sorteras och pumpas genom nätverksstacken;
    • en domäntjänst kommer ofta att tillhandahålla mer data än vad som krävs för affärslogik att bearbeta på grund av otillräckliga möjligheter för att parametrisera förfrågningar på nivån för tjänstens externa API;
    • flera oberoende delar av affärslogik kan upprepade gånger begära samma data för bearbetning (detta problem kan mildras genom att lägga till sessionskomponenter som cachelagrar data, men detta komplicerar arkitekturen ytterligare och skapar problem med datarelevans och cache-ogiltigförklaring);
  • transaktionsproblem:
    • affärsprocesser med beständigt tillstånd, som lagras av en plattformstjänst, är inkonsekventa med domändata, och det finns inga enkla sätt att lösa detta problem;
    • placera domändatablockering utanför transaktionen: om domänens affärslogik behöver göra ändringar efter att först ha kontrollerat riktigheten av de aktuella uppgifterna, är det nödvändigt att utesluta möjligheten till en konkurrensutsatt förändring av de behandlade uppgifterna. Extern datablockering kan hjälpa till att lösa problemet, men en sådan lösning medför ytterligare risker och minskar systemets övergripande tillförlitlighet;
  • ytterligare svårigheter vid uppdatering: i vissa fall måste persistenstjänsten och affärslogiken uppdateras synkront eller i strikt ordning.

I slutändan var vi tvungna att gå tillbaka till grunderna: kapsla in domändata och domänaffärslogik i en mikrotjänst. Detta tillvägagångssätt förenklar uppfattningen av en mikrotjänst som en integrerad komponent i systemet och ger inte upphov till ovanstående problem. Detta ges inte heller gratis:

  • API-standardisering krävs för interaktion med affärslogik (särskilt för att tillhandahålla användaraktiviteter som en del av affärsprocesser) och API-plattformstjänster; kräver mer noggrann uppmärksamhet på API-ändringar, framåt- och bakåtkompatibilitet;
  • det är nödvändigt att lägga till ytterligare runtime-bibliotek för att säkerställa att affärslogik fungerar som en del av varje sådan mikrotjänst, och detta ger upphov till nya krav för sådana bibliotek: lätthet och ett minimum av transitiva beroenden;
  • affärslogikutvecklare måste övervaka biblioteksversioner: om en mikrotjänst inte har slutförts på länge, kommer den med största sannolikhet att innehålla en föråldrad version av biblioteken. Detta kan vara ett oväntat hinder för att lägga till en ny funktion och kan kräva migrering av den gamla affärslogiken för en sådan tjänst till nya versioner av bibliotek om det fanns inkompatibla ändringar mellan versionerna.

BPM stil integration

Ett lager av plattformstjänster finns också i en sådan arkitektur, men detta lager utgör inte längre en behållare för exekvering av domänens affärslogik, utan bara dess miljö, som tillhandahåller extra "plattforms"-funktioner. Ett sådant lager behövs inte bara för att bibehålla domänmikrotjänsternas lätta natur, utan också för att centralisera hanteringen.

Till exempel genererar användaraktiviteter i affärsprocesser uppgifter. Men när man arbetar med uppgifter måste användaren se uppgifter från alla domäner i den allmänna listan, vilket innebär att det måste finnas en motsvarande plattformsuppgiftsregistreringstjänst, rensat från domänens affärslogik. Att upprätthålla inkapsling av affärslogik i ett sådant sammanhang är ganska problematiskt, och detta är ytterligare en kompromiss med denna arkitektur.

Integration av affärsprocesser genom en applikationsutvecklares ögon

Som nämnts ovan måste en applikationsutvecklare abstraheras från de tekniska och tekniska funktionerna i att implementera interaktionen mellan flera applikationer så att man kan räkna med god utvecklingsproduktivitet.

Låt oss försöka lösa ett ganska svårt integrationsproblem, speciellt uppfunnit för artikeln. Detta kommer att vara en "spel"-uppgift som involverar tre applikationer, där var och en av dem definierar ett visst domännamn: "app1", "app2", "app3".

Inuti varje applikation lanseras affärsprocesser som börjar "spela boll" genom integrationsbussen. Meddelanden med namnet "Ball" kommer att fungera som en boll.

Spelregler:

  • den första spelaren är initiativtagaren. Han bjuder in andra spelare till spelet, startar spelet och kan avsluta det när som helst;
  • andra spelare förklarar sitt deltagande i spelet, "lär känna" varandra och den första spelaren;
  • efter att ha tagit emot bollen väljer spelaren en annan deltagande spelare och skickar bollen till honom. Det totala antalet sändningar räknas;
  • Varje spelare har "energi" som minskar för varje bollpassning av den spelaren. När energin tar slut, lämnar spelaren spelet och tillkännager sin avgång;
  • om spelaren lämnas ensam, meddelar han omedelbart sin avgång;
  • När alla spelare är eliminerade förklarar den första spelaren att spelet är avslutat. Om han lämnar spelet tidigt, återstår han att följa spelet för att slutföra det.

För att lösa det här problemet kommer jag att använda vår DSL för affärsprocesser, vilket gör att vi kan beskriva logiken i Kotlin kompakt, med ett minimum av plattan.

Affärsprocessen för den första spelaren (alias initiativtagaren till spelet) kommer att fungera i app1-applikationen:

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

Förutom att exekvera affärslogik kan ovanstående kod producera en objektmodell av en affärsprocess, som kan visualiseras i form av ett diagram. Vi har inte implementerat visualizern ännu, så vi var tvungna att lägga lite tid på att rita (här förenklade jag lite BPMN-notationen angående användningen av grindar för att förbättra diagrammets överensstämmelse med koden nedan):

BPM stil integration

app2 kommer att inkludera den andra spelarens affärsprocess:

klass 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 stil integration

I app3-applikationen kommer vi att göra en spelare med ett lite annorlunda beteende: istället för att slumpmässigt välja nästa spelare, kommer han att agera enligt round-robin-algoritmen:

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

Annars skiljer sig spelarens beteende inte från det föregående, så diagrammet ändras inte.

Nu behöver vi ett test för att köra allt detta. Jag kommer bara att ge koden för själva testet, för att inte belamra artikeln med en platta (i själva verket använde jag testmiljön som skapades tidigare för att testa integrationen av andra affärsprocesser):

testGame()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

private Map<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Låt oss köra testet och titta på loggen:

konsolutgång

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

Av allt detta kan vi dra flera viktiga slutsatser:

  • med de nödvändiga verktygen kan applikationsutvecklare skapa integrationsinteraktioner mellan applikationer utan att avbryta affärslogiken;
  • komplexiteten i en integrationsuppgift som kräver ingenjörskompetens kan döljas inom ramverket om detta initialt ingår i ramverkets arkitektur. Svårigheten med ett problem kan inte döljas, så lösningen på ett svårt problem i kod kommer att se ut som det;
  • När man utvecklar integrationslogik är det absolut nödvändigt att ta hänsyn till eventuell konsekvens och bristen på lineariserbarhet av förändringar i tillståndet för alla integrationsdeltagare. Detta tvingar oss att komplicera logiken för att göra den okänslig för i vilken ordning yttre händelser inträffar. I vårt exempel tvingas spelaren delta i spelet efter att han förklarat att han lämnat spelet: andra spelare kommer att fortsätta att skicka bollen till honom tills informationen om hans utträde når och behandlas av alla deltagare. Denna logik följer inte av spelets regler och är en kompromisslösning inom ramen för den valda arkitekturen.

Därefter kommer vi att prata om de olika krångligheterna i vår lösning, kompromisser och andra punkter.

Alla meddelanden finns i en kö

Alla integrerade applikationer fungerar med en integrationsbuss, som presenteras i form av en extern mäklare, en BPMQueue för meddelanden och ett BPMTopic-ämne för signaler (händelser). Att placera alla meddelanden i en kö är i sig en kompromiss. På affärslogiknivå kan du nu introducera så många nya meddelandetyper du vill utan att göra ändringar i systemstrukturen. Detta är en betydande förenkling, men den medför vissa risker, som i samband med våra typiska arbetsuppgifter inte verkade så betydande för oss.

BPM stil integration

Det finns dock en subtilitet här: varje applikation filtrerar "sina" meddelanden från kön vid ingången, efter namnet på sin domän. Domänen kan också specificeras i signaler om du behöver begränsa signalens "omfattning av synlighet" till en enda applikation. Detta bör öka bussgenomströmningen, men affärslogiken måste nu fungera med domännamn: för adressering av meddelanden - obligatoriskt, för signaler - önskvärt.

Säkerställande av integrationsbuss tillförlitlighet

Tillförlitlighet består av flera punkter:

  • Den valda meddelandeförmedlaren är en kritisk komponent i arkitekturen och en enda felpunkt: den måste vara tillräckligt feltolerant. Du bör endast använda beprövade implementeringar, med bra support och en stor community;
  • det är nödvändigt att säkerställa hög tillgänglighet för meddelandeförmedlaren, för vilken den måste vara fysiskt separerad från de integrerade applikationerna (hög tillgänglighet för applikationer med tillämpad affärslogik är mycket svårare och dyrare att säkerställa);
  • mäklaren är skyldig att lämna ”minst en gång” leveransgarantier. Detta är ett obligatoriskt krav för tillförlitlig drift av integrationsbussen. Det finns inget behov av "exakt en gång"-nivågarantier: affärsprocesser är som regel inte känsliga för upprepade inkommande meddelanden eller händelser, och i speciella uppgifter där detta är viktigt är det lättare att lägga till ytterligare kontroller till verksamheten logik än att ständigt använda ganska "dyra" " garantier;
  • att skicka meddelanden och signaler måste vara involverat i en övergripande transaktion med förändringar i tillståndet för affärsprocesser och domändata. Det föredragna alternativet skulle vara att använda ett mönster Transaktionsutkorg, men det kommer att kräva en extra tabell i databasen och en repeater. I JEE-applikationer kan detta förenklas genom att använda en lokal JTA-ansvarig, men kopplingen till den valda mäklaren måste kunna fungera i XA;
  • hanterare av inkommande meddelanden och händelser måste också arbeta med en transaktion som ändrar tillståndet för en affärsprocess: om en sådan transaktion återställs måste mottagandet av meddelandet avbrytas;
  • meddelanden som inte kunde levereras på grund av fel måste lagras i ett separat lager D.L.Q. (Döda bokstäver kö). För detta ändamål skapade vi en separat plattformsmikrotjänst som lagrar sådana meddelanden i sin lagring, indexerar dem efter attribut (för snabb gruppering och sökning) och exponerar ett API för visning, återsändning till destinationsadressen och radering av meddelanden. Systemadministratörer kan arbeta med den här tjänsten via deras webbgränssnitt;
  • i mäklarinställningarna måste du justera antalet leveransförsök och förseningar mellan leveranser för att minska sannolikheten för att meddelanden kommer in i DLQ (det är nästan omöjligt att beräkna de optimala parametrarna, men du kan agera empiriskt och justera dem under drift );
  • DLQ-butiken måste övervakas kontinuerligt och övervakningssystemet måste varna systemadministratörer så att de kan svara så snabbt som möjligt när meddelanden som inte levereras uppstår. Detta kommer att minska det "berörda området" av ett fel eller affärslogikfel;
  • integrationsbussen måste vara okänslig för den tillfälliga frånvaron av applikationer: prenumerationer på ett ämne måste vara varaktiga och applikationens domännamn måste vara unikt så att medan applikationen är frånvarande, kommer någon annan inte att försöka behandla dess meddelanden från kö.

Säkerställande av trådsäkerhet för affärslogik

Samma instans av en affärsprocess kan ta emot flera meddelanden och händelser samtidigt, vars bearbetning kommer att starta parallellt. Samtidigt ska allt för en applikationsutvecklare vara enkelt och trådsäkert.

Affärslogiken i en process bearbetar varje extern händelse som påverkar den affärsprocessen individuellt. Sådana händelser kan vara:

  • lansering av en affärsprocessinstans;
  • användaråtgärd relaterad till aktivitet inom en affärsprocess;
  • mottagande av ett meddelande eller signal som en affärsprocessinstans prenumererar på;
  • triggning av en timer inställd av en affärsprocessinstans;
  • kontrollåtgärd via API (till exempel processavbrott).

Varje sådan händelse kan ändra tillståndet för en affärsprocessinstans: vissa aktiviteter kan sluta och andra kan börja, och värdena på beständiga egenskaper kan ändras. Att stänga någon aktivitet kan resultera i aktivering av en eller flera av följande aktiviteter. De kan i sin tur sluta vänta på andra händelser eller, om de inte behöver ytterligare data, kan de slutföra i samma transaktion. Innan transaktionen avslutas sparas det nya tillståndet för affärsprocessen i databasen, där den väntar på att nästa externa händelse inträffar.

Beständiga affärsprocessdata lagrade i en relationsdatabas är en mycket bekväm punkt för att synkronisera bearbetning om du använder SELECT FOR UPDATE. Om en transaktion lyckades få tillståndet för en affärsprocess från basen för att ändra den, kommer ingen annan transaktion parallellt att kunna erhålla samma tillstånd för en annan förändring, och efter slutförandet av den första transaktionen är den andra transaktionen garanterat att få det redan ändrade tillståndet.

Med hjälp av pessimistiska lås på DBMS-sidan uppfyller vi alla nödvändiga krav SYRA, och även behålla möjligheten att skala applikationen med affärslogik genom att öka antalet körande instanser.

Pessimistiska lås hotar oss dock med dödlägen, vilket innebär att VÄLJ FÖR UPPDATERING fortfarande bör begränsas till en rimlig tidsgräns om låsningar uppstår i vissa allvarliga fall i affärslogiken.

Ett annat problem är synkroniseringen av starten av en affärsprocess. Även om det inte finns någon instans av en affärsprocess, finns det inget tillstånd i databasen, så den beskrivna metoden kommer inte att fungera. Om du behöver säkerställa unikheten hos en affärsprocessinstans i ett specifikt omfång, behöver du något slags synkroniseringsobjekt kopplat till processklassen och motsvarande omfattning. För att lösa detta problem använder vi en annan låsmekanism som gör att vi kan låsa en godtycklig resurs som specificeras av en nyckel i URI-format via en extern tjänst.

I våra exempel innehåller InitialPlayers affärsprocess en deklaration

uniqueConstraint = UniqueConstraints.singleton

Därför innehåller loggen meddelanden om att ta och släppa låset för motsvarande nyckel. Det finns inga sådana meddelanden för andra affärsprocesser: uniqueConstraint är inte inställt.

Problem med affärsprocesser med ihållande tillstånd

Ibland hjälper det inte bara att ha ett ihållande tillstånd, utan det hindrar verkligen utvecklingen.
Problem börjar när förändringar behöver göras i affärslogiken och/eller affärsprocessmodellen. Inte varje sådan förändring är förenlig med det gamla tillståndet för affärsprocesser. Om det finns många live-instanser i databasen kan inkompatibla ändringar orsaka mycket problem, vilket vi ofta stöter på när vi använder jBPM.

Beroende på förändringarnas djup kan du agera på två sätt:

  1. skapa en ny affärsprocesstyp för att inte göra inkompatibla ändringar av den gamla, och använd den istället för den gamla när du startar nya instanser. Gamla kopior kommer att fortsätta att fungera "som tidigare";
  2. migrera det beständiga tillståndet för affärsprocesser vid uppdatering av affärslogik.

Det första sättet är enklare, men har sina begränsningar och nackdelar, till exempel:

  • dubbelarbete av affärslogik i många affärsprocessmodeller, vilket ökar volymen av affärslogik;
  • Ofta krävs en omedelbar övergång till ny affärslogik (när det gäller integrationsuppgifter - nästan alltid);
  • utvecklaren vet inte vid vilken tidpunkt föråldrade modeller kan tas bort.

I praktiken använder vi båda metoderna, men har tagit ett antal beslut för att göra våra liv enklare:

  • I databasen lagras det beständiga tillståndet för en affärsprocess i en lättläslig och lättbearbetad form: i en JSON-formatsträng. Detta gör att migrering kan utföras både inom applikationen och externt. Som en sista utväg kan du korrigera det manuellt (särskilt användbart vid utveckling under felsökning);
  • Integrationsaffärslogiken använder inte namnen på affärsprocesser, så att det när som helst är möjligt att ersätta implementeringen av en av de deltagande processerna med en ny med ett nytt namn (till exempel "InitialPlayerV2"). Bindningen sker genom meddelande- och signalnamn;
  • processmodellen har ett versionsnummer, som vi ökar om vi gör inkompatibla ändringar av denna modell, och detta nummer sparas tillsammans med processinstansens tillstånd;
  • det ihållande tillståndet för processen läses först från databasen till en bekväm objektmodell, som migreringsproceduren kan arbeta med om modellens versionsnummer har ändrats;
  • migreringsproceduren placeras bredvid affärslogiken och kallas "lat" för varje instans av affärsprocessen vid tidpunkten för dess återställning från databasen;
  • om du behöver migrera tillståndet för alla processinstanser snabbt och synkront används mer klassiska databasmigreringslösningar, men du måste arbeta med JSON.

Behöver du ett annat ramverk för affärsprocesser?

Lösningarna som beskrivs i artikeln gjorde det möjligt för oss att avsevärt förenkla vårt liv, utöka utbudet av problem som lösts på applikationsutvecklingsnivå och göra idén om att separera affärslogik i mikrotjänster mer attraktiv. För att uppnå detta gjordes mycket arbete, ett mycket "lättviktigt" ramverk för affärsprocesser skapades, samt tjänstekomponenter för att lösa de identifierade problemen inom ramen för ett brett spektrum av applikationsproblem. Vi har en önskan att dela dessa resultat och göra utvecklingen av gemensamma komponenter öppen åtkomst under en fri licens. Detta kommer att kräva lite ansträngning och tid. Att förstå efterfrågan på sådana lösningar kan vara ett ytterligare incitament för oss. I den föreslagna artikeln ägnas mycket lite uppmärksamhet åt själva ramverkets kapacitet, men några av dem är synliga från exemplen som presenteras. Om vi ​​publicerar vårt ramverk kommer en separat artikel att ägnas åt det. Under tiden skulle vi vara tacksamma om du lämnar lite feedback genom att svara på frågan:

Endast registrerade användare kan delta i undersökningen. Logga in, Snälla du.

Behöver du ett annat ramverk för affärsprocesser?

  • 18,8%Ja, jag har letat efter något sånt här länge

  • 12,5%Jag är intresserad av att lära mig mer om din implementering, det kan vara användbart2

  • 6,2%Vi använder ett av de befintliga ramverken, men funderar på att ersätta1

  • 18,8%Vi använder ett av de befintliga ramverken, allt är bra3

  • 18,8%vi klarar oss utan ramverk3

  • 25,0%skriv ditt 4

16 användare röstade. 7 användare avstod från att rösta.

Källa: will.com

Lägg en kommentar