BPM stil integrasjon

BPM stil integrasjon

Hei, Habr!

Vårt firma spesialiserer seg på utvikling av ERP-klasse programvareløsninger, der brorparten er okkupert av transaksjonssystemer med en enorm mengde forretningslogikk og arbeidsflyt a la EDMS. Moderne versjoner av produktene våre er basert på JavaEE-teknologier, men vi eksperimenterer også aktivt med mikrotjenester. Et av de mest problematiske områdene for slike løsninger er integrering av ulike delsystemer knyttet til tilstøtende domener. Integreringsoppgaver har alltid gitt oss en enorm hodepine, uavhengig av arkitektoniske stiler, teknologistabler og rammeverk vi bruker, men den siste tiden har det vært fremgang i å løse slike problemer.

I artikkelen som ble gjort oppmerksom på, vil jeg snakke om erfaringen og arkitekturforskningen til NPO Krista i det utpekte området. Vi vil også vurdere et eksempel på en enkel løsning på et integrasjonsproblem fra en applikasjonsutviklers synspunkt og finne ut hva som skjuler seg bak denne enkelheten.

Ansvarsfraskrivelse

De arkitektoniske og tekniske løsningene beskrevet i artikkelen tilbys av meg basert på personlig erfaring i forbindelse med spesifikke oppgaver. Disse løsningene hevder ikke å være universelle og er kanskje ikke optimale under andre bruksforhold.

Hva har BPM med det å gjøre?

For å svare på dette spørsmålet, må vi fordype oss litt i detaljene for de anvendte problemene til løsningene våre. Hoveddelen av forretningslogikken i vårt typiske transaksjonssystem er datainnføring i databasen gjennom brukergrensesnitt, manuell og automatisert verifisering av disse dataene, sende dem gjennom en arbeidsflyt, publisere dem til et annet system / analytisk database / arkiv, generere rapporter. Dermed er nøkkelfunksjonen til systemet for kundene automatisering av deres interne forretningsprosesser.

For enkelhets skyld bruker vi begrepet "dokument" i kommunikasjon som en abstraksjon av et datasett, forent av en felles nøkkel, som en spesifikk arbeidsflyt kan "vedlegges".
Men hva med integrasjonslogikken? Tross alt genereres integrasjonsoppgaven av systemets arkitektur, som "sages" i deler IKKE på forespørsel fra kunden, men under påvirkning av helt andre faktorer:

  • under påvirkning av Conways lov;
  • som et resultat av gjenbruk av delsystemer som tidligere er utviklet for andre produkter;
  • som bestemt av arkitekten, basert på ikke-funksjonelle krav.

Det er en stor fristelse å skille integrasjonslogikken fra forretningslogikken til hovedarbeidsflyten for ikke å forurense forretningslogikken med integrasjonsartefakter og spare applikasjonsutvikleren fra å måtte fordype seg i særegenhetene til systemets arkitektoniske landskap. Denne tilnærmingen har en rekke fordeler, men praksis viser dens ineffektivitet:

  • å løse integreringsproblemer glir vanligvis ned til de enkleste alternativene i form av synkrone samtaler på grunn av de begrensede utvidelsespunktene i implementeringen av hovedarbeidsflyten (mer om manglene ved synkron integrasjon nedenfor);
  • integrasjonsartefakter trenger fortsatt inn i hovedforretningslogikken når tilbakemelding fra et annet delsystem er nødvendig;
  • applikasjonsutvikleren ignorerer integrasjonen og kan enkelt bryte den ved å endre arbeidsflyten;
  • systemet slutter å være en enkelt helhet fra brukerens synspunkt, "sømmer" mellom delsystemer blir merkbare, overflødige brukeroperasjoner dukker opp som setter i gang overføring av data fra et delsystem til et annet.

En annen tilnærming er å vurdere integrasjonsinteraksjoner som en integrert del av kjernevirksomhetens logikk og arbeidsflyt. For å forhindre at ferdighetskravene til applikasjonsutviklere skyter i været, bør det å skape nye integrasjonsinteraksjoner gjøres enkelt og naturlig, med minimale alternativer for valg av løsning. Dette er vanskeligere enn det ser ut: Verktøyet må være kraftig nok til å gi brukeren den nødvendige variasjonen av muligheter for bruk og samtidig ikke tillate seg å bli skutt i foten. Det er mange spørsmål som en ingeniør må svare på i sammenheng med integrasjonsoppgaver, men som en applikasjonsutvikler ikke bør tenke på i sitt daglige arbeid: transaksjonsgrenser, konsistens, atomitet, sikkerhet, skalering, last- og ressursfordeling, ruting, marshaling, utbredelse og byttekontekster osv. Det er nødvendig å tilby applikasjonsutviklere ganske enkle beslutningsmaler, der svar på alle slike spørsmål allerede er skjult. Disse mønstrene bør være sikre nok: forretningslogikken endres veldig ofte, noe som øker risikoen for å introdusere feil, kostnadene for feil bør holde seg på et ganske lavt nivå.

Men likevel, hva har BPM med det å gjøre? Det er mange alternativer for å implementere arbeidsflyt ...
En annen implementering av forretningsprosesser er faktisk veldig populær i våre løsninger - gjennom den deklarative innstillingen av tilstandsovergangsdiagrammet og koble behandlere med forretningslogikk til overgangene. Samtidig er staten som bestemmer den nåværende posisjonen til "dokumentet" i forretningsprosessen et attributt til selve "dokumentet".

BPM stil integrasjon
Slik ser prosessen ut ved oppstart av prosjektet

Populariteten til en slik implementering skyldes den relative enkelheten og hastigheten til å lage lineære forretningsprosesser. Men etter hvert som programvaresystemer blir mer komplekse, vokser den automatiserte delen av forretningsprosessen og blir mer kompleks. Det er behov for dekomponering, gjenbruk av deler av prosesser, samt gaffelprosesser slik at hver gren utføres parallelt. Under slike forhold blir verktøyet upraktisk, og tilstandsovergangsdiagrammet mister informasjonsinnholdet (integrasjonsinteraksjoner reflekteres ikke i diagrammet i det hele tatt).

BPM stil integrasjon
Slik ser prosessen ut etter flere iterasjoner med avklaring av kravene

Veien ut av denne situasjonen var integrasjonen av motoren jBPM inn i noen produkter med de mest komplekse forretningsprosessene. På kort sikt hadde denne løsningen en viss suksess: det ble mulig å implementere komplekse forretningsprosesser samtidig som man opprettholder et ganske informativt og oppdatert diagram i notasjonen BPMN2.

BPM stil integrasjon
En liten del av en kompleks forretningsprosess

I det lange løp levde ikke løsningen opp til forventningene: Den høye arbeidsintensiteten ved å lage forretningsprosesser gjennom visuelle verktøy tillot ikke å oppnå akseptable produktivitetsindikatorer, og selve verktøyet ble et av de mest mislikte blant utviklere. Det var også klager på den interne strukturen til motoren, noe som førte til utseendet på mange "lapper" og "krykker".

Det viktigste positive aspektet ved å bruke jBPM var realiseringen av fordelene og skadene ved å ha sin egen vedvarende tilstand for en forretningsprosessforekomst. Vi så også muligheten for å bruke en prosesstilnærming for å implementere komplekse integrasjonsprotokoller mellom ulike applikasjoner ved å bruke asynkrone interaksjoner gjennom signaler og meldinger. Tilstedeværelsen av en vedvarende tilstand spiller en avgjørende rolle i dette.

Basert på ovenstående kan vi konkludere: Prosesstilnærmingen i BPM-stilen lar oss løse et bredt spekter av oppgaver for å automatisere stadig mer komplekse forretningsprosesser, harmonisk tilpasse integrasjonsaktiviteter i disse prosessene og beholde muligheten til å visuelt vise den implementerte prosessen i en passende notasjon.

Ulemper med synkrone samtaler som integreringsmønster

Synkron integrasjon refererer til den enkleste blokkeringsanropet. Ett delsystem fungerer som serversiden og eksponerer API med ønsket metode. Et annet delsystem fungerer som en klientside og ringer til rett tid med forventning om et resultat. Avhengig av systemets arkitektur, kan klient- og serversiden være vert enten i samme applikasjon og prosess, eller i forskjellige. I det andre tilfellet må du bruke en viss implementering av RPC og gi rangering av parametrene og resultatet av samtalen.

BPM stil integrasjon

Et slikt integreringsmønster har et ganske stort sett med ulemper, men det er veldig mye brukt i praksis på grunn av sin enkelhet. Hastigheten på implementeringen er fengslende og gjør at du bruker den igjen og igjen under betingelser med "brennende" tidsfrister, og skriver løsningen inn i teknisk gjeld. Men det hender også at uerfarne utviklere bruker det ubevisst, rett og slett ikke innser de negative konsekvensene.

I tillegg til den mest åpenbare økningen i tilkoblingen til delsystemer, er det mindre åpenbare problemer med å "sløse" og "strekke" transaksjoner. Faktisk, hvis forretningslogikken gjør noen endringer, er transaksjoner uunnværlige, og transaksjoner låser i sin tur visse applikasjonsressurser som påvirkes av disse endringene. Det vil si at inntil et delsystem venter på svar fra et annet, vil det ikke være i stand til å fullføre transaksjonen og frigjøre låser. Dette øker risikoen for en rekke effekter betydelig:

  • systemrespons går tapt, brukere venter lenge på svar på forespørsler;
  • serveren slutter vanligvis å svare på brukerforespørsler på grunn av en overfylt trådpool: de fleste trådene "står" på låsen til ressursen som er okkupert av transaksjonen;
  • låsninger begynner å dukke opp: sannsynligheten for at de skjer, avhenger sterkt av transaksjonens varighet, mengden forretningslogikk og låser involvert i transaksjonen;
  • utløpsfeil for transaksjonstidsavbrudd vises;
  • serveren "faller" på OutOfMemory hvis oppgaven krever behandling og endring av store datamengder, og tilstedeværelsen av synkrone integrasjoner gjør det svært vanskelig å dele opp behandlingen i "lettere" transaksjoner.

Fra et arkitektonisk synspunkt fører bruken av blokkering av samtaler under integrasjon til tap av kvalitetskontroll av individuelle delsystemer: det er umulig å sikre kvalitetsmålene til ett delsystem isolert fra kvalitetsmålene til et annet delsystem. Hvis delsystemer utvikles av ulike team, er dette et stort problem.

Ting blir enda mer interessant hvis delsystemene som integreres er i forskjellige applikasjoner og synkrone endringer må gjøres på begge sider. Hvordan gjøre disse endringene transaksjonelle?

Hvis endringer gjøres i separate transaksjoner, må robust unntakshåndtering og kompensasjon gis, og dette eliminerer helt hovedfordelen med synkrone integrasjoner - enkelhet.

Distribuerte transaksjoner kommer også til syne, men vi bruker dem ikke i våre løsninger: det er vanskelig å sikre pålitelighet.

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

Med den økende populariteten til mikrotjenester er det en økende etterspørsel etter Saga mønster.

Dette mønsteret løser perfekt de ovennevnte problemene med lange transaksjoner, og utvider også mulighetene for å administrere systemets tilstand fra siden av forretningslogikk: kompensasjon etter en mislykket transaksjon kan ikke rulle tilbake systemet til sin opprinnelige tilstand, men gir et alternativ databehandlingsrute. Det lar deg også ikke gjenta vellykket fullførte databehandlingstrinn når du prøver å bringe prosessen til en "god" slutt.

Interessant nok, i monolitiske systemer er dette mønsteret også relevant når det gjelder integrasjon av løst koplede delsystemer, og det er negative effekter forårsaket av lange transaksjoner og tilsvarende ressurslåser.

Når det gjelder våre forretningsprosesser i BPM-stil, viser det seg å være veldig enkelt å implementere Sagas: individuelle trinn i Sagaene kan settes som aktiviteter innenfor forretningsprosessen, og den vedvarende tilstanden til forretningsprosessen avgjør bl.a. , sagaenes indre tilstand. Det vil si at vi ikke trenger noen ekstra koordineringsmekanisme. Alt du trenger er en meldingsmegler med støtte for «minst én gang»-garantier som transport.

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

  • forretningslogikken blir mer kompleks: du må regne ut kompensasjon;
  • det vil være nødvendig å forlate full konsistens, noe som kan være spesielt følsomt for monolitiske systemer;
  • arkitekturen blir litt mer komplisert, det er et ekstra behov for en meldingsmegler;
  • ytterligere overvåkings- og administrasjonsverktøy vil være nødvendig (selv om dette generelt sett er bra: kvaliteten på systemtjenesten vil øke).

For monolittiske systemer er ikke begrunnelsen for å bruke "Sags" så åpenbar. For mikrotjenester og andre SOA, hvor det mest sannsynlig allerede er en megler, og full konsistens ble ofret ved starten av prosjektet, kan fordelene ved å bruke dette mønsteret betydelig oppveie ulempene, spesielt hvis det er en praktisk API på forretningslogikknivå.

Innkapsle forretningslogikk i mikrotjenester

Da vi begynte å eksperimentere med mikrotjenester, dukket det opp et rimelig spørsmål: hvor skal man sette domeneforretningslogikken i forhold til tjenesten som gir domenedatapersistens?

Når man ser på arkitekturen til ulike BPMS, kan det virke rimelig å skille forretningslogikk fra persistens: lag et lag med plattform- og domeneuavhengige mikrotjenester som danner miljøet og beholderen for å utføre domeneforretningslogikk, og ordne domenedatapersistens som en separat lag med veldig enkle og lette mikrotjenester. Forretningsprosesser i dette tilfellet orkestrerer tjenestene til utholdenhetslaget.

BPM stil integrasjon

Denne tilnærmingen har et veldig stort pluss: du kan øke funksjonaliteten til plattformen så mye du vil, og bare det tilsvarende laget med plattformmikrotjenester vil "bli fet" av dette. Forretningsprosesser fra ethvert domene får umiddelbart muligheten til å bruke den nye funksjonaliteten til plattformen så snart den er oppdatert.

En mer detaljert studie avdekket betydelige mangler ved denne tilnærmingen:

  • en plattformtjeneste som utfører forretningslogikken til mange domener samtidig, medfører store risikoer som et enkelt feilpunkt. Hyppige endringer i forretningslogikk øker risikoen for feil som fører til systemomfattende feil;
  • ytelsesproblemer: forretningslogikk arbeider med dataene sine gjennom et smalt og tregt grensesnitt:
    • dataene vil igjen samles og pumpes gjennom nettverksstakken;
    • domenetjenesten vil ofte returnere mer data enn forretningslogikken krever for behandling, på grunn av utilstrekkelige søkeparameteriseringsmuligheter på nivået til tjenestens eksterne API;
    • flere uavhengige deler av forretningslogikk kan gjentatte ganger be om de samme dataene for behandling (du kan redusere dette problemet ved å legge til øktbønner som cacher data, men dette kompliserer arkitekturen ytterligere og skaper problemer med datafriskhet og cache-uvalidering);
  • transaksjonsproblemer:
    • forretningsprosesser med vedvarende tilstand lagret av plattformtjenesten er inkonsistente med domenedata, og det er ingen enkle måter å løse dette problemet på;
    • flytte låsen av domenedata ut av transaksjonen: hvis domeneforretningslogikken trenger å gjøre endringer, etter først å ha kontrollert riktigheten av de faktiske dataene, er det nødvendig å utelukke muligheten for en konkurransedyktig endring i de behandlede dataene. Ekstern blokkering av data kan bidra til å løse problemet, men en slik løsning medfører ytterligere risiko og reduserer systemets generelle pålitelighet;
  • ytterligere komplikasjoner ved oppdatering: i noen tilfeller må du oppdatere utholdenhetstjenesten og forretningslogikken synkront eller i streng rekkefølge.

Til slutt måtte jeg gå tilbake til det grunnleggende: kapsle inn domenedata og domeneforretningslogikk i én mikrotjeneste. Denne tilnærmingen forenkler oppfatningen av mikrotjenesten som en integrert komponent i systemet og gir ikke opphav til problemene ovenfor. Dette er heller ikke gratis:

  • API-standardisering er nødvendig for interaksjon med forretningslogikk (spesielt for å tilby brukeraktiviteter som en del av forretningsprosesser) og API-plattformtjenester; mer nøye oppmerksomhet på API-endringer, forover- og bakoverkompatibilitet er nødvendig;
  • det er nødvendig å legge til flere kjøretidsbiblioteker for å sikre at forretningslogikken fungerer som en del av hver slik mikrotjeneste, og dette gir opphav til nye krav til slike biblioteker: letthet og et minimum av transitive avhengigheter;
  • forretningslogikkutviklere må holde styr på bibliotekversjoner: hvis en mikrotjeneste ikke har blitt ferdigstilt på lenge, vil den mest sannsynlig inneholde en utdatert versjon av bibliotekene. Dette kan være en uventet hindring for å legge til en ny funksjon og kan kreve at den gamle forretningslogikken til en slik tjeneste migreres til nye versjoner av bibliotekene hvis det var inkompatible endringer mellom versjoner.

BPM stil integrasjon

Et lag med plattformtjenester er også tilstede i en slik arkitektur, men dette laget danner ikke lenger en beholder for utføring av domeneforretningslogikk, men bare dets miljø, og gir ekstra "plattform"-funksjoner. Et slikt lag er ikke bare nødvendig for å opprettholde lettheten til domenemikrotjenester, men også for å sentralisere administrasjonen.

For eksempel genererer brukeraktiviteter i forretningsprosesser oppgaver. Men når du arbeider med oppgaver, må brukeren se oppgaver fra alle domener i den generelle listen, noe som betyr at det må være en passende oppgaveregistreringsplattformtjeneste, renset for domeneforretningslogikk. Å beholde innkapslingen av forretningslogikk i denne sammenhengen er ganske problematisk, og dette er nok et kompromiss med denne arkitekturen.

Integrasjon av forretningsprosesser gjennom øynene til en applikasjonsutvikler

Som allerede nevnt ovenfor, må applikasjonsutvikleren abstraheres fra de tekniske og tekniske funksjonene ved implementeringen av samspillet mellom flere applikasjoner for å kunne regne med god utviklingsproduktivitet.

La oss prøve å løse et ganske vanskelig integreringsproblem, spesielt oppfunnet for artikkelen. Dette vil være en "spill"-oppgave som involverer tre applikasjoner, der hver av dem definerer et domenenavn: "app1", "app2", "app3".

Inne i hver applikasjon lanseres forretningsprosesser som begynner å "spille ball" gjennom integrasjonsbussen. Meldinger kalt "Ball" vil fungere som ballen.

Regler for spillet:

  • den første spilleren er initiativtakeren. Han inviterer andre spillere til spillet, starter spillet og kan avslutte det når som helst;
  • andre spillere erklærer sin deltakelse i spillet, "bli kjent" med hverandre og den første spilleren;
  • etter å ha mottatt ballen, velger spilleren en annen deltakende spiller og sender ballen til ham. Totalt antall passeringer telles;
  • hver spiller har "energi", som avtar med hver pasning av ballen fra den spilleren. Når energien tar slutt, blir spilleren eliminert fra spillet, og kunngjør at de trekker seg;
  • hvis spilleren blir alene, kunngjør han umiddelbart sin avgang;
  • når alle spillere er eliminert, erklærer den første spilleren slutten av spillet. Hvis han forlot spillet tidligere, gjenstår det å følge spillet for å fullføre det.

For å løse dette problemet vil jeg bruke vår DSL for forretningsprosesser, som lar deg beskrive logikken i Kotlin kompakt, med et minimum av en kjeleplate.

I app1-applikasjonen vil forretningsprosessen til den første spilleren (han er også initiativtakeren 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}")
}

I tillegg til å utføre forretningslogikk, kan koden ovenfor produsere en objektmodell av en forretningsprosess som kan visualiseres som et diagram. Vi har ikke implementert visualizeren ennå, så vi måtte bruke litt tid på å tegne (her forenklet jeg litt BPMN-notasjonen angående bruk av porter for å forbedre konsistensen av diagrammet med koden ovenfor):

BPM stil integrasjon

app2 vil inkludere en annen spillers forretningsprosess:

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 integrasjon

I app3-applikasjonen vil vi gjøre spilleren til en litt annen oppførsel: i stedet for å velge neste spiller tilfeldig, 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 skiller ikke spillerens oppførsel seg fra den forrige, så diagrammet endres ikke.

Nå trenger vi en test for å kjøre alt. Jeg vil bare gi koden til selve testen, for ikke å rote til artikkelen med en kjeleplate (faktisk brukte jeg testmiljøet som ble opprettet tidligere for å teste integrasjonen av andre forretningsprosesser):

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

Kjør testen, se på loggen:

konsollutgang

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

Det kan trekkes flere viktige konklusjoner fra alt dette:

  • hvis de nødvendige verktøyene er tilgjengelige, kan applikasjonsutviklere skape integrasjonsinteraksjoner mellom applikasjoner uten å bryte bort fra forretningslogikken;
  • kompleksiteten (kompleksiteten) til en integrasjonsoppgave som krever ingeniørkompetanse kan skjules inne i rammeverket dersom det i utgangspunktet er nedfelt i rammeverkets arkitektur. Vanskeligheten til oppgaven (vanskeligheten) kan ikke skjules, så løsningen på en vanskelig oppgave i koden vil se deretter ut;
  • når man utvikler integrasjonslogikk, er det nødvendig å ta hensyn til til slutt konsistens og mangelen på linearisering av tilstandsendringen til alle integreringsdeltakere. Dette tvinger oss til å komplisere logikken for å gjøre den ufølsom for rekkefølgen eksterne hendelser inntreffer. I vårt eksempel blir spilleren tvunget til å delta i spillet etter at han har kunngjort at han går ut av spillet: andre spillere vil fortsette å sende ballen til ham til informasjonen om hans utgang når og blir behandlet av alle deltakerne. Denne logikken følger ikke av spillereglene og er en kompromissløsning innenfor rammen av den valgte arkitekturen.

La oss deretter snakke om de ulike finessene i løsningen vår, kompromisser og andre punkter.

Alle meldinger i én kø

Alle integrerte applikasjoner fungerer med én integrasjonsbuss, som er representert som en ekstern megler, én BPMQueue for meldinger og én BPMTopic-emne for signaler (hendelser). Å sende alle meldinger gjennom én enkelt kø er i seg selv et kompromiss. På forretningslogikknivå kan du nå introdusere så mange nye typer meldinger du vil uten å gjøre endringer i systemstrukturen. Dette er en betydelig forenkling, men den medfører visse risikoer som, i sammenheng med våre typiske oppgaver, for oss ikke virket så betydelige.

BPM stil integrasjon

Det er imidlertid en subtilitet her: hver applikasjon filtrerer "sin" meldinger fra køen ved inngangen, etter navnet på domenet. Domenet kan også spesifiseres i signalene hvis du trenger å begrense "omfanget" av signalet til en enkelt applikasjon. Dette bør øke båndbredden til bussen, men forretningslogikken må nå operere med domenenavn: obligatorisk for adressering av meldinger, ønskelig for signaler.

Sikre påliteligheten til integrasjonsbussen

Pålitelighet består av flere ting:

  • Den valgte meldingsmegleren er en kritisk komponent i arkitekturen og et enkelt feilpunkt: den må være tilstrekkelig feiltolerant. Du bør kun bruke tidtestede implementeringer med god støtte og et stort fellesskap;
  • det er nødvendig å sikre høy tilgjengelighet av meldingsmegleren, for hvilken den må være fysisk atskilt fra de integrerte applikasjonene (høy tilgjengelighet av applikasjoner med anvendt forretningslogikk er mye vanskeligere og dyrere å tilby);
  • megleren plikter å stille «minst én gang» leveringsgarantier. Dette er et obligatorisk krav for pålitelig drift av integrasjonsbussen. Det er ikke behov for "nøyaktig én gang" nivågarantier: forretningsprosesser er vanligvis ikke sensitive for gjentatt mottak av meldinger eller hendelser, og i spesielle oppgaver der dette er viktig, er det lettere å legge til ytterligere kontroller til forretningslogikken enn å stadig bruke heller "dyre" " garantier;
  • sending av meldinger og signaler må være involvert i en felles transaksjon med en endring i tilstanden til forretningsprosesser og domenedata. Det foretrukne alternativet vil være å bruke mønsteret Transaksjonsutboks, men det vil kreve en ekstra tabell i databasen og et relé. I JEE-applikasjoner kan dette forenkles ved å bruke en lokal JTA-manager, men koblingen til den valgte megleren må kunne fungere i modus XA;
  • behandlere av innkommende meldinger og hendelser må også jobbe med transaksjonen for å endre tilstanden til forretningsprosessen: hvis en slik transaksjon rulles tilbake, må også mottaket av meldingen kanselleres;
  • meldinger som ikke kunne leveres på grunn av feil bør lagres i en egen butikk D.L.Q. (Død bokstavkø). For å gjøre dette opprettet vi en egen plattformmikrotjeneste som lagrer slike meldinger i lagringen, indekserer dem etter attributter (for rask gruppering og søking), og avslører API for visning, resending til destinasjonsadressen og sletting av meldinger. Systemadministratorer kan arbeide med denne tjenesten gjennom deres webgrensesnitt;
  • i meglerinnstillingene må du justere antall leveringsforsøk og forsinkelser mellom leveranser for å redusere sannsynligheten for at meldinger kommer inn i DLQ (det er nesten umulig å beregne de optimale parameterne, men du kan handle empirisk og justere dem under operasjon);
  • DLQ-butikken bør overvåkes kontinuerlig, og overvåkingssystemet bør varsle systemadministratorer slik at de kan svare så raskt som mulig når meldinger som ikke er levert. Dette vil redusere "skadesonen" for en feil eller forretningslogikkfeil;
  • integrasjonsbussen må være ufølsom for det midlertidige fraværet av applikasjoner: emneabonnementer må være holdbare, og domenenavnet til applikasjonen må være unikt slik at noen andre ikke prøver å behandle meldingen fra køen under fravær av applikasjonen.

Sikre trådsikkerhet for forretningslogikk

Den samme forekomsten av en forretningsprosess kan motta flere meldinger og hendelser samtidig, og behandlingen vil starte parallelt. Samtidig, for en applikasjonsutvikler, skal alt være enkelt og trådsikkert.

Prosessforretningslogikken behandler hver ekstern hendelse som påvirker denne forretningsprosessen individuelt. Disse hendelsene kan være:

  • lansering av en forretningsprosessforekomst;
  • en brukerhandling relatert til en aktivitet i en forretningsprosess;
  • mottak av en melding eller signal som en forretningsprosessforekomst abonnerer på;
  • utløp av tidtakeren satt av forretningsprosessforekomsten;
  • kontrollhandling via API (f.eks. prosessavbrudd).

Hver slik hendelse kan endre tilstanden til en forretningsprosessforekomst: noen aktiviteter kan avsluttes og andre starte, verdiene til vedvarende egenskaper kan endres. Å stenge en aktivitet kan føre til aktivering av en eller flere av følgende aktiviteter. De kan på sin side slutte å vente på andre hendelser, eller hvis de ikke trenger ytterligere data, kan de fullføre i samme transaksjon. Før transaksjonen avsluttes, lagres den nye tilstanden til forretningsprosessen i databasen, hvor den vil vente på neste eksterne hendelse.

Vedvarende forretningsprosessdata lagret i en relasjonsdatabase er et veldig praktisk behandlingssynkroniseringspunkt når du bruker SELECT FOR UPDATE. Hvis en transaksjon klarte å få statusen til forretningsprosessen fra basen for å endre den, vil ingen annen transaksjon parallelt kunne få samme tilstand for en annen endring, og etter fullføringen av den første transaksjonen er den andre transaksjonen garantert å motta den allerede endrede tilstanden.

Ved å bruke pessimistiske låser på DBMS-siden oppfyller vi alle nødvendige krav ACID, og også beholde muligheten til å skalere applikasjonen med forretningslogikk ved å øke antallet kjørende forekomster.

Imidlertid truer pessimistiske låser oss med vranglåser, noe som betyr at SELECT FOR UPDATE fortsatt bør begrenses til en rimelig tidsavbrudd i tilfelle vranglås på noen alvorlige tilfeller i forretningslogikk.

Et annet problem er synkroniseringen av starten av forretningsprosessen. Selv om det ikke er noen forretningsprosessforekomst, er det heller ingen tilstand i databasen, så den beskrevne metoden vil ikke fungere. Hvis du vil sikre unikheten til en forretningsprosessforekomst i et bestemt omfang, trenger du et slags synkroniseringsobjekt knyttet til prosessklassen og det tilsvarende omfanget. For å løse dette problemet bruker vi en annen låsemekanisme som lar oss ta en lås på en vilkårlig ressurs spesifisert av en nøkkel i URI-format gjennom en ekstern tjeneste.

I våre eksempler inneholder InitialPlayer-forretningsprosessen en erklæring

uniqueConstraint = UniqueConstraints.singleton

Derfor inneholder loggen meldinger om å ta og frigjøre låsen til den tilsvarende nøkkelen. Det er ingen slike meldinger for andre forretningsprosesser: uniqueConstraint er ikke satt.

Forretningsprosessproblemer med vedvarende tilstand

Noen ganger hjelper det ikke bare å ha en vedvarende tilstand, men hindrer også utviklingen.
Problemer starter når du trenger å gjøre endringer i forretningslogikken og/eller forretningsprosessmodellen. Ingen slik endring er funnet å være forenlig med den gamle tilstanden til forretningsprosessene. Hvis det er mange "live" forekomster i databasen, kan det å gjøre inkompatible endringer føre til mye trøbbel, som vi ofte møtte ved bruk av jBPM.

Avhengig av dybden av endring, kan du handle på to måter:

  1. opprette en ny forretningsprosesstype for ikke å gjøre inkompatible endringer i den gamle, og bruk den i stedet for den gamle når du starter nye forekomster. Gamle instanser vil fortsette å fungere "på den gamle måten";
  2. migrer den vedvarende tilstanden til forretningsprosesser ved oppdatering av forretningslogikk.

Den første måten er enklere, men har sine begrensninger og ulemper, for eksempel:

  • duplisering av forretningslogikk i mange forretningsprosessmodeller, en økning i volumet av forretningslogikk;
  • ofte kreves en umiddelbar overgang til en ny forretningslogikk (nesten alltid når det gjelder integrasjonsoppgaver);
  • utvikleren vet ikke på hvilket tidspunkt det er mulig å slette utdaterte modeller.

I praksis bruker vi begge tilnærmingene, men har tatt en rekke beslutninger for å forenkle livene våre:

  • i databasen lagres den vedvarende tilstanden til forretningsprosessen i en lett lesbar og lett behandlet form: i en JSON-formatstreng. Dette lar deg utføre migreringer både inne i applikasjonen og utenfor. I ekstreme tilfeller kan du også justere den med håndtak (spesielt nyttig i utvikling under feilsøking);
  • integrasjonsforretningslogikken bruker ikke forretningsprosessnavn, slik at det til enhver tid er mulig å erstatte implementeringen av en av de deltakende prosessene med en ny, med et nytt navn (for eksempel "InitialPlayerV2"). Bindingen skjer gjennom navn på meldinger og signaler;
  • prosessmodellen har et versjonsnummer, som vi øker hvis vi gjør inkompatible endringer i denne modellen, og dette nummeret lagres sammen med tilstanden til prosessforekomsten;
  • den vedvarende tilstanden til prosessen leses fra basen først til en praktisk objektmodell som migreringsprosedyren kan fungere med hvis versjonsnummeret til modellen har endret seg;
  • migreringsprosedyren plasseres ved siden av forretningslogikken og kalles "lat" for hver forekomst av forretningsprosessen på tidspunktet for gjenoppretting fra databasen;
  • hvis du trenger å migrere tilstanden til alle prosessinstanser raskt og synkront, brukes mer klassiske databasemigreringsløsninger, men du må jobbe med JSON der.

Trenger jeg et annet rammeverk for forretningsprosesser?

Løsningene beskrevet i artikkelen tillot oss å forenkle livene våre betydelig, utvide utvalget av problemer som er løst på applikasjonsutviklingsnivå, og gjøre ideen om å skille forretningslogikk i mikrotjenester mer attraktiv. For dette har det blitt gjort mye arbeid, et veldig "lett" rammeverk for forretningsprosesser er laget, samt tjenestekomponenter for å løse de identifiserte problemene i sammenheng med et bredt spekter av anvendte oppgaver. Vi har et ønske om å dele disse resultatene, for å bringe utviklingen av felleskomponenter inn i åpen tilgang under en fri lisens. Dette vil kreve litt innsats og tid. Å forstå etterspørselen etter slike løsninger kan være et ekstra insentiv for oss. I den foreslåtte artikkelen er det veldig lite oppmerksomhet på egenskapene til selve rammeverket, men noen av dem er synlige fra eksemplene som presenteres. Hvis vi likevel publiserer rammeverket vårt, vil en egen artikkel vies til det. I mellomtiden vil vi være takknemlige om du gir en liten tilbakemelding ved å svare på spørsmålet:

Kun registrerte brukere kan delta i undersøkelsen. Logg inn, vær så snill.

Trenger jeg et annet rammeverk for forretningsprosesser?

  • 18,8%Ja, jeg har lett etter noe slikt lenge.

  • 12,5%det er interessant å lære mer om implementeringen din, det kan være nyttig2

  • 6,2%vi bruker et av de eksisterende rammeverkene, men vi vurderer å erstatte det1

  • 18,8%vi bruker et av de eksisterende rammeverkene, alt passer3

  • 18,8%mestring uten rammer3

  • 25,0%skriv din egen4

16 brukere stemte. 7 brukere avsto.

Kilde: www.habr.com

Legg til en kommentar