
Hej, Habr!
VÄrt företag specialiserar sig pÄ utveckling av ERP-lösningar, dÀr en stor del bestÄr av transaktionssystem med en enorm mÀngd affÀrslogik och dokumenthantering liknande EDI. Moderna versioner av vÄra produkter bygger pÄ JavaEE-teknologier, men vi experimenterar ocksÄ aktivt med mikrotjÀnster. En av de mest utmanande aspekterna av sÄdana lösningar Àr integrationen av olika delsystem som tillhör angrÀnsande domÀner. Integrationsuppgifter har alltid varit en stor huvudvÀrk för oss, oavsett vilka arkitekturstilar, teknologiska stackar eller ramverk vi anvÀnder, men pÄ senare tid har det skett framsteg i att lösa sÄdana uppgifter.
I den hÀr artikeln kommer jag att berÀtta om erfarenheter och arkitekturinsikter frÄn NPO "Krista" inom det angivna omrÄdet. Vi kommer ocksÄ att titta pÄ ett exempel pÄ en enkel lösning pÄ ett integrationsproblem frÄn en tillÀmpad utvecklares perspektiv och ta reda pÄ vad som döljer sig bakom denna enkelhet.
Disclaimer
De arkitektoniska och tekniska lösningar som beskrivs i artikeln föreslÄr jag baserat pÄ personlig erfarenhet i kontexten av specifika uppgifter. Dessa lösningar gör ansprÄk pÄ att inte vara universella och kan visa sig vara suboptimala under andra anvÀndningsvillkor.
Vad har BPM med detta att göra?
För att besvara denna frÄga behöver vi dyka lite djupare i specifikationen av tillÀmpningsuppgifterna för vÄra lösningar. Huvuddelen av affÀrslogiken i vÄrt typiska transaktionssystem handlar om att mata in data i databasen via anvÀndargrÀnssnitt, manuell och automatiserad verifiering av dessa data, bearbetning genom en viss arbetsflöde, publicering till ett annat system / analytisk databas / arkiv, och rapportgenerering. DÀrmed Àr den nyckelfunktion hos systemet för kunderna att automatisera deras interna affÀrsprocesser.
För vÄr bekvÀmlighet anvÀnder vi termen "dokument" i kommunikationen som en viss abstraktion av en datamÀngd som Àr sammanlÀnkad med en gemensam nyckel, till vilken ett visst arbetsflöde kan "kopplas".
Men hur ska vi hantera integrationslogiken? Integrationsuppgiften uppstÄr ju frÄn systemarkitekturen, som Àr "uppdelad" i delar INTE pÄ kundens begÀran, utan under pÄverkan av helt andra faktorer:
- under pÄverkan av Conway-lagen;
- som resultat av ÄteranvÀndningen av delsystem som tidigare har utvecklats för andra produkter;
- enligt arkitektens beslut med hÀnsyn till icke-funktionella krav.
Det finns en stor frestelse att separera integrationslogik frÄn affÀrslogik i huvud-arbetsflödet för att undvika att förorena affÀrslogiken med integrationsartefakter och frigöra den applikationsutvecklare frÄn behovet av att förstÄ de arkitektoniska landskapets egenskaper. SÄdan metodik har sina fördelar, men praktiken visar att den Àr ineffektiv:
- lösningen pÄ integrationsfrÄgor rör sig ofta mot de enklaste alternativen i form av synkrona anrop pÄ grund av begrÀnsningen av expansionspunkterna i implementeringen av det huvudsakliga arbetsflödet (om nackdelarna med synkron integration - senare);
- integrationsartefakter trÀnger sig fortfarande in i den grundlÀggande affÀrslogiken nÀr feedback frÄn ett annat delsystem krÀvs;
- den applikationsutvecklare ignorerar integrationen och kan lÀtt bryta den genom att Àndra arbetsflödet;
- systemet slutar vara en enhet frÄn anvÀndarens synvinkel, "skarvar" mellan delsystemen blir tydliga, och det uppstÄr överflödiga anvÀndaroperationer som initierar dataöverföring frÄn ett delsystem till ett annat.
Ett annat tillvÀgagÄngssÀtt Àr att betrakta integrationsinteraktioner som en oumbÀrlig del av den grundlÀggande affÀrslogiken och arbetsflödet. För att kraven pÄ kompetens hos tillÀmpade utvecklare inte ska skjuta i höjden mÄste skapandet av nya integrationsinteraktioner göras enkelt och okomplicerat, med minimi möjliga valmöjligheter för hur problemet kan lösas. Detta Àr svÄrare Àn det verkar: verktyget mÄste vara tillrÀckligt kraftfullt för att ge anvÀndaren ett nödvÀndigt antal alternativ för dess tillÀmpning, samtidigt som det inte fÄr tillÄta att man "skjuter sig sjÀlv i foten". Det finns mÄnga frÄgor som ingenjören mÄste besvara i samband med integrationsuppgifter, men som den tillÀmpade utvecklaren inte bör behöva tÀnka pÄ i sitt dagliga arbete: grÀnser för transaktioner, konsistens, atomaritet, sÀkerhet, skalbarhet, distribution av belastningar och resurser, routning, marshalling, spridning och vÀxling av kontexter, osv. Det behövs tillrÀckligt enkla lösningsmallar för tillÀmpade utvecklare som redan döljer svar pÄ alla sÄdana frÄgor. Dessa mallar mÄste vara tillrÀckligt sÀkra: affÀrslogiken förÀndras mycket ofta, vilket ökar riskerna för fel, och kostnaden för fel bör förbli pÄ en tillrÀckligt lÄg nivÄ.
Men vad har BPM att göra med detta? Det finns ju mÄnga alternativ för att implementera arbetsflöden...
Verkligen, i vĂ„ra lösningar Ă€r en annan implementering av affĂ€rsprocesser mycket populĂ€r â genom deklarativ specifikation av tillstĂ„ndsövergĂ„ngsdiagram och anslutning av hanterare med affĂ€rslogik vid övergĂ„ngarna. I detta sammanhang Ă€r tillstĂ„ndet som definierar det aktuella lĂ€get för "dokumentet" i affĂ€rsprocessen en attribut av sjĂ€lva "dokumentet".

SÄ hÀr ser processen ut i starten av projektet
Populariteten av denna implementering beror pÄ den relativa enkelheten och hastigheten att skapa linjÀra affÀrsprocesser. Men i takt med att mjukvarusystemen stÀndigt blir mer komplexa vÀxer och kompliceras den automatiserade delen av affÀrsprocessen. Det uppstÄr ett behov av dekomposition, ÄteranvÀndning av delar av processerna, samt förgrening av processerna sÄ att varje gren körs parallellt. Under sÄdana förhÄllanden blir verktyget otympligt, och tillstÄndsdiagrammet förlorar sin informativitet (integrationsinteraktioner Äterspeglas överhuvudtaget inte i diagrammet).

SÄ hÀr ser processen ut efter nÄgra iterationer av kravutveckling.
Lösningen pÄ denna situation blev integrationen av motorn 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 medan man bibehöll ett tillrÀckligt informativt och aktuellt diagram i notationen .

En liten del av en komplex affÀrsprocess.
PÄ lÄng sikt uppfyllde inte lösningen förvÀntningarna: den höga arbetsinsatsen för att skapa affÀrsprocesser genom visuella verktyg gjorde det omöjligt att nÄ acceptabla produktivitetsnivÄer, och sjÀlva verktyget blev en av de mest ogillade bland utvecklare. Det fanns ocksÄ klagomÄl pÄ den interna strukturen hos motorn, vilket ledde till att det förekom mÄnga "patchar" och "krÄngel".
Den frÀmsta positiva aspekten av att anvÀnda jBPM blev insikten om fördelarna och nackdelarna med att ha ett eget persistent tillstÄnd för ett affÀrsprocess-exemplar. Vi sÄg ocksÄ möjligheten att anvÀnda den processbaserade ansatsen för att genomföra komplexa integrationsprotokoll mellan olika applikationer med anvÀndning av asynkrona interaktioner via signaler och meddelanden. NÀrvaron av ett persistent tillstÄnd spelar en avgörande roll i detta.
Baserat pÄ det som sagts kan man dra slutsatsen: den processbaserade ansatsen i stil med BPM gör det möjligt för oss att lösa ett brett spektrum av uppgifter för att automatisera stÀndigt mer komplexa affÀrsprocesser, harmoniskt integrera integrationsaktiviteter i dessa processer och bevara möjligheten till visuell representation av den genomförda processen i en lÀmplig notation.
Nackdelar med synkrona anrop som integrationsmönster
Under synkron integration förstÄs en enkel blockerande anrop. En delsystem fungerar som server och tillhandahÄller ett API med önskad metod. Den andra delen fungerar som klient och utför anropet vid rÀtt tidpunkt med förvÀntan pÄ resultatet. Beroende pÄ systemarkitekturen kan klient- och serversidan placeras antingen i samma applikation och process eller i olika. I det senare fallet krÀvs en viss implementation av RPC och hantering av parametrar samt resultatet av anropet.

Detta integrationsmönster har ett ganska stort antal nackdelar, men det anvÀnds mycket praktiskt pÄ grund av sin enkelhet. Implementeringshastigheten Àr tilltalande och fÄr en att anvÀnda det gÄng pÄ gÄng under "brinnande" deadlines, vilket skapar teknisk skuld. Men det hÀnder ocksÄ att oerfarna utvecklare anvÀnder det omedvetet, utan att förstÄ de negativa konsekvenserna.
Förutom den mest uppenbara ökningen av kopplingen mellan delsystemen finns det Àven mindre uppenbara problem med "kastning" och "utdragning" av transaktioner. Om affÀrslogiken gör nÄgra Àndringar kan man inte undvika transaktioner, och transaktioner blockerar i sin tur vissa resurser i applikationen som berörs av dessa Àndringar. Det betyder att medan ett delsystem vÀntar pÄ svar frÄn ett annat, kan det inte slutföra transaktionen och frigöra blockeringar. Detta ökar avsevÀrt risken för olika effekter:
- systemets responsivitet gÄr förlorad, anvÀndarna fÄr vÀnta lÀnge pÄ svar pÄ sina förfrÄgningar;
- servern slutar helt att svara pÄ anvÀndarförfrÄgningar pÄ grund av en överfull trÄdpool: de flesta trÄdarna Àr "fast" i resursblockeringen som upptas av transaktionen;
- det börjar uppstÄ dödlÀgen: sannolikheten för deras uppkomst beror starkt pÄ varaktigheten av transaktionerna, mÀngden affÀrslogik som Àr involverad i transaktionen och blockeringarna;
- det uppstÄr timeoutfel i transaktionen;
- servern "kraschar" pÄ OutOfMemory om uppgiften krÀver att stora datamÀngder bearbetas och Àndras, och förekomsten av synkrona integrationer gör det mycket svÄrt att dela upp bearbetningen i mer "lÀttviktiga" transaktioner.
Ur ett arkitektoniskt perspektiv leder anvÀndningen av blockerande anrop vid integration till en förlust av kontroll över kvaliteten i enskilda delsystem: det Àr omöjligt att sÀkerstÀlla kvalitetsmÄlen för ett delsystem oberoende av kvalitetsmÄlen för ett annat delsystem. Om delsystemen utvecklas av olika team Àr detta ett stort problem.
Det blir Ànnu mer intressant om de integrerade delsystemen finns i olika applikationer och synkrona förÀndringar mÄste göras frÄn tvÄ sidor. Hur sÀkerstÀller man transaktionalitet av dessa förÀndringar?
Om förĂ€ndringar görs i separata transaktioner kommer man att behöva sĂ€kerstĂ€lla tillförlitlig hantering av undantag och kompensation, vilket helt upphĂ€ver den frĂ€msta fördelen med synkrona integrationer â enkelheten.
Man tÀnker ocksÄ pÄ distribuerade transaktioner, men vi anvÀnder dem inte i vÄra lösningar: det Àr svÄrt att sÀkerstÀlla tillförlitlighet.
Saga som lösning pÄ transaktionsproblem
Med den ökande populariteten av mikrotjÀnster blir .
Detta mönster löser utmÀrkt de ovan nÀmnda problemen med lÄnga transaktioner, samt utökar möjligheterna att hantera systemets tillstÄnd frÄn affÀrslogikens sida: kompensation efter en misslyckad transaktion kan ge en alternativ vÀg för databehandling istÀllet för att ÄterstÀlla systemet till sitt ursprungliga tillstÄnd. Detta gör det ocksÄ möjligt att inte upprepa framgÄngsrikt avslutade steg i databehandlingen vid försök att föra processen till ett 'bra' slut.
Intressant nog Àr detta mönster ocksÄ relevant i monolitiska system, om det handlar om integration av svagt kopplade delsystem och negativa effekter observeras som orsakas av lÄnga transaktioner och motsvarande resurslÄsningar.
TillÀmpat pÄ vÄra affÀrsprocesser i BPM-stil visar det sig att implementera Saga Àr mycket enkelt: enskilda steg i Saga kan definieras som aktiviteter inom affÀrsprocessen, och det persistenta tillstÄndet för affÀrsprocessen definierar bland annat det interna tillstÄndet för Sagan. Det vill sÀga, vi behöver inget ytterligare koordineringsmekanism. Vi behöver bara en meddelandebroker med stöd för 'at least once'-garantier som transport.
Men Àven en sÄdan lösning har sitt 'pris':
- AffÀrslogiken blir mer komplex: det behövs kompenserande ÄtgÀrder;
- Det kommer att krÀvas avstÄende frÄn fullstÀndig konsistens, vilket kan vara sÀrskilt kÀnsligt för monolitiska system;
- Arkitekturen blir nÄgot mer komplex, och det uppstÄr ett ytterligare behov av en meddelandebroker;
- Det kommer att behövas ytterligare övervaknings- och administrationsverktyg (Àven om detta faktiskt Àr positivt: kvaliteten pÄ systemtjÀnsten kommer att förbÀttras).
För monolitiska system Àr det inte sÄ sjÀlvklart att anvÀnda 'Sagas'. För mikrotjÀnster och andra SOA, dÀr det troligtvis redan finns en broker och fullstÀndig konsistens har offrats redan frÄn början av projektet, kan nyttan av att anvÀnda denna mall kraftigt övervÀga nackdelarna, sÀrskilt om det finns ett bekvÀmt API pÄ affÀrslogiknivÄ.
Inkaplsering av affÀrslogik i mikrotjÀnster
NÀr vi började experimentera med mikrotjÀnster uppstod den rimliga frÄgan: var ska den domÀnspecifika affÀrslogiken placeras i förhÄllande till tjÀnsten som tillhandahÄller persistent domÀndata?
Vid en titt pÄ arkitekturen hos olika BPMS kan det verka rimligt att separera affÀrslogik frÄn persistens: skapa ett lager av plattforms- och domÀnoberoende mikrotjÀnster som formar en miljö och container för exekvering av den domÀnspecifika affÀrslogiken, medan persistensen av domÀndata utformas som ett separat lager av mycket enkla och lÀttviktiga mikrotjÀnster. AffÀrsprocesser kommer i detta fall att orchestrera tjÀnsterna i persistenslagret.

En sÄdan ansats har en stor fördel: man kan utöka plattformens funktionalitet hur mycket man vill, och det enda som kommer att bli 'tyngre' Àr det motsvarande lagret av plattformsmikrotjÀnster. AffÀrsprocesser frÄn vilken domÀn som helst fÄr omedelbart möjlighet att anvÀnda den nya funktionaliteten i plattformen, sÄ snart den uppdateras.
En mer detaljerad genomgÄng avslöjade betydande nackdelar med detta tillvÀgagÄngssÀtt:
- Den plattformstjÀnst som kör affÀrslogik för flera domÀner utgör en stor risk som en enhetlig felpunkt. Frekventa förÀndringar av affÀrslogik ökar risken för fel som kan leda till störningar som sprider sig över hela systemet;
- Prestandaproblem: affÀrslogiken arbetar med sina data genom ett smalt och lÄngsamt grÀnssnitt:
- data kommer att gÄ igenom nÀtverksstacken ytterligare en gÄng;
- domÀnservicen kommer ofta att leverera mer data Àn vad affÀrslogiken behöver för behandling, pÄ grund av otillrÀckliga möjligheter för parametrisering av frÄgor pÄ nivÄn för den externa API-tjÀnsten;
- flera oberoende delar av affÀrslogiken kan repetera begÀran pÄ samma data för behandling (denna problematik kan mildras genom att lÀgga till sessionskomponenter som cachelagrar data, men detta komplicerar ytterligare arkitekturen och skapar problem med datakonsistens och cacheinvalidation);
- transaktionalitetsproblem:
- affÀrsprocesser med ett kvarhÄllande tillstÄnd, vars lagring hanteras av plattformstjÀnsten, kommer att vara i osynk med domÀndata, och inga enkla lösningar pÄ detta problem förvÀntas;
- att flytta lÄsning av domÀndata utanför transaktionen: om den domÀnspecifika affÀrslogiken behöver göra Àndringar efter att ha verifierat aktuella data, mÄste möjligheten för konkurrerande Àndringar av de hanterade uppgifterna uteslutas. Extern datalÄsning kan hjÀlpa till att lösa problemet, men en sÄdan lösning medför ytterligare risker och minskar det totala systemets tillförlitlighet;
- ytterligare svÄrigheter med uppdateringar: i vissa fall mÄste uppdatera persistensservicen och affÀrslogiken ske synkront eller i strikt sekvens.
Till slut var vi tvungna att gÄ tillbaka till grunderna: att kapsla in domÀndata och domÀnspecifik affÀrslogik i en mikroservice. Denna metod underlÀttar uppfattningen av mikroservicen som en helhet inom systemet och genererar inte de ovan nÀmnda problemen. Detta kommer dock inte utan kostnad:
- det krÀvs standardisering av API:er för interaktion med affÀrslogiken (sÀrskilt för att stödja anvÀndaraktiviteter inom affÀrsprocesser) och API:er för plattformstjÀnster; ett mer noggrant förhÄllningssÀtt till förÀndringar i API:er, direkt och bakÄtkompatibilitet krÀvs;
- det behöver lÀggas till extra runtime-bibliotek för att sÀkerstÀlla att affÀrslogiken fungerar inom varje sÄdan mikroservice, och detta ger upphov till nya krav för dessa bibliotek: lÄg vikt och minimala transitberoenden;
- Utvecklare av affÀrslogik mÄste hÄlla koll pÄ versionerna av biblioteken: om en mikrotjÀnst inte har uppdaterats pÄ lÀnge, finns det en stor sannolikhet att den har en förÄldrad version av biblioteken. Detta kan bli en ovÀntad hinder vid tillÀgg av nya funktioner och kan krÀva att den gamla affÀrslogiken migreras till nya versioner av biblioteken om det har skett inkompatibla förÀndringar mellan versionerna.

Lagret av plattformstjÀnster i en sÄdan arkitektur finns ocksÄ, men detta lager skapar inte lÀngre en behÄllare för att köra den domÀnspecifika affÀrslogiken, utan snarare enbart dess omgivning, som erbjuder stödjande "plattform"-funktioner. Ett sÄdant lager Àr behövligt för att inte bara bevara lÀttvikten hos domÀnmikrotjÀnsterna, utan ocksÄ för att centralisera hanteringen.
Till exempel skapar anvÀndaraktiviteter i affÀrsprocesser uppgifter. Men nÀr anvÀndaren arbetar med uppgifter mÄste han eller hon se uppgifter frÄn alla domÀner i en gemensam lista, vilket innebÀr att det mÄste finnas en motsvarande plattformstjÀnst för registrering av uppgifter, som Àr ren frÄn domÀnspecifik affÀrslogik. Att behÄlla inkapseringen av affÀrslogiken i detta sammanhang Àr tillrÀckligt problematiskt, och detta Àr ytterligare en kompromiss i denna arkitektur.
Integrering av affÀrsprocesser ur ett applikationsutvecklares perspektiv
Som nÀmnts tidigare mÄste applikationsutvecklaren vara abstraherad frÄn de tekniska och ingenjörsmÀssiga detaljerna i implementeringen av interaktionen mellan flera applikationer för att kunna rÀkna med god utvecklingsproduktivitet.
LÄt oss försöka lösa en ganska svÄr integrationsuppgift, avsiktligt skapad för denna artikel. Detta kommer att vara en "spel"-uppgift med tre applikationer dÀr var och en av dem definierar ett visst domÀnnamn: "app1", "app2", "app3".
Inom varje applikation startas affÀrsprocesser som börjar "spela boll" genom en integrationsbuss. I rollen som bollen kommer meddelanden med namnet "Ball" att fungera.
Spelregler:
- den första spelaren â initiativtagaren. Han eller hon bjuder in de andra spelarna till spelet, startar spelet och kan avsluta det nĂ€r som helst;
- de andra spelarna anmÀler sitt deltagande i spelet, och "lÀr kÀnna" varandra samt den första spelaren;
- nÀr han eller hon tar emot bollen, vÀljer spelaren en annan deltagande spelare och överför bollen till honom eller henne. Det hÄlls en rÀkning pÄ det totala antalet överföringar;
- Varje spelare har "energi" som minskar med varje passning av bollen frÄn den spelaren. NÀr energin tar slut, lÀmnar spelaren spelet och meddelar sin avsked;
- om spelaren Àr ensam kvar, meddelar han om sin avsked genast;
- NÀr alla spelare har lÀmnat, meddelar den första spelaren att spelet Àr över. Om han har lÀmnat tidigare, stannar han kvar för att övervaka spelet och avsluta det.
För att lösa denna uppgift kommer jag att anvÀnda vÄr DSL för affÀrsprocesser, som lÄter oss beskriva logik i Kotlin pÄ ett kompakt sÀtt med minimalt med boilerplate.
I appen app1 kommer affÀrsprocessen för den första spelaren (som ocksÄ Àr initiativtagaren till spelet) att köras:
class InitialPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.constraint.UniqueConstraints
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.dsl.taskOperation
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList : ArrayList()
// Detta Àr klass av processinstans: den inkapslar dess interna tillstÄnd
class InitialPlayer : ProcessImpl(initialPlayerModel) {
var playerName: String by persistent("Player1")
var energy: Int by persistent(30)
var players: PlayersList by persistent(PlayersList())
var shotCounter: Int = 0
}
// Detta Àr deklarationen av processmodellen: den skapas en gÄng, anvÀnds av alla
// instanser av processens motsvarande klass
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Enligt reglerna Àr den första spelaren initiativtagare av spelet och mÄste vara den enda
uniqueConstraint = UniqueConstraints.singleton
// Vi deklarerar aktiviteterna som affÀrsprocessen bestÄr av
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "${players.size} spelare har anslutit. Börjar vi?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... spelare ${signal.data} ansluter ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... spelare ${signal.data} Àr ute ...")
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val throwStartBall = messageSend("Ball") {
messageData = { 1 }
activation = { selectNextPlayer() }
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
// Nu bygger vi processen av de deklarerade aktiviteterna
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)
}
// Vi knyter extra hanterare till aktiviteterna för loggning
sendNewGameSignal.onExit { println("LÄt oss spela!") }
sendStopGameSignal.onExit { println("Stopp!") }
sendPlayerOut.onExit { println("$playerName: Jag Àr ute!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Steg ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Förutom att utföra affÀrslogik kan den givna koden generera en objektmodell av affÀrsprocessen, som kan visualiseras i form av ett diagram. Vi har Ànnu inte implementerat visualiseringen, sÄ jag fick lÀgga ner lite tid pÄ att rita (hÀr har jag förenklat BPMN-notationen nÀr det gÀller anvÀndning av grindar, för att förbÀttra överensstÀmmelsen mellan diagrammet och den givna koden):

Applikationen app2 kommer att inkludera affÀrsprocessen för en annan aktör:
class RandomPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RandomPlayer : ProcessImpl(randomPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RandomPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val randomPlayerModel = processModel(name = "RandomPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = if (process.players.isNotEmpty())
process.players.random()
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Diagram:

I applikationen app3 kommer vi att ge spelaren lite annat beteende: istÀllet för att slumpmÀssigt vÀlja nÀsta spelare kommer den att agera enligt round-robin-algoritmen:
class RoundRobinPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RoundRobinPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var nextPlayerIndex: Int by persistent(-1)
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val roundRobinPlayerModel = processModel(
name = "RoundRobinPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
var idx = process.nextPlayerIndex + 1
if (idx >= process.players.size) {
idx = 0
}
process.nextPlayerIndex = idx
val player = if (process.players.isNotEmpty())
process.players[idx]
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}I övrigt skiljer sig spelarens beteende inte frÄn det tidigare, sÄ diagrammet förÀndras inte.
Nu behövs ett test för att köra allt detta. Jag kommer bara att ange koden för sjÀlva testet, för att inte överbelasta artikeln med boilerplate (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");
// Nu behöver vi vÀnta lite sÄ att spelarna "lÀr kÀnna" varandra.
// Att vÀnta med sleep Àr en dÄlig lösning, men den Àr den enklaste.
// Gör inte sÄ i seriösa tester!
Thread.sleep(1000);
// Startar spelet, stÀnger av anvÀndaraktiviteten
assertTrue(closeTask(app1, pl1, "Start"));
app1.getWaiting().waitProcessFinished(pl1);
app2.getWaiting().waitProcessFinished(pl2);
app2.getWaiting().waitProcessFinished(pl3);
app3.getWaiting().waitProcessFinished(pl4);
app3.getWaiting().waitProcessFinished(pl5);
}
private Map playerParams(String name, int energy) {
Map params = new HashMap();
params.put("playerName", name);
params.put("energy", energy);
return params;
}Kör testet, kolla loggen:
konsolutdata
LÄsning av nyckel lock://app1/process/InitialPlayer har tagits bort
LÄt oss spela!
LÄsning av nyckel lock://app1/process/InitialPlayer har tagits bort
Spelare 2: Jag Àr hÀr!
Spelare 3: Jag Àr hÀr!
Spelare 4: Jag Àr hÀr!
Spelare 5: Jag Àr hÀr!
... gÄ med spelare Spelare 2 ...
... gÄ med spelare Spelare 4 ...
... gÄ med spelare Spelare 3 ...
... gÄ med spelare Spelare 5 ...
Steg 1: Spelare 1 >>> Spelare 3
Steg 2: Spelare 3 >>> Spelare 5
Steg 3: Spelare 5 >>> Spelare 3
Steg 4: Spelare 3 >>> Spelare 4
Steg 5: Spelare 4 >>> Spelare 3
Steg 6: Spelare 3 >>> Spelare 4
Steg 7: Spelare 4 >>> Spelare 5
Steg 8: Spelare 5 >>> Spelare 2
Steg 9: Spelare 2 >>> Spelare 5
Steg 10: Spelare 5 >>> Spelare 4
Steg 11: Spelare 4 >>> Spelare 2
Steg 12: Spelare 2 >>> Spelare 4
Steg 13: Spelare 4 >>> Spelare 1
Steg 14: Spelare 1 >>> Spelare 4
Steg 15: Spelare 4 >>> Spelare 3
Steg 16: Spelare 3 >>> Spelare 1
Steg 17: Spelare 1 >>> Spelare 2
Steg 18: Spelare 2 >>> Spelare 3
Steg 19: Spelare 3 >>> Spelare 1
Steg 20: Spelare 1 >>> Spelare 5
Steg 21: Spelare 5 >>> Spelare 1
Steg 22: Spelare 1 >>> Spelare 2
Steg 23: Spelare 2 >>> Spelare 4
Steg 24: Spelare 4 >>> Spelare 5
Steg 25: Spelare 5 >>> Spelare 3
Steg 26: Spelare 3 >>> Spelare 4
Steg 27: Spelare 4 >>> Spelare 2
Steg 28: Spelare 2 >>> Spelare 5
Steg 29: Spelare 5 >>> Spelare 2
Steg 30: Spelare 2 >>> Spelare 1
Steg 31: Spelare 1 >>> Spelare 3
Steg 32: Spelare 3 >>> Spelare 4
Steg 33: Spelare 4 >>> Spelare 1
Steg 34: Spelare 1 >>> Spelare 3
Steg 35: Spelare 3 >>> Spelare 4
Steg 36: Spelare 4 >>> Spelare 3
Steg 37: Spelare 3 >>> Spelare 2
Steg 38: Spelare 2 >>> Spelare 5
Steg 39: Spelare 5 >>> Spelare 4
Steg 40: Spelare 4 >>> Spelare 5
Steg 41: Spelare 5 >>> Spelare 1
Steg 42: Spelare 1 >>> Spelare 5
Steg 43: Spelare 5 >>> Spelare 3
Steg 44: Spelare 3 >>> Spelare 5
Steg 45: Spelare 5 >>> Spelare 2
Steg 46: Spelare 2 >>> Spelare 3
Steg 47: Spelare 3 >>> Spelare 2
Steg 48: Spelare 2 >>> Spelare 5
Steg 49: Spelare 5 >>> Spelare 4
Steg 50: Spelare 4 >>> Spelare 2
Steg 51: Spelare 2 >>> Spelare 5
Steg 52: Spelare 5 >>> Spelare 1
Steg 53: Spelare 1 >>> Spelare 5
Steg 54: Spelare 5 >>> Spelare 3
Steg 55: Spelare 3 >>> Spelare 5
Steg 56: Spelare 5 >>> Spelare 2
Steg 57: Spelare 2 >>> Spelare 1
Steg 58: Spelare 1 >>> Spelare 4
Steg 59: Spelare 4 >>> Spelare 1
Steg 60: Spelare 1 >>> Spelare 4
Steg 61: Spelare 4 >>> Spelare 3
Steg 62: Spelare 3 >>> Spelare 2
Steg 63: Spelare 2 >>> Spelare 5
Steg 64: Spelare 5 >>> Spelare 4
Steg 65: Spelare 4 >>> Spelare 5
Steg 66: Spelare 5 >>> Spelare 1
Steg 67: Spelare 1 >>> Spelare 5
Steg 68: Spelare 5 >>> Spelare 3
Steg 69: Spelare 3 >>> Spelare 4
Steg 70: Spelare 4 >>> Spelare 2
Steg 71: Spelare 2 >>> Spelare 5
Steg 72: Spelare 5 >>> Spelare 2
Steg 73: Spelare 2 >>> Spelare 1
Steg 74: Spelare 1 >>> Spelare 4
Steg 75: Spelare 4 >>> Spelare 1
Steg 76: Spelare 1 >>> Spelare 2
Steg 77: Spelare 2 >>> Spelare 5
Steg 78: Spelare 5 >>> Spelare 4
Steg 79: Spelare 4 >>> Spelare 3
Steg 80: Spelare 3 >>> Spelare 1
Steg 81: Spelare 1 >>> Spelare 5
Steg 82: Spelare 5 >>> Spelare 1
Steg 83: Spelare 1 >>> Spelare 4
Steg 84: Spelare 4 >>> Spelare 5
Steg 85: Spelare 5 >>> Spelare 3
Steg 86: Spelare 3 >>> Spelare 5
Steg 87: Spelare 5 >>> Spelare 2
Steg 88: Spelare 2 >>> Spelare 3
Spelare 2: Jag Àr ute!
Steg 89: Spelare 3 >>> Spelare 4
... spelare Spelare 2 Àr ute ...
Steg 90: Spelare 4 >>> Spelare 1
Steg 91: Spelare 1 >>> Spelare 3
Steg 92: Spelare 3 >>> Spelare 1
Steg 93: Spelare 1 >>> Spelare 4
Steg 94: Spelare 4 >>> Spelare 3
Steg 95: Spelare 3 >>> Spelare 5
Steg 96: Spelare 5 >>> Spelare 1
Steg 97: Spelare 1 >>> Spelare 5
Steg 98: Spelare 5 >>> Spelare 3
Steg 99: Spelare 3 >>> Spelare 5
Steg 100: Spelare 5 >>> Spelare 4
Steg 101: Spelare 4 >>> Spelare 5
Spelare 4: Jag Àr ute!
... spelare Spelare 4 Àr ute ...
Steg 102: Spelare 5 >>> Spelare 1
Steg 103: Spelare 1 >>> Spelare 3
Steg 104: Spelare 3 >>> Spelare 1
Steg 105: Spelare 1 >>> Spelare 3
Steg 106: Spelare 3 >>> Spelare 5
Steg 107: Spelare 5 >>> Spelare 3
Steg 108: Spelare 3 >>> Spelare 1
Steg 109: Spelare 1 >>> Spelare 3
Steg 110: Spelare 3 >>> Spelare 5
Steg 111: Spelare 5 >>> Spelare 1
Steg 112: Spelare 1 >>> Spelare 3
Steg 113: Spelare 3 >>> Spelare 5
Steg 114: Spelare 5 >>> Spelare 3
Steg 115: Spelare 3 >>> Spelare 1
Steg 116: Spelare 1 >>> Spelare 3
Steg 117: Spelare 3 >>> Spelare 5
Steg 118: Spelare 5 >>> Spelare 1
Steg 119: Spelare 1 >>> Spelare 3
Steg 120: Spelare 3 >>> Spelare 5
Steg 121: Spelare 5 >>> Spelare 3
Spelare 5: Jag Àr ute!
... spelare Spelare 5 Àr ute ...
Steg 122: Spelare 3 >>> Spelare 5
Steg 123: Spelare 5 >>> Spelare 1
Spelare 5: Jag Àr ute!
Steg 124: Spelare 1 >>> Spelare 3
... spelare Spelare 5 Àr ute ...
Steg 125: Spelare 3 >>> Spelare 1
Steg 126: Spelare 1 >>> Spelare 3
Spelare 1: Jag Àr ute!
... spelare Spelare 1 Àr ute ...
Steg 127: Spelare 3 >>> Spelare 3
Spelare 3: Jag Àr ute!
Steg 128: Spelare 3 >>> Spelare 3
... spelare Spelare 3 Àr ute ...
Spelare 3: Jag Àr ute!
Stopp!
Steg 129: Spelare 3 >>> Spelare 3
Spelare 3: Jag Àr ute!Av detta kan vi dra flera viktiga slutsatser:
- med nödvÀndiga verktyg kan applikationsutvecklare skapa integrationsinteraktioner mellan applikationer utan att avbryta affÀrslogiken;
- komplexiteten av integrationsuppgiften, som krÀver ingenjörskompetens, kan döljas inom ramen för ett ramverk om det initialt Àr inbyggt i ramverkets arkitektur. SvÄrighetsgraden av uppgiften gÄr inte att dölja, sÄ lösningen pÄ en svÄr uppgift i koden kommer att se ut dÀrefter;
- vid utveckling av integrationslogik Àr det nödvÀndigt att ta hÀnsyn till eventual consistency och avsaknaden av linjÀrisering av tillstÄndsförÀndringar hos alla deltagare i integrationen. Detta tvingar oss att komplicera logiken för att göra den okÀnslig för ordningen av yttre hÀndelser. I vÄrt exempel mÄste spelaren delta i spelet först efter att han har anmÀlt sin avgÄng frÄn spelet: andra spelare kommer fortsÀtta att passera bollen till honom, tills informationen om hans avgÄng nÄr och behandlas av alla deltagare. Denna logik följer inte reglerna för spelet utan Àr en kompromisslösning inom den valda arkitekturen.
Vi kommer nu att diskutera olika detaljer kring vÄr lösning, kompromisser och andra aspekter.
Alla meddelanden â i en kö
Alla integrerade applikationer arbetar med en gemensam integrationsbuss, som representeras av en extern mĂ€klare, en kö BPMQueue â för meddelanden och en topic BPMTopic â för signaler (hĂ€ndelser). Att skicka alla meddelanden genom en enda kö Ă€r i sig en kompromiss. PĂ„ affĂ€rslogiknivĂ„ kan det nu introduceras hur mĂ„nga nya typer av meddelanden som helst utan att behöva Ă€ndra systemets struktur. Detta Ă€r en betydande förenkling, men det medför vissa risker som i kontexten av vĂ„ra typiska uppgifter inte verkade sĂ€rskilt betydande.

Det finns dock en viktig detalj: varje applikation filtrerar bort "sina" meddelanden frÄn kön redan vid ingÄngen, baserat pÄ sitt domÀnnamn. DomÀnen kan ocksÄ anges i signalerna om det Àr nödvÀndigt att begrÀnsa signalens "synomrÄde" till en enda applikation. Detta bör öka bussens kapacitet, men affÀrslogiken mÄste nu hantera domÀnnamn: för adressering av meddelanden - obligatoriskt, för signaler - önskvÀrt.
SÀkerstÀllande av tillförlitligheten för integrationsbussen
Tillförlitlighet bestÄr av flera delar:
- den valda meddelandebrokaren - en kritiskt viktig komponent i arkitekturen och en enhetlig felpunkt: den mÄste vara tillrÀckligt felresistent. Endast beprövade implementationer ska anvÀndas, med bra stöd och ett stort community;
- det Àr nödvÀndigt att sÀkerstÀlla hög tillgÀnglighet för meddelandebrokaren, för vilket den mÄste vara fysiskt separat frÄn de integrerade applikationerna (det Àr betydligt svÄrare och dyrare att sÀkerstÀlla hög tillgÀnglighet för applikationer med affÀrslogik);
- brokaren mÄste sÀkerstÀlla "at least once" leveransgarantier. Detta Àr ett obligatoriskt krav för att integrationsbussen ska fungera pÄ ett tillförlitligt sÀtt. Det finns inget behov av garantier pÄ nivÄn "exactly once": affÀrsprocesser Àr vanligtvis inte kÀnsliga för att meddelanden eller hÀndelser Äterkommer, och i sÀrskilda uppgifter dÀr detta Àr viktigt, Àr det enklare att lÀgga till en extra kontroll i affÀrslogiken Àn att stÀndigt anvÀnda tillrÀckligt "dyra" garantier;
- meddelande- och signalöverföringar bör involveras i den gemensamma transaktionen med förÀndringar i affÀrsprocessernas och domÀnens data. Det föredragna alternativet Àr att anvÀnda mönstret , men det krÀver en extra tabell i databasen och en retranslator. I JEE-applikationer kan detta förenklas med anvÀndning av en lokal JTA-hanterare, men anslutningen till den valda brokaren mÄste kunna fungera i lÀget ;
- behandlare för inkommande meddelanden och hÀndelser mÄste ocksÄ fungera med transaktionen för förÀndring av affÀrsprocessens status: om en sÄdan transaktion Ängras, bör Àven mottagandet av meddelandet avbrytas;
- meddelanden som inte kunde levereras pÄ grund av fel ska lagras i ett separat förrÄd (Dead Letter Queue). Vi har skapat en separat plattformsmikrotjÀnst för detta ÀndamÄl som lagrar sÄdana meddelanden i sitt lager, indexerar dem baserat pÄ attribut (för snabb gruppering och sökning) och erbjuder ett API för att visa, ÄterbestÀlla till destinationsadressen och ta bort meddelanden. Systemadministratörer kan anvÀnda denna tjÀnst genom sitt webbgrÀnssnitt;
- I brokerinstÀllningarna mÄste antalet leveransförsök och fördröjningarna mellan leveranser justeras för att minska risken för att meddelanden hamnar i DLQ (det Àr praktiskt taget omöjligt att berÀkna optimala parametrar, men man kan agera empiriskt och justera dem under drift);
- DLQ-lagringen mÄste övervakas kontinuerligt, och övervakningssystemet ska meddela systemadministratörer för att snabbt reagera pÄ eventuella otillförlitliga meddelanden. Detta kommer att minska "skadans omfattning" av det uppkomna felet eller affÀrslogikfelet;
- Integrationsbussen ska vara okÀnslig för tillfÀllig frÄnvaro av applikationer: prenumerationer pÄ Àmnet mÄste vara hÄllbara, och applikationens domÀnnamn mÄste vara unikt sÄ att inga andra försöker behandla dess meddelanden i kön under dess frÄnvaro.
SÀkerstÀllande av trÄdsÀkerhet för affÀrslogik
En och samma förekomst av affÀrsprocessen kan snart ta emot flera meddelanden och hÀndelser som kommer att behandlas parallellt. Samtidigt mÄste allt vara enkelt och trÄdsÀkert för applikationsutvecklaren.
AffÀrslogiken i processen hanterar varje extern hÀndelse som pÄverkar denna affÀrsprocess för sig. SÄdana hÀndelser kan vara:
- starta en instans av affÀrsprocessen;
- ÄtgÀrd frÄn anvÀndaren som rör aktiviteten inom affÀrsprocessen;
- mottagande av ett meddelande eller signal som förekomsten av affÀrsprocessen Àr prenumererad pÄ;
- utlösning av en timer som stÀllts in av affÀrsprocessens instans;
- kontrollerande pÄverkan via API (t.ex. en nödstopp av processen).
Varje sÄdant event kan förÀndra tillstÄndet för en instans av affÀrsprocessen: vissa aktiviteter kan avslutas och andra kan pÄbörjas, de persistenta egenskaperna kan Àndras. Avslutningen av vilken aktivitet som helst kan leda till aktiverandet av en eller flera efterföljande aktiviteter. Dessa kan i sin tur stanna kvar och vÀnta pÄ andra hÀndelser eller, om de inte krÀver nÄgra ytterligare data, avslutas i samma transaktion. Innan transaktionen avslutas sparas det nya tillstÄndet för affÀrsprocessen i databasen, dÀr det kommer att vÀnta pÄ att den nÀsta externa hÀndelsen intrÀffar.
De persistenta affÀrsprocessdata som sparas i en relationsdatabas Àr en mycket praktisk synkroniseringspunkt för bearbetning, om man anvÀnder SELECT FOR UPDATE. Om en transaktion lyckas hÀmta tillstÄndet för affÀrsprocessen frÄn databasen för att Àndra det, kan ingen annan transaktion parallellt hÀmta samma tillstÄnd för en annan Àndring, och efter att den första transaktionen har avslutats kommer den andra garanterat att fÄ det redan Àndrade tillstÄndet.
Genom att anvÀnda pessimiska lÄsningar pÄ databassidan uppfyller vi alla nödvÀndiga krav , samtidigt som vi bibehÄller möjligheten att skala applikationen med affÀrslogik genom att öka antalet körande instanser.
Men pessimistiska lÄsningar riskerar att leda till dödlÀgen, vilket innebÀr att SELECT FOR UPDATE ÀndÄ borde begrÀnsas med en rimlig timeout för att hantera situationer med dödlÀgen i tydliga fall i affÀrslogiken.
Ett annat problem Àr synkronisering av starten av affÀrsprocessen. SÄ lÀnge det inte finns nÄgon instans av affÀrsprocessen, finns det inte heller nÄgot tillstÄnd i databasen, sÄ den beskrivna metoden Àr inte tillÀmplig. Om unikhet för instansen av affÀrsprocessen i ett visst omfattning behövs, krÀvs det ett synkroniseringsobjekt kopplat till processklass och motsvarande omfattning. För att lösa detta problem anvÀnder vi en annan mekanism för lÄsning som tillÄter att ta lÄs pÄ en godtycklig resurs, angiven med en nyckel i URI-format, via en extern tjÀnst.
I vÄra exempel innehÄller affÀrsprocessen InitialPlayer en deklaration
uniqueConstraint = UniqueConstraints.singletonDÀrför finns det meddelanden i loggen om att lÄsningen av den aktuella nyckeln tas och frigörs. Det finns inga sÄdana meddelanden för andra affÀrsprocesser: uniqueConstraint Àr inte angiven.
Problem med affÀrsprocesser som har ett persistent tillstÄnd
Ibland hjÀlper ett persistent tillstÄnd inte bara, utan kan Àven vara mycket besvÀrligt för utvecklingen.
Problem uppstÄr nÀr det behövs förÀndringar i affÀrslogiken och/eller affÀrsprocessmodellen. Inte alla sÄdana Àndringar Àr kompatibla med det gamla tillstÄndet i affÀrsprocesserna. Om det finns mÄnga 'levande' instanser i databasen kan införandet av inkompatibla Àndringar orsaka mycket besvÀr, som vi ofta stött pÄ vid anvÀndning av jBPM.
Beroende pÄ hur djupa Àndringarna Àr, kan man agera pÄ tvÄ sÀtt:
- skapa en ny typ av affÀrsprocess för att undvika inkompatibla Àndringar i den gamla, och anvÀnda den istÀllet för den gamla nÀr nya instanser startas. De gamla instanserna kommer att fortsÀtta fungera 'som vanligt';
- migrera det persistenta tillstÄndet i affÀrsprocesserna nÀr affÀrslogiken uppdateras.
Den första vÀgen Àr enklare, men har sina begrÀnsningar och nackdelar, till exempel:
- duplicering av affÀrslogik i mÄnga modeller av affÀrsprocesser, ökad volym av affÀrslogik;
- det krĂ€vs ofta en omedelbar övergĂ„ng till den nya affĂ€rslogiken (i frĂ„ga om integrationsuppgifter â nĂ€stan alltid);
- utvecklaren vet inte nÀr man kan ta bort förÄldrade modeller.
I praktiken anvÀnder vi bÄda tillvÀgagÄngssÀtten, men vi har vidtagit ÄtgÀrder för att förenkla vÄrt arbete:
- i databasen lagras det persistenta tillstÄndet för affÀrsprocessen i ett lÀttlÀst och bearbetbart format: i JSON-format. Detta möjliggör migrationer bÄde inom och utanför applikationen. I nödfall kan man Àven justera manuellt (sÀrskilt anvÀndbart under utveckling och felsökning);
- integrationsaffÀrslogiken anvÀnder inte namn pÄ affÀrsprocesser, sÄ att man nÀr som helst kan byta implementeringen av en av de deltagande processerna till en ny, med ett nytt namn (till exempel 'InitialPlayerV2'). Kopplingen sker genom namnen pÄ meddelanden och signaler;
- Processmodellen har ett versionsnummer som vi ökar om vi gör inkompatibla Àndringar i denna modell, och detta nummer sparas tillsammans med tillstÄndet för processinstansen;
- Det persistenta tillstÄndet för processen lÀses först frÄn databasen till en bekvÀm objektmodell som migrationsproceduren kan arbeta med om versionsnumret har Àndrats;
- Migrationsproceduren placeras bredvid affÀrslogiken och anropas "lat" för varje instans av affÀrsprocessen vid dess ÄterstÀllning frÄn databasen;
- Om det Àr nödvÀndigt att migrera tillstÄndet för alla processinstanser snabbt och synkront, tillÀmpas mer klassiska lösningar för databas-migrering, men dÀr mÄste man arbeta med JSON.
Behöver vi en ytterligare ramverk för affÀrsprocesser?
De lösningar som beskrivs i artikeln har gjort det möjligt för oss att avsevÀrt förenkla vÄrt arbete, utöka mÀngden frÄgor som kan lösas pÄ applikationsutvecklingsnivÄ och göra idéerna om att separera affÀrslogik i mikrotjÀnster mer attraktiva. Mycket arbete har lagts ner för detta, en mycket "lÀttviktig" ramverk för affÀrsprocesser har skapats, liksom hjÀlpkomponenter för att lösa de angivna problemen i ett brett sammanhang av applikationsuppgifter. Vi har en önskan att dela dessa resultat, fÄ utvecklingen av gemensamma komponenter tillgÀnglig som öppen kÀllkod under en fri licens. Detta kommer att krÀva vissa anstrÀngningar och tid. FörstÄelsen för efterfrÄgan pÄ sÄdana lösningar skulle kunna bli ett ytterligare incitament för oss. I den föreslagna artikeln ges mycket lite uppmÀrksamhet Ät möjligheterna hos sjÀlva ramverket, men nÄgra av dem Àr synliga frÄn de presenterade exemplen. Om vi ÀndÄ publicerar vÄrt ramverk kommer det att Àgnas en separat artikel. Tills dess vore vi tacksamma om ni kunde lÀmna en liten feedback genom att svara pÄ frÄgan:
Endast registrerade anvÀndare kan delta i omröstningen. , tack.
Behöver vi en ytterligare ramverk för affÀrsprocesser?
18,8%Ja, vi har lÀnge letat efter nÄgot liknande.
12,5%Intressant att fÄ veta mer om er implementering, kanske blir det anvÀndbart.
6,2%Vi anvÀnder ett av de befintliga ramverken, men övervÀger att byta.
18,8%Vi anvÀnder ett av de befintliga ramverken, allt Àr okej.
18,8%Vi klarar oss utan ramverk.
25,0%Vi skriver vÄrt eget.
16 anvÀndare har röstat. 7 anvÀndare har avstÄtt.
KĂ€lla: habr.com
