Integration i BPM-stil

Integration i BPM-stil

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

Integration i BPM-stil
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).

Integration i BPM-stil
SÄ hÀr ser processen ut efter nÄgra iterationer av kravutveckling.

Lösningen pÄ denna situation blev 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 medan man bibehöll ett tillrÀckligt informativt och aktuellt diagram i notationen BPMN2.

Integration i BPM-stil
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.

Integration i BPM-stil

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 Saga-mönstret.

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.

Integration i BPM-stil

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.

Integration i BPM-stil

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

Integration i BPM-stil

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:

Integration i BPM-stil

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.

Integration i BPM-stil

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 Transactional Outbox, 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 XA;
  • 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 DLQ (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 ACID, 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.singleton

DÀ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:

  1. 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';
  2. 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. Logga in, 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

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster