BPM stil integration

BPM stil integration

Hej, Habr!

Vores virksomhed har specialiseret sig i udvikling af ERP-klasse softwareløsninger, hvor broderparten er optaget af transaktionssystemer med en enorm mængde forretningslogik og arbejdsgange a la EDMS. Moderne versioner af vores produkter er baseret på JavaEE-teknologier, men vi eksperimenterer også aktivt med mikrotjenester. Et af de mest problematiske områder af sådanne løsninger er integrationen af ​​forskellige undersystemer relateret til tilstødende domæner. Integrationsopgaver har altid givet os en kæmpe hovedpine, uanset de arkitektoniske stilarter, teknologistabler og rammer, vi bruger, men på det seneste er der sket fremskridt med at løse sådanne problemer.

I artiklen, der er gjort opmærksom på, vil jeg tale om NPO Kristas erfaringer og arkitektoniske forskning i det udpegede område. Vi vil også overveje et eksempel på en simpel løsning på et integrationsproblem fra en applikationsudviklers synspunkt og finde ud af, hvad der gemmer sig bag denne enkelhed.

Ansvarsfraskrivelse

De arkitektoniske og tekniske løsninger, der er beskrevet i artiklen, tilbydes af mig baseret på personlig erfaring i forbindelse med specifikke opgaver. Disse løsninger hævder ikke at være universelle og er muligvis ikke optimale under andre brugsforhold.

Hvad har BPM med det at gøre?

For at besvare dette spørgsmål er vi nødt til at dykke lidt ned i detaljerne i de anvendte problemer i vores løsninger. Hoveddelen af ​​forretningslogikken i vores typiske transaktionssystem er at indtaste data i databasen gennem brugergrænseflader, manuelt og automatisk kontrollere disse data, sende dem gennem nogle arbejdsgange, udgive dem til et andet system / analytisk database / arkiv, generere rapporter. Systemets nøglefunktion for kunderne er således automatiseringen af ​​deres interne forretningsprocesser.

For nemheds skyld bruger vi udtrykket "dokument" i kommunikation som en abstraktion af et datasæt, forenet af en fælles nøgle, hvortil en specifik arbejdsgang kan "tilknyttes".
Men hvad med integrationslogikken? Når alt kommer til alt er integrationsopgaven genereret af systemets arkitektur, som "saves" i dele IKKE efter kundens anmodning, men under indflydelse af helt andre faktorer:

  • under indflydelse af Conways lov;
  • som et resultat af genbrug af delsystemer, der tidligere er udviklet til andre produkter;
  • som besluttet af arkitekten, baseret på ikke-funktionelle krav.

Der er en stor fristelse til at adskille integrationslogikken fra hovedarbejdsgangens forretningslogik for ikke at forurene forretningslogikken med integrationsartefakter og spare applikationsudvikleren fra at skulle dykke ned i de særlige kendetegn ved systemets arkitektoniske landskab. Denne tilgang har en række fordele, men praksis viser dens ineffektivitet:

  • løsning af integrationsproblemer glider normalt ned til de enkleste muligheder i form af synkrone opkald på grund af de begrænsede forlængelsespunkter i implementeringen af ​​hovedarbejdsgangen (mere om manglerne ved synkron integration nedenfor);
  • integrationsartefakter trænger stadig ind i den primære forretningslogik, når feedback fra et andet undersystem er påkrævet;
  • applikationsudvikleren ignorerer integrationen og kan nemt bryde den ved at ændre arbejdsgangen;
  • systemet ophører med at være en enkelt helhed fra brugerens synspunkt, "sømme" mellem undersystemer bliver mærkbare, redundante brugeroperationer opstår, der initierer overførsel af data fra et undersystem til et andet.

En anden tilgang er at betragte integrationsinteraktioner som en integreret del af kerneforretningens logik og arbejdsgang. For at forhindre, at applikationsudvikleres færdighedskrav stiger i vejret, bør skabelsen af ​​nye integrationsinteraktioner ske nemt og naturligt med minimale muligheder for at vælge en løsning. Dette er sværere, end det ser ud: Værktøjet skal være kraftigt nok til at give brugeren den nødvendige variation af muligheder for dets brug og samtidig ikke tillade sig at blive skudt i foden. Der er mange spørgsmål, som en ingeniør skal besvare i forbindelse med integrationsopgaver, men som en applikationsudvikler ikke bør tænke på i deres daglige arbejde: transaktionsgrænser, konsistens, atomicitet, sikkerhed, skalering, belastning og ressourcefordeling, routing, marshaling, udbredelse og omskiftningskontekster osv. Det er nødvendigt at tilbyde applikationsudviklere ret simple beslutningsskabeloner, hvori svar på alle sådanne spørgsmål allerede er gemt. Disse mønstre bør være sikre nok: forretningslogikken ændrer sig meget ofte, hvilket øger risikoen for at indføre fejl, omkostningerne ved fejl bør forblive på et ret lavt niveau.

Men alligevel, hvad har BPM med det at gøre? Der er mange muligheder for at implementere workflow...
Faktisk er en anden implementering af forretningsprocesser meget populær i vores løsninger - gennem den deklarative indstilling af tilstandsovergangsdiagrammet og at forbinde handlere med forretningslogik til overgangene. Samtidig er den tilstand, der bestemmer "dokumentets" nuværende position i forretningsprocessen, en egenskab ved selve "dokumentet".

BPM stil integration
Sådan ser processen ud i starten af ​​projektet

Populariteten af ​​en sådan implementering skyldes den relative enkelhed og hastighed ved at skabe lineære forretningsprocesser. Men efterhånden som softwaresystemer bliver mere komplekse, vokser den automatiserede del af forretningsprocessen og bliver mere kompleks. Der er behov for nedbrydning, genbrug af dele af processer, samt gaffelprocesser, så hver gren udføres parallelt. Under sådanne forhold bliver værktøjet ubelejligt, og tilstandsovergangsdiagrammet mister sit informationsindhold (integrationsinteraktioner afspejles slet ikke i diagrammet).

BPM stil integration
Sådan ser processen ud efter flere gentagelser af afklaring af kravene

Vejen ud af denne situation var integrationen af ​​motoren jBPM ind i nogle produkter med de mest komplekse forretningsprocesser. På kort sigt havde denne løsning en vis succes: det blev muligt at implementere komplekse forretningsprocesser og samtidig bevare et ret informativt og opdateret diagram i notationen BPMN2.

BPM stil integration
En lille del af en kompleks forretningsproces

På lang sigt levede løsningen ikke op til forventningerne: Den høje arbejdsintensitet ved at skabe forretningsprocesser gennem visuelle værktøjer gjorde det ikke muligt at opnå acceptable produktivitetsindikatorer, og selve værktøjet blev et af de mest mislikede blandt udviklere. Der var også klager over motorens indre struktur, hvilket førte til udseendet af mange "patches" og "krykker".

Det vigtigste positive aspekt ved at bruge jBPM var erkendelsen af ​​fordelene og skaderne ved at have sin egen vedvarende tilstand for en forretningsprocesinstans. Vi så også muligheden for at bruge en procestilgang til at implementere komplekse integrationsprotokoller mellem forskellige applikationer ved hjælp af asynkrone interaktioner gennem signaler og beskeder. Tilstedeværelsen af ​​en vedvarende tilstand spiller en afgørende rolle i dette.

På baggrund af ovenstående kan vi konkludere: Procestilgangen i BPM-stilen giver os mulighed for at løse en bred vifte af opgaver til automatisering af stadig mere komplekse forretningsprocesser, harmonisk indpasse integrationsaktiviteter i disse processer og bevare evnen til visuelt at vise den implementerede proces i en passende notation.

Ulemper ved synkrone opkald som integrationsmønster

Synkron integration refererer til det enkleste blokeringsopkald. Et undersystem fungerer som serversiden og eksponerer API'et med den ønskede metode. Et andet delsystem fungerer som klientside og ringer på det rigtige tidspunkt med forventning om et resultat. Afhængigt af systemets arkitektur kan klient- og serversiden hostes enten i samme applikation og proces eller i forskellige. I det andet tilfælde skal du anvende en vis implementering af RPC og sørge for rangering af parametrene og resultatet af opkaldet.

BPM stil integration

Et sådant integrationsmønster har et ret stort sæt af ulemper, men det er meget udbredt i praksis på grund af dets enkelthed. Implementeringshastigheden fængsler og får dig til at anvende den igen og igen i betingelserne for "brændende" deadlines, hvor du skriver løsningen i teknisk gæld. Men det sker også, at uerfarne udviklere bruger det ubevidst, simpelthen ikke indser de negative konsekvenser.

Ud over den mest åbenlyse stigning i forbindelsen mellem delsystemer er der mindre indlysende problemer med at "spredning" og "strække" transaktioner. Faktisk, hvis forretningslogikken foretager ændringer, så er transaktioner uundværlige, og transaktioner låser til gengæld visse applikationsressourcer, der påvirkes af disse ændringer. Det vil sige, at indtil et undersystem venter på et svar fra et andet, vil det ikke være i stand til at fuldføre transaktionen og frigive låse. Dette øger risikoen for en række forskellige effekter markant:

  • systemets reaktionsevne går tabt, brugere venter længe på svar på anmodninger;
  • serveren holder generelt op med at svare på brugeranmodninger på grund af en overfyldt trådpulje: de fleste af trådene "står" på låsen af ​​den ressource, der er optaget af transaktionen;
  • dødvande begynder at dukke op: sandsynligheden for deres forekomst afhænger stærkt af transaktionernes varighed, mængden af ​​forretningslogik og låse involveret i transaktionen;
  • Transaktions timeout udløbsfejl vises;
  • serveren "falder" på OutOfMemory, hvis opgaven kræver behandling og ændring af store mængder data, og tilstedeværelsen af ​​synkrone integrationer gør det meget vanskeligt at opdele behandlingen i "lettere" transaktioner.

Fra et arkitektonisk synspunkt fører brugen af ​​blokering af opkald under integration til et tab af kvalitetskontrol af individuelle delsystemer: det er umuligt at sikre et delsystems kvalitetsmål isoleret fra et andet delsystems kvalitetsmål. Hvis delsystemer udvikles af forskellige teams, er dette et stort problem.

Tingene bliver endnu mere interessante, hvis de delsystemer, der integreres, er i forskellige applikationer, og der skal laves synkrone ændringer på begge sider. Hvordan gør man disse ændringer transaktionelle?

Hvis der foretages ændringer i separate transaktioner, skal der sørges for robust undtagelseshåndtering og kompensation, og dette eliminerer helt den største fordel ved synkrone integrationer - enkelhed.

Distribuerede transaktioner kommer også til syne, men vi bruger dem ikke i vores løsninger: det er svært at sikre pålidelighed.

"Saga" som en løsning på problemet med transaktioner

Med den stigende popularitet af mikrotjenester er der en stigende efterspørgsel efter Saga mønster.

Dette mønster løser perfekt ovenstående problemer med lange transaktioner og udvider også mulighederne for at styre systemets tilstand fra siden af ​​forretningslogikken: kompensation efter en mislykket transaktion ruller muligvis ikke systemet tilbage til dets oprindelige tilstand, men giver et alternativ databehandlingsrute. Det giver dig også mulighed for ikke at gentage vellykket gennemførte databehandlingstrin, når du forsøger at bringe processen til en "god" afslutning.

Interessant nok er dette mønster i monolitiske systemer også relevant, når det kommer til integration af løst koblede delsystemer, og der er negative effekter forårsaget af lange transaktioner og de tilsvarende ressourcelåse.

Med hensyn til vores forretningsprocesser i BPM-stilen, viser det sig at være meget let at implementere sagaerne: sagaernes individuelle trin kan indstilles som aktiviteter inden for forretningsprocessen, og den vedvarende tilstand af forretningsprocessen afgør bl.a. andre ting, sagaernes indre tilstand. Det vil sige, at vi ikke har brug for nogen yderligere koordineringsmekanisme. Alt du behøver er en meddelelsesmægler med support til "mindst én gang" garantier som transport.

Men sådan en løsning har også sin egen "pris":

  • forretningslogikken bliver mere kompleks: du skal regne ud kompensation;
  • det vil være nødvendigt at opgive fuld konsistens, hvilket kan være særligt følsomt for monolitiske systemer;
  • arkitekturen bliver lidt mere kompliceret, der er et yderligere behov for en meddelelsesmægler;
  • Der vil være behov for yderligere overvågnings- og administrationsværktøjer (selvom det generelt er endog godt: kvaliteten af ​​systemservicen vil stige).

For monolitiske systemer er begrundelsen for at bruge "Sags" ikke så indlysende. For mikrotjenester og andre SOA'er, hvor der højst sandsynligt allerede er en mægler, og fuld konsekvens blev ofret i starten af ​​projektet, kan fordelene ved at bruge dette mønster væsentligt opveje ulemperne, især hvis der er en praktisk API på forretningslogik niveau.

Indkapsling af forretningslogik i mikrotjenester

Da vi begyndte at eksperimentere med mikrotjenester, opstod et rimeligt spørgsmål: hvor skal man sætte domæneforretningslogikken i forhold til den service, der giver domænedatapersistens?

Når man ser på arkitekturen af ​​forskellige BPMS, kan det virke rimeligt at adskille forretningslogik fra persistens: skab et lag af platforms- og domæne-uafhængige mikrotjenester, der danner miljøet og containeren til eksekvering af domæneforretningslogik, og arrangere domænedatapersistens som en separat lag af meget enkle og lette mikrotjenester. Forretningsprocesser i dette tilfælde orkestrerer tjenesterne i persistenslaget.

BPM stil integration

Denne tilgang har et meget stort plus: du kan øge platformens funktionalitet, så meget du vil, og kun det tilsvarende lag af platformsmikrotjenester bliver "fed" af dette. Forretningsprocesser fra ethvert domæne får straks mulighed for at bruge platformens nye funktionalitet, så snart den er opdateret.

En mere detaljeret undersøgelse afslørede betydelige mangler ved denne tilgang:

  • en platformstjeneste, der udfører forretningslogikken for mange domæner på én gang, indebærer store risici som et enkelt point of failure. Hyppige ændringer af forretningslogikken øger risikoen for fejl, der fører til systemomfattende fejl;
  • præstationsproblemer: forretningslogik arbejder med sine data gennem en smal og langsom grænseflade:
    • dataene vil igen blive samlet og pumpet gennem netværksstakken;
    • domænetjenesten vil ofte returnere flere data, end forretningslogikken kræver til behandling, på grund af utilstrækkelige forespørgselsparameteriseringskapaciteter på niveauet af tjenestens eksterne API;
    • flere uafhængige stykker forretningslogik kan gentagne gange anmode om de samme data til behandling (du kan afhjælpe dette problem ved at tilføje sessionsbeans, der cacher data, men dette komplicerer arkitekturen yderligere og skaber problemer med datafrihed og cache-uvalidering);
  • transaktionsproblemer:
    • forretningsprocesser med vedvarende tilstand lagret af platformstjenesten er inkonsistente med domænedata, og der er ingen nemme måder at løse dette problem på;
    • flytning af låsen af ​​domænedata ud af transaktionen: Hvis domænets forretningslogik skal foretage ændringer, efter først at have kontrolleret rigtigheden af ​​de faktiske data, er det nødvendigt at udelukke muligheden for en konkurrencemæssig ændring i de behandlede data. Ekstern blokering af data kan hjælpe med at løse problemet, men en sådan løsning indebærer yderligere risici og reducerer systemets overordnede pålidelighed;
  • yderligere komplikationer ved opdatering: I nogle tilfælde skal du opdatere persistenstjenesten og forretningslogikken synkront eller i streng rækkefølge.

Til sidst måtte jeg gå tilbage til det grundlæggende: indkapsle domænedata og domæneforretningslogik i én mikrotjeneste. Denne tilgang forenkler opfattelsen af ​​mikrotjenesten som en integreret komponent i systemet og giver ikke anledning til ovenstående problemer. Dette er heller ikke gratis:

  • API-standardisering er påkrævet for interaktion med forretningslogik (især for at levere brugeraktiviteter som en del af forretningsprocesser) og API-platformtjenester; mere omhyggelig opmærksomhed på API-ændringer, fremad- og bagudkompatibilitet er påkrævet;
  • det er nødvendigt at tilføje yderligere runtime-biblioteker for at sikre, at forretningslogikken fungerer som en del af hver sådan mikroservice, og dette giver anledning til nye krav til sådanne biblioteker: lethed og et minimum af transitive afhængigheder;
  • forretningslogikudviklere skal holde styr på biblioteksversioner: hvis en mikroservice ikke er blevet færdiggjort i lang tid, så vil den højst sandsynligt indeholde en forældet version af bibliotekerne. Dette kan være en uventet hindring for at tilføje en ny funktion og kan kræve, at den gamle forretningslogik for en sådan tjeneste migreres til nye versioner af bibliotekerne, hvis der var inkompatible ændringer mellem versioner.

BPM stil integration

Et lag af platformstjenester er også til stede i en sådan arkitektur, men dette lag danner ikke længere en beholder til at udføre domæneforretningslogik, men kun dets miljø, der giver hjælpe "platform"-funktioner. Et sådant lag er ikke kun nødvendigt for at bevare domænemikrotjenesters lethed, men også for at centralisere administrationen.

For eksempel genererer brugeraktiviteter i forretningsprocesser opgaver. Når man arbejder med opgaver, skal brugeren dog se opgaver fra alle domæner i den generelle liste, hvilket betyder, at der skal være en passende opgaveregistreringsplatformstjeneste, renset for domæneforretningslogik. At beholde indkapslingen af ​​forretningslogik i denne sammenhæng er ret problematisk, og dette er endnu et kompromis i denne arkitektur.

Integration af forretningsprocesser gennem øjnene af en applikationsudvikler

Som allerede nævnt ovenfor skal applikationsudvikleren abstraheres fra de tekniske og tekniske funktioner i implementeringen af ​​samspillet mellem flere applikationer for at kunne regne med god udviklingsproduktivitet.

Lad os prøve at løse et ret vanskeligt integrationsproblem, specielt opfundet til artiklen. Dette vil være en "spil"-opgave, der involverer tre applikationer, hvor hver af dem definerer et eller andet domænenavn: "app1", "app2", "app3".

Inde i hver applikation lanceres forretningsprocesser, der begynder at "spille bold" gennem integrationsbussen. Beskeder med navnet "Ball" vil fungere som bolden.

Regler for spillet:

  • den første spiller er initiativtageren. Han inviterer andre spillere til spillet, starter spillet og kan afslutte det når som helst;
  • andre spillere erklærer deres deltagelse i spillet, "bliv bekendt" med hinanden og den første spiller;
  • efter at have modtaget bolden, vælger spilleren en anden deltagende spiller og sender bolden til ham. Det samlede antal gennemløb tælles med;
  • hver spiller har "energi", som aftager med hver aflevering af bolden fra den pågældende spiller. Når energien løber tør, bliver spilleren elimineret fra spillet og annoncerer deres pensionering;
  • hvis spilleren efterlades alene, erklærer han straks sin afgang;
  • når alle spillere er elimineret, erklærer den første spiller slutningen af ​​spillet. Hvis han forlod spillet tidligere, så er det tilbage at følge spillet for at fuldføre det.

For at løse dette problem vil jeg bruge vores DSL til forretningsprocesser, som giver dig mulighed for at beskrive logikken i Kotlin kompakt, med et minimum af en kedelplade.

I app1-applikationen vil den første spillers forretningsproces (han er også initiativtageren til spillet) fungere:

klasse InitialPlayer

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

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

class PlayersList : ArrayList<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    startFrom(sendNewGameSignal)
            .fork("mainFork") {
                next(startTask)
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut)
                        .branch("checkPlayers") {
                            ifTrue { players.isEmpty() }
                                    .next(sendStopGameSignal)
                                    .terminate()
                            ifElse().next(waitPlayerOut)
                        }
            }
    startTask.fork("afterStart") {
        next(throwStartBall)
                .branch("mainLoop") {
                    ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
                    ifElse().next(waitBall).next(throwBall).loop()
                }
        next(stopTask).next(sendStopGameSignal)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<InitialPlayer, Int>.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Ud over at udføre forretningslogik kan ovenstående kode producere en objektmodel af en forretningsproces, der kan visualiseres som et diagram. Vi har ikke implementeret visualizeren endnu, så vi var nødt til at bruge lidt tid på at tegne (her har jeg forenklet BPMN-notationen en smule vedrørende brugen af ​​porte for at forbedre konsistensen af ​​diagrammet med ovenstående kode):

BPM stil integration

app2 vil inkludere en anden spillers forretningsproces:

klasse RandomPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RandomPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val randomPlayerModel = processModel<RandomPlayer>(name = "RandomPlayer", 
                                                   version = 1) {

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

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RandomPlayer, Int>.selectNextPlayer() {
    val player = if (process.players.isNotEmpty()) 
        process.players.random() 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Diagram:

BPM stil integration

I app3-applikationen vil vi gøre spilleren til en lidt anderledes adfærd: i stedet for tilfældigt at vælge den næste spiller, vil han handle i henhold til round-robin-algoritmen:

klasse RoundRobinPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RoundRobinPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var nextPlayerIndex: Int by persistent(-1)
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val roundRobinPlayerModel = processModel<RoundRobinPlayer>(
        name = "RoundRobinPlayer", 
        version = 1) {

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

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RoundRobinPlayer, Int>.selectNextPlayer() {
    var idx = process.nextPlayerIndex + 1
    if (idx >= process.players.size) {
        idx = 0
    }
    process.nextPlayerIndex = idx
    val player = if (process.players.isNotEmpty()) 
        process.players[idx] 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Ellers adskiller spillerens adfærd sig ikke fra den forrige, så diagrammet ændres ikke.

Nu mangler vi en test for at køre det hele. Jeg vil kun give koden for selve testen for ikke at rode artiklen med en kedelplade (faktisk brugte jeg det tidligere oprettede testmiljø til at teste integrationen af ​​andre forretningsprocesser):

testGame()

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

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

Kør testen, se på loggen:

konsoludgang

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

Der kan drages flere vigtige konklusioner af alt dette:

  • hvis de nødvendige værktøjer er tilgængelige, kan applikationsudviklere skabe integrationsinteraktioner mellem applikationer uden at bryde væk fra forretningslogikken;
  • kompleksiteten (kompleksiteten) af en integrationsopgave, der kræver ingeniørkompetencer, kan skjules inde i rammen, hvis den i første omgang er fastlagt i rammeværkets arkitektur. Opgavens sværhedsgrad (sværhedsgrad) kan ikke skjules, så løsningen på en svær opgave i koden vil se ud i overensstemmelse hermed;
  • når man udvikler integrationslogik, er det nødvendigt at tage højde for i sidste ende konsistens og manglen på linearisering af tilstandsændringen for alle integrationsdeltagere. Dette tvinger os til at komplicere logikken for at gøre den ufølsom over for rækkefølgen, hvori eksterne begivenheder opstår. I vores eksempel er spilleren tvunget til at deltage i spillet, efter at han har meddelt, at han forlader spillet: andre spillere vil fortsætte med at sende bolden til ham, indtil oplysningerne om hans udgang når frem og behandles af alle deltagere. Denne logik følger ikke af spillets regler og er en kompromisløsning inden for rammerne af den valgte arkitektur.

Lad os derefter tale om de forskellige finesser i vores løsning, kompromiser og andre punkter.

Alle beskeder i én kø

Alle integrerede applikationer arbejder med én integrationsbus, der præsenteres som en ekstern mægler, én BPMQueue til beskeder og én BPMTopic-emne for signaler (hændelser). At sende alle beskeder gennem en enkelt kø er i sig selv et kompromis. På forretningslogikniveau kan du nu introducere så mange nye typer meddelelser, som du vil, uden at foretage ændringer i systemstrukturen. Dette er en væsentlig forenkling, men den indebærer visse risici, som i forbindelse med vores typiske opgaver ikke forekom os så væsentlige.

BPM stil integration

Der er dog en subtilitet her: hver applikation filtrerer "sine" beskeder fra køen ved indgangen efter navnet på sit domæne. Domænet kan også specificeres i signalerne, hvis du skal begrænse signalets "omfang" til en enkelt applikation. Dette skulle øge bussens båndbredde, men forretningslogikken skal nu fungere med domænenavne: obligatorisk for adressering af beskeder, ønskelig for signaler.

Sikring af pålideligheden af ​​integrationsbussen

Pålidelighed består af flere ting:

  • Den valgte meddelelsesmægler er en kritisk komponent i arkitekturen og et enkelt fejlpunkt: den skal være tilstrækkeligt fejltolerant. Du bør kun bruge gennemtestede implementeringer med god support og et stort fællesskab;
  • det er nødvendigt at sikre høj tilgængelighed af meddelelsesmægleren, for hvilken den skal være fysisk adskilt fra de integrerede applikationer (høj tilgængelighed af applikationer med anvendt forretningslogik er meget sværere og dyrere at levere);
  • mægleren er forpligtet til at stille "mindst én gang" leveringsgarantier. Dette er et obligatorisk krav for pålidelig drift af integrationsbussen. Der er ikke behov for garantier på "præcis én gang" niveau: Forretningsprocesser er normalt ikke følsomme over for gentagne modtagelse af beskeder eller begivenheder, og i særlige opgaver, hvor dette er vigtigt, er det lettere at tilføje yderligere kontroller til forretningslogikken end konstant at bruge snarere "dyre" " garantier;
  • afsendelse af beskeder og signaler skal være involveret i en fælles transaktion med en ændring i tilstanden af ​​forretningsprocesser og domænedata. Den foretrukne mulighed ville være at bruge mønsteret Transaktionel udbakke, men det vil kræve en ekstra tabel i databasen og et relæ. I JEE applikationer kan dette forenkles ved at bruge en lokal JTA manager, men forbindelsen til den valgte mægler skal kunne fungere i mode XA;
  • behandlere af indgående meddelelser og begivenheder skal også arbejde med transaktionen med at ændre tilstanden af ​​forretningsprocessen: hvis en sådan transaktion rulles tilbage, skal modtagelsen af ​​meddelelsen også annulleres;
  • beskeder, der ikke kunne leveres på grund af fejl, skal opbevares i et separat lager D.L.Q. (Døde bogstaver Kø). For at gøre dette har vi oprettet en separat platformsmikrotjeneste, der gemmer sådanne meddelelser i dets lager, indekserer dem efter attributter (til hurtig gruppering og søgning) og udsætter API'et til visning, genafsendelse til destinationsadressen og sletning af meddelelser. Systemadministratorer kan arbejde med denne service via deres webgrænseflade;
  • i mæglerindstillingerne skal du justere antallet af leveringsforsøg og forsinkelser mellem leveringer for at reducere sandsynligheden for, at meddelelser kommer ind i DLQ'en (det er næsten umuligt at beregne de optimale parametre, men du kan handle empirisk og justere dem under operation);
  • DLQ-butikken bør løbende overvåges, og overvågningssystemet bør underrette systemadministratorer, så de kan reagere så hurtigt som muligt, når der opstår ikke-leverede beskeder. Dette vil reducere "skadezonen" af en fejl eller forretningslogikfejl;
  • integrationsbussen skal være ufølsom over for det midlertidige fravær af applikationer: Emneabonnementer skal være holdbare, og applikationens domænenavn skal være unikt, så en anden ikke forsøger at behandle dens besked fra køen under applikationens fravær.

Sikring af trådsikkerhed af forretningslogik

Den samme forekomst af en forretningsproces kan modtage flere beskeder og begivenheder på én gang, hvis behandling vil starte parallelt. Samtidig skal alt for en applikationsudvikler være enkelt og trådsikkert.

Processens forretningslogik behandler hver ekstern hændelse, der påvirker denne forretningsproces individuelt. Disse begivenheder kan være:

  • lancering af en forretningsprocesinstans;
  • en brugerhandling relateret til en aktivitet i en forretningsproces;
  • modtagelse af en besked eller et signal, som en forretningsprocesinstans abonnerer på;
  • udløb af timeren indstillet af forretningsprocesinstansen;
  • kontrolhandling via API (f.eks. procesafbrydelse).

Hver sådan hændelse kan ændre tilstanden af ​​en forretningsprocesinstans: nogle aktiviteter kan slutte og andre starte, værdierne af vedvarende egenskaber kan ændre sig. Lukning af enhver aktivitet kan resultere i aktivering af en eller flere af følgende aktiviteter. Disse kan til gengæld stoppe med at vente på andre begivenheder, eller hvis de ikke har brug for yderligere data, kan de gennemføre i samme transaktion. Inden transaktionen lukkes, gemmes den nye tilstand af forretningsprocessen i databasen, hvor den venter på den næste eksterne begivenhed.

Vedvarende forretningsprocesdata gemt i en relationsdatabase er et meget praktisk behandlingssynkroniseringspunkt, når du bruger SELECT FOR UPDATE. Hvis en transaktion formåede at få status for forretningsprocessen fra databasen til at ændre den, så vil ingen anden transaktion parallelt være i stand til at få den samme tilstand for en anden ændring, og efter afslutningen af ​​den første transaktion er den anden transaktion garanteret at modtage den allerede ændrede tilstand.

Ved at bruge pessimistiske låse på DBMS-siden opfylder vi alle de nødvendige krav ACID, og også bevare muligheden for at skalere applikationen med forretningslogik ved at øge antallet af kørende forekomster.

Men pessimistiske låse truer os med dødvande, hvilket betyder, at VÆLG TIL OPDATERING stadig bør være begrænset til en rimelig timeout i tilfælde af dødvande på nogle alvorlige sager i forretningslogikken.

Et andet problem er synkroniseringen af ​​starten af ​​forretningsprocessen. Selvom der ikke er nogen forretningsprocesinstans, er der heller ingen tilstand i databasen, så den beskrevne metode virker ikke. Hvis du vil sikre det unikke ved en forretningsprocesinstans i et bestemt omfang, så har du brug for en form for synkroniseringsobjekt tilknyttet procesklassen og det tilsvarende omfang. For at løse dette problem bruger vi en anden låsemekanisme, der giver os mulighed for at tage en lås på en vilkårlig ressource specificeret af en nøgle i URI-format gennem en ekstern tjeneste.

I vores eksempler indeholder InitialPlayer-forretningsprocessen en erklæring

uniqueConstraint = UniqueConstraints.singleton

Derfor indeholder loggen beskeder om at tage og slippe låsen på den tilsvarende nøgle. Der er ingen sådanne meddelelser for andre forretningsprocesser: uniqueConstraint er ikke indstillet.

Forretningsprocesproblemer med vedvarende tilstand

Nogle gange hjælper det ikke kun at have en vedvarende tilstand, men det hæmmer også virkelig udviklingen.
Problemer starter, når du skal foretage ændringer i forretningslogikken og/eller forretningsprocesmodellen. Ingen sådan ændring er fundet at være forenelig med den gamle tilstand af forretningsprocesserne. Hvis der er mange "live" forekomster i databasen, kan det at lave inkompatible ændringer forårsage en masse problemer, som vi ofte stødte på, når vi brugte jBPM.

Afhængigt af forandringens dybde kan du handle på to måder:

  1. opret en ny forretningsprocestype for ikke at foretage inkompatible ændringer af den gamle, og brug den i stedet for den gamle, når du starter nye forekomster. Gamle instanser vil fortsætte med at fungere "på den gamle måde";
  2. migrere den vedvarende tilstand af forretningsprocesser ved opdatering af forretningslogik.

Den første måde er enklere, men har sine begrænsninger og ulemper, for eksempel:

  • duplikering af forretningslogik i mange forretningsprocesmodeller, en stigning i mængden af ​​forretningslogik;
  • ofte kræves en øjeblikkelig overgang til en ny forretningslogik (næsten altid med hensyn til integrationsopgaver);
  • udvikleren ved ikke på hvilket tidspunkt det er muligt at slette forældede modeller.

I praksis bruger vi begge tilgange, men har truffet en række beslutninger for at forenkle vores liv:

  • i databasen gemmes den vedvarende tilstand af forretningsprocessen i en letlæselig og let behandlet form: i en JSON-formatstreng. Dette giver dig mulighed for at udføre migreringer både inde i applikationen og udenfor. I ekstreme tilfælde kan du også justere det med håndtag (især nyttigt i udvikling under fejlretning);
  • integrationsforretningslogikken bruger ikke navnene på forretningsprocesser, så det til enhver tid er muligt at erstatte implementeringen af ​​en af ​​de deltagende processer med en ny, med et nyt navn (f.eks. "InitialPlayerV2"). Bindingen sker gennem navnene på beskeder og signaler;
  • procesmodellen har et versionsnummer, som vi øger, hvis vi foretager inkompatible ændringer af denne model, og dette nummer gemmes sammen med procesforekomstens tilstand;
  • processens vedvarende tilstand læses fra basen først til en bekvem objektmodel, som migreringsproceduren kan arbejde med, hvis modellens versionsnummer er ændret;
  • migreringsproceduren er placeret ved siden af ​​forretningslogikken og kaldes "doven" for hver forekomst af forretningsprocessen på tidspunktet for dens gendannelse fra databasen;
  • hvis du har brug for at migrere tilstanden af ​​alle procesforekomster hurtigt og synkront, bruges mere klassiske databasemigreringsløsninger, men du skal arbejde med JSON der.

Har jeg brug for en anden ramme for forretningsprocesser?

Løsningerne beskrevet i artiklen gjorde det muligt for os at forenkle vores liv betydeligt, udvide rækken af ​​problemer, der er løst på applikationsudviklingsniveau, og gøre ideen om at adskille forretningslogik i mikrotjenester mere attraktiv. Hertil er der blevet arbejdet meget, skabt et meget "letvægts"-rammer for forretningsprocesser, samt servicekomponenter til løsning af de identificerede problemer i forbindelse med en lang række anvendte opgaver. Vi har et ønske om at dele disse resultater, for at bringe udviklingen af ​​fælles komponenter i åben adgang under en gratis licens. Dette vil kræve en vis indsats og tid. At forstå efterspørgslen efter sådanne løsninger kan være et yderligere incitament for os. I den foreslåede artikel er der meget lidt opmærksomhed på selve rammens muligheder, men nogle af dem er synlige fra de præsenterede eksempler. Hvis vi alligevel offentliggør vores rammeværk, vil en separat artikel blive viet til det. I mellemtiden vil vi være taknemmelige, hvis du giver en lille feedback ved at besvare spørgsmålet:

Kun registrerede brugere kan deltage i undersøgelsen. Log ind, Vær venlig.

Har jeg brug for en anden ramme for forretningsprocesser?

  • 18,8 %Ja, jeg har ledt efter sådan noget længe.

  • 12,5 %det er interessant at lære mere om din implementering, det kan være nyttigt2

  • 6,2 %vi bruger et af de eksisterende rammer, men vi overvejer at erstatte det1

  • 18,8 %vi bruger en af ​​de eksisterende rammer, alt passer3

  • 18,8 %mestring uden rammer3

  • 25,0 %skriv din egen 4

16 brugere stemte. 7 brugere undlod at stemme.

Kilde: www.habr.com

Tilføj en kommentar