
Hej, Habr!
Vores virksomhed specialiserer sig i udvikling af ERP-løsninger, hvoraf en stor del består af transaktionssystemer med omfattende forretningslogik og dokumenthåndtering som a la SED. Moderne versioner af vores produkter er baseret på JavaEE-teknologier, men vi eksperimenterer også aktivt med mikrotjenester. Et af de mest problematiske områder for sådanne løsninger er integrationen af forskellige undersystemer, der tilhører relaterede domæner. Integrationsopgaver har altid givet os store hovedpiner, uanset hvilke arkitektoniske stilarter, teknologistakke og frameworks vi anvender, men for nylig har der været fremskridt i løsningen af sådanne opgaver.
I den artikel, jeg præsenterer for dig, vil jeg fortælle om erfaringerne og de arkitektoniske undersøgelser, som NPO "Krista" har i det angivne område. Vi vil også se på et eksempel på en simpel løsning af en integrationsopgave fra en anvendt udviklers perspektiv og finde ud af, hvad der gemmer sig bag denne enkelhed.
Fraskrivelse
De arkitektoniske og tekniske løsninger, der beskrives i artiklen, tilbydes af mig baseret på personlig erfaring i konteksten af konkrete opgaver. Disse løsninger skal ikke anses for universelle og kan vise sig at være suboptimale under andre anvendelsesforhold.
Hvad har BPM at gøre med det?
For at besvare dette spørgsmål er det vigtigt at dykke lidt ned i specifikken af anvendte opgaver for vores løsninger. Hovedparten af forretningslogikken i vores typiske transaktionssystem er dataindtastning i databasen via brugergrænseflader, manuel og automatiseret kontrol af disse data, behandling gennem et workflow, publicering til et andet system / analytisk database / arkiv, samt rapportgenerering. Dermed er den nøglen funktion i systemet for kunderne automatiseringen af deres interne forretningsprocesser.
For nemheds skyld bruger vi i kommunikationen termen "dokument" som en vis abstraktion af en datamængde, der er samlet under en fælles nøgle, til hvilken man kan "binde" et bestemt workflow.
Men hvordan skal vi forholde os til integreringslogikken? For integrationsopgaven opstår som følge af systemets arkitektur, som ikke er "skårede" i dele efter kundens krav, men under indflydelse af helt andre faktorer:
- under indflydelse af Conway's lov;
- som følge af genanvendelse af undersystemer, der tidligere er udviklet til andre produkter;
- ifølge arkitektens beslutning, baseret på ikke-funktionelle krav.
Der er en stor fristelse til at adskille integrationslogik fra forretningslogik i hovedworkflowet for ikke at forurene forretningslogikken med integrationsartefakter og fritage applikationsudvikleren for behovet for at forstå detaljer ved systemets arkitektoniske landskab. Denne tilgang har flere fordele, men praksis viser dens ineffektivitet:
- løsningen på integrationsopgaver glider ofte hen imod de simpleste muligheder i form af synkrone opkald på grund af begrænsede udvidelsespunkter i implementeringen af hovedworkflowet (om ulemperne ved synkron integration – se nedenfor);
- integrationsartefakter trænger stadig ind i den primære forretningslogik, når der er behov for feedback fra et andet undersystem;
- applikationsudvikleren ignorerer integrationen og kan nemt ødelægge den ved at ændre workflowet;
- systemet ophører med at være en sammenhængende helhed set fra brugerens perspektiv, "samlingerne" mellem undersystemerne bliver tydelige, og der opstår redundante brugerhandlinger, der initierer overførsel af data fra et undersystem til et andet.
En anden tilgang er at betragte integrationsinteraktioner som en uundgåelig del af den grundlæggende forretningslogik og workflow. For at kravene til applikationsudvikleres kvalifikationer ikke skal stige til himmels, skal oprettelse af nye integrationsinteraktioner udføres let og ubesværet, med minimale muligheder for valg af løsning. Det er sværere at gøre end som så: værktøjet skal være kraftigt nok til at give brugeren det nødvendige antal anvendelsesmuligheder og samtidig ikke tillade, at man "skyder sig selv i foden". Der er mange spørgsmål, som ingeniøren skal besvare i konteksten af integrationsopgaver, men som applikationsudvikleren ikke bør tænke på i sit daglige arbejde: grænser for transaktioner, konsistens, atomaritet, sikkerhed, skalering, belastningsfordeling og ressourcer, routing, marshalling, distribution og skift af kontekster osv. Der skal tilbydes applikationsudviklere tilstrækkeligt enkle løsningsskabeloner, hvorpå svarene på alle sådanne spørgsmål allerede er indlejret. Disse skabeloner skal være tilstrækkeligt sikre: forretningslogikken ændrer sig meget ofte, hvilket øger risikoen for fejl, og prisen for fejl skal forblive på et rimeligt lavt niveau.
Men hvad har BPM med det at gøre? Der er jo mange måder at implementere workflow på…
Virkelig, i vores løsninger er en anden implementering af forretningsprocesser meget populær – gennem deklarativt at definere tilstandsovergangsdiagrammer og tilslutte behandlere med forretningslogik ved overgange. Herudover er tilstanden, der definerer den nuværende position af "dokumentet" i forretningsprocessen, en attribut af selve "dokumentet".

Sådan ser processen ud ved projektets start
Populariteten af denne implementering skyldes den relative enkelhed og hastighed ved oprettelsen af lineære forretningsprocesser. Men efterhånden som software-systemerne bliver mere komplekse, vokser den automatiserede del af forretningsprocessen, og kompleksiteten øges. Der opstår et behov for dekomponering, genbrug af dele af processer samt forgrening af processer, så hver gren kan udføres parallelt. Under sådanne betingelser bliver værktøjet ubehageligt, og tilstandsdiagrammet mister sin informativitet (integrationsinteraktioner afspejles slet ikke i diagrammet).

Sådan ser processen ud efter flere iterationer med afklaring af krav.
Udfordringen blev løst ved at integrere motoren i nogle produkter med de mest komplekse forretningsprocesser. På kort sigt havde denne løsning en vis succes: der blev mulighed for at implementere komplekse forretningsprocesser, samtidig med at der blev bevaret et tilstrækkeligt informativt og aktuelt diagram i notationen .

En lille del af den komplekse forretningsproces.
På lang sigt levede løsningen ikke op til forventningerne: den høje arbejdskraft ved oprettelse af forretningsprocesser gennem visuelle værktøjer gjorde det umuligt at opnå acceptable produktivitetsniveauer, og selve værktøjet blev et af de mest upopulære blandt udviklerne. Der var også klager over motorens interne struktur, hvilket førte til oprettelsen af mange 'lappeløsninger' og 'støtteordninger'.
Den vigtigste positive aspekt ved anvendelsen af jBPM var erkendelsen af nytten og skaden ved at have en egen persistent tilstand for instansen af forretningsprocessen. Vi så også mulighederne for at anvende en procesorienteret tilgang til at implementere komplekse integrationsprotokoller mellem forskellige applikationer med anvendelse af asynkrone interaktioner gennem signaler og beskeder. Eksistensen af en persistent tilstand spiller en afgørende rolle i dette.
Baseret på ovenstående kan der drages en konklusion: den procesorienterede tilgang i stil med BPM gør det muligt for os at løse et bredt udvalg af opgaver ved automatisering af konstant mere komplekse forretningsprocesser, harmonisk integrere integrationsaktiviteter i disse processer og bevare muligheden for visuel repræsentation af den implementerede proces i en passende notation.
Ulemper ved synkrone kald som integrationsmønster
Med synkron integration forstås det enkleste blokkerende kald. Ét subsystem fungerer som serversiden og præsenterer et API med den nødvendige metode. Et andet subsystem fungerer som klientsiden og udfører kaldet på det rette tidspunkt med forventning om resultatet. Afhængigt af systemarkitekturen kan klient- og serversiden placeres enten i én applikation og proces eller i forskellige. I det andet tilfælde kræves en eller anden implementering af RPC og sikring af marshalling af parametre og resultatet af kaldet.

Dette integrationsmønster har et betydeligt antal ulemper, men det er meget udbredt i praksis på grund af sin enkelhed. Implementeringshastigheden er tiltalende og får det til at blive anvendt igen og igen under »pressede« deadlines, hvilket fører til teknisk gæld. Men det er også sådan, at uerfarne udviklere bruger det ubevidst, blot uden at indse de negative konsekvenser.
Udover de mest åbenlyse forbindelser mellem subsystems, er der også mindre åbenlyse problemer med »udtrækning« og »udvidelse« af transaktioner. Faktisk, hvis forretningslogikken foretager ændringer, kan man ikke undgå transaktioner, og transaktioner blokerer på sin side visse ressourcer i applikationen, der berøres af disse ændringer. Det vil sige, at så længe ét subsystem ikke modtager svar fra et andet, kan det ikke afslutte transaktionen og frigive blokeringerne. Dette øger væsentligt risikoen for opståen af forskellige effekter:
- systemets responsivitet går tabt, og brugerne venter længe på svar på deres forespørgsler;
- serveren stopper helt med at svare på brugerforespørgsler på grund af et overfyldt trådpool: de fleste tråde er 'fastlåst' på blokeringen af ressourcen, der er optaget af transaktionen;
- deadlocks begynder at opstå: sandsynligheden for deres opståen afhænger kraftigt af varigheden af transaktionerne, mængden af forretningslogik involveret i transaktionen og blokeringerne;
- timeout-fejl på transaktionen opstår;
- serveren 'går ned' på grund af OutOfMemory, hvis opgaven kræver behandling og ændring af store datamængder, og tilstedeværelsen af synkrone integrationer gør det betydeligt vanskeligere at opdele behandlingerne i mere 'lette' transaktioner.
Set fra et arkitektonisk perspektiv fører brugen af blokkerende opkald ved integration til tab af kontrol over kvaliteten af de enkelte delsystemer: det er umuligt at sikre målsætningerne for kvaliteten af ét delsystem uden at tage hensyn til kvaliteten af de andre delsystemer. Hvis delsystemerne udvikles af forskellige teams, udgør dette en stor udfordring.
Det bliver endnu mere interessant, hvis de integrerede delsystemer findes i forskellige applikationer, og der skal foretages synkrone ændringer fra begge sider. Hvordan sikrer man transaktionaliteten af disse ændringer?
Hvis ændringerne foretages med separate transaktioner, vil det være nødvendigt at sikre pålidelig fejlhåndtering og kompensation, hvilket fuldstændig underminerer den primære fordel ved synkrone integrationer – enkelhed.
Man tænker også på distribuerede transaktioner, men vi bruger dem ikke i vores løsninger: det er svært at sikre pålidelighed.
„Saga“ som løsning på transaktionsproblemet
Med den stigende popularitet af mikrotjenester bliver det stadig mere efterspurgt .
Denne skabelon løser de nævnte problemer med langvarige transaktioner rigtig godt og udvider samtidig mulighederne for systemstatusstyring fra forretningslogikkens side: kompensation efter en mislykket transaktion kan sikre en alternativ databehandlingsrute uden at tilbageføre systemet til den oprindelige tilstand. Det gør det også muligt at undgå at gentage de succesfuldt afsluttede trin i databehandlingen ved gentagne forsøg på at nå en „god“ afslutning.
Interessant nok er denne skabelon også relevant i monolitiske systemer, når det handler om integration af svagt sammenkoblede delsystemer, hvor der observeres negative effekter forårsaget af langvarige transaktioner og de tilhørende ressourceblokeringer.
Relateret til vores forretningsprocesser i stil med BPM viser det sig at være meget let at implementere „Sagaer“: de enkelte trin i en „Saga“ kan defineres som aktiviteter inden for forretningsprocessen, og den persistente tilstand i forretningsprocessen definerer også den interne tilstand i „Sagaen“. Det vil sige, at vi ikke har brug for nogen ekstra koordineringsmekanisme. Det kræver blot en meddelelsesbroker med støtte til „at least once“ garantier som transport.
Men selv dette løsning har sin „pris“:
- Forretningslogik bliver mere kompleks: der skal arbejdes med kompensationer;
- Det vil være nødvendigt at give afkald på full consistency, hvilket kan være særligt følsomt for monolitiske systemer;
- Arkitekturen bliver en smule mere kompliceret, og der opstår et ekstra behov for en meddelelsesbroker;
- Der vil være behov for ekstra overvågnings- og administrationsmidler (selvom det overordnet set faktisk er godt: kvaliteten af systemets service vil stige).
For monolitiske systemer er berettigelsen af "Sagas" ikke så åbenlys. For mikrotjenester og andre SOA, hvor der sandsynligvis allerede findes en broker, og hvor full consistency blev ofret fra projektets start, kan fordelene ved at bruge denne skabelon langt opveje ulemperne, især når der findes et praktisk API på forretningslogik-niveau.
Kapsling af forretningslogik i mikrotjenester
Da vi begyndte at eksperimentere med mikrotjenester, opstod der et rimeligt spørgsmål: hvor skal den domænerelaterede forretningslogik placeres i forhold til tjenesten, der sikrer persistens af domænedata?
Når man ser på arkitekturen af forskellige BPMS, kan det virke fornuftigt at adskille forretningslogikken fra persistens: at skabe et lag af platform- og domæneuafhængige mikrotjenester, der danner et miljø og container til udførelse af domænerelateret forretningslogik, mens persistens af domænedata implementeres som et separat lag af meget simple og letvægtige mikrotjenester. Forretningsprocesser vil i så fald orkestrere tjenesterne i persistenslaget.

Denne tilgang har en kæmpe fordel: man kan frit udbygge platformens funktionalitet, og "tyngden" vil kun være i det tilsvarende lag af platformmikrotjenester. Forretningsprocesser fra ethvert domæne får straks mulighed for at bruge den nye funktionalitet på platformen, så snart den er opdateret.
En mere detaljeret undersøgelse afslørede væsentlige mangler ved denne tilgang:
- Platformtjenesten, der udfører forretningslogikken for mange domæner, bærer store risici som en enkelt fejlkilde. Hyppige ændringer i forretningslogikken øger risikoen for fejl, der kan føre til nedbrud, som spreder sig til hele systemet;
- Ydelsesproblemer: forretningslogikken arbejder med sine data gennem et smalt og langsomt interface:
- data vil blive marshallet og opgraderet gennem netværksstakken flere gange;
- domænetjenesten vil ofte returnere flere data, end forretningslogikken har brug for til behandling, på grund af utilstrækkelige parametriseringsevner på niveauet for den eksterne API-tjeneste;
- flere uafhængige dele af forretningslogikken kan gentagne gange anmode om de samme data til behandling (man kan mildne dette problem ved at tilføje sessionselementer, der cacher data, men det komplicerer arkitekturen yderligere og skaber problemer med datavaliditet og cache-invalidation);
- transaktionsproblemer:
- forretningsprocesser med vedholdende tilstand, hvis opbevaring håndteres af platformtjenesten, vil være uoverensstemmende med domænedataene, og der er ikke umiddelbare løsninger på dette problem;
- flytning af låsning af domænedata uden for transaktionen: hvis domæneforretningslogikken skal foretage ændringer efter først at have kontrolleret gyldigheden af de aktuelle data, er det nødvendigt at udelukke muligheden for konkurrencemæssige ændringer af de behandlede data. Ekstern datalåsning kan hjælpe med at løse denne opgave, men en sådan løsning medfører yderligere risici og reducerer systemets samlede pålidelighed;
- yderligere komplikationer ved opdatering: i nogle tilfælde skal opdatering af persistenttjenesten og forretningslogikken ske synkront eller i en striks rækkefølge.
I sidste ende måtte vi vende tilbage til rødderne: at inkapsulere domænedata og domæneforretningslogik i en enkelt mikrotjeneste. Denne tilgang gør det lettere at opfatte mikrotjenesten som en sammenhængende komponent i systemet og undgår de ovennævnte problemer. Det kommer heller ikke gratis:
- der kræves standardisering af API til interaktion med forretningslogikken (især for at sikre brugeraktiviteter inden for forretningsprocesserne) og API-platformtjenester; der kræves mere opmærksomhed ved ændring af API, direkte og bagudkompatibilitet;
- der kræves tilføjelse af yderligere runtime-biblioteker for at sikre funktionen af forretningslogikken inden for hver sådan mikrotjeneste, og dette medfører nye krav til disse biblioteker: letvægts og minimum af transitive afhængigheder;
- Udviklere af forretningslogik skal holde øje med versionerne af bibliotekerne: hvis en mikrotjeneste ikke er blevet vedligeholdt længe, vil den sandsynligvis have en forældet version af bibliotekerne. Dette kan blive en uventet hindring for at tilføje en ny funktion og kan kræve migrering af den gamle forretningslogik i en sådan tjeneste til nye versioner af bibliotekerne, hvis der har været inkompatible ændringer mellem versionerne.

Lag af platformtjenester i en sådan arkitektur er også til stede, men dette lag danner ikke længere en container til udførelse af domæneforretningslogik, men blot dens miljø, som tilbyder yderligere "platform"-funktioner. Et sådant lag er nødvendigt, ikke kun for at bevare letvægtskarakteren af domænemikrotjenester, men også for at centralisere styringen.
For eksempel genererer brugeraktiviteter i forretningsprocesser opgaver. Men når brugeren arbejder med opgaver, skal han kunne se opgaver fra alle domæner i en samlet liste, og derfor skal der være en tilsvarende platformtjeneste til registrering af opgaver, renset for domæneforretningslogik. At bevare inkapslingen af forretningslogik i denne kontekst er temmelig problematisk, og det er endnu en kompromis i denne arkitektur.
Integration af forretningsprocesser set fra en applikationsudviklers synspunkt
Som allerede nævnt skal applikationsudvikleren være abstraheret fra de tekniske og ingeniørtekniske detaljer ved implementeringen af interaktionen mellem flere applikationer for at kunne forvente god udviklingsproduktivitet.
Lad os prøve at løse en temmelig kompleks integrationsopgave, der er specielt designet til denne artikel. Det vil være en "spil"-opgave med tre applikationer, hvor hver af dem definerer et bestemt domænenavn: "app1", "app2", "app3".
Inden for hver applikation starter forretningsprocesser, der begynder at "spille bold" gennem integreringsbussen. Rollen som bold vil blive spillet af meddelelser med navnet "Ball".
Spillets regler:
- den første spiller – initiativtageren. Han inviterer andre spillere til spillet, starter spillet og kan til enhver tid afslutte det;
- andre spillere erklærer deres deltagelse i spillet, "lærer" hinanden at kende og den første spiller;
- når en spiller har taget bolden, vælger han en anden deltagende spiller og passer bolden til ham. Det samlede antal afleveringer tælles.
- Hver spiller har 'energi', som mindskes med hver aflevering, de laver. Når energien er opbrugt, forlader spilleren spillet og erklærer deres exit;
- hvis spilleren er alene, erklærer de straks deres exit;
- Når alle spillere er ude, erklærer den første spiller spillets afslutning. Hvis han er blevet elimineret tidligere, skal han fortsætte med at overvåge spillet for at afslutte det.
For at løse denne opgave vil jeg benytte vores DSL til forretningsprocesser, som gør det muligt at beskrive logikken på Kotlin kompakt med et minimum af boilerplate.
I applikationen app1 vil forretningsprocessen for den første spiller (som også er initiatoren af spillet) køre:
class InitialPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.constraint.UniqueConstraints
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.dsl.taskOperation
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList : ArrayList()
// Dette er klassen for processeksemplet: den indeholder dens indre tilstand
class InitialPlayer : ProcessImpl(initialPlayerModel) {
var playerName: String by persistent("Player1")
var energy: Int by persistent(30)
var players: PlayersList by persistent(PlayersList())
var shotCounter: Int = 0
}
// Dette er deklarationen af procesmodellen: den oprettes én gang, bruges af alle
// eksempler af den tilsvarende klasse
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Ifølge reglerne er den første spiller initiator af spillet og skal være den eneste
uniqueConstraint = UniqueConstraints.singleton
// Vi erklærer aktiviteterne, som forretningsprocessen består af
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "${players.size} spillere er tilsluttet. Starter vi?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... spiller ${signal.data} joiner ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... spiller ${signal.data} er ude ...")
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val throwStartBall = messageSend("Ball") {
messageData = { 1 }
activation = { selectNextPlayer() }
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
// Nu konstruerer vi procesgrafen fra de erklærede aktiviteter
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)
}
// Tilføjer ekstra håndteringer til aktiviteterne for logning
sendNewGameSignal.onExit { println("Lad os spille!") }
sendStopGameSignal.onExit { println("Stop!") }
sendPlayerOut.onExit { println("$playerName: Jeg er ude!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Trin ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Udover at udføre forretningslogik kan den angivne kode generere en objektmodel af forretningsprocessen, som kan visualiseres i form af et diagram. Visualisatoren har vi endnu ikke implementeret, så jeg har brugt lidt tid på at tegne (her har jeg forenklet BPMN-notationen ved brug af gates for at forbedre sammenhængen mellem diagrammet og den angivne kode):

Applikationen app2 vil inkludere forretningsprocessen fra en anden spiller:
class RandomPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList<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:

I appen app3 vil vi ændre spillerens adfærd en smule: i stedet for at vælge den næste spiller tilfældigt, vil han handle efter round-robin algoritmen:
class RoundRobinPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RoundRobinPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var nextPlayerIndex: Int by persistent(-1)
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val roundRobinPlayerModel = processModel(
name = "RoundRobinPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
var idx = process.nextPlayerIndex + 1
if (idx >= process.players.size) {
idx = 0
}
process.nextPlayerIndex = idx
val player = if (process.players.isNotEmpty())
process.players[idx]
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Ellers adskiller spillerens opførsel sig ikke fra den tidligere, så diagrammet ændrer sig ikke.
Nu er der brug for en test for at køre alt dette. Jeg vil kun medtage koden for selve testen for ikke at overbelaste artiklen med boilerplate (faktisk har jeg brugt et testmiljø, der tidligere blev oprettet 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");
// Nu skal vi vente lidt, så spillerne kan "lære" hinanden at kende.
// At vente med sleep er en dårlig løsning, men det er det enkleste.
// Gør ikke sådan i seriøse tests!
Thread.sleep(1000);
// Vi starter spillet og afslutter brugeraktiviteten
assertTrue(closeTask(app1, pl1, "Start"));
app1.getWaiting().waitProcessFinished(pl1);
app2.getWaiting().waitProcessFinished(pl2);
app2.getWaiting().waitProcessFinished(pl3);
app3.getWaiting().waitProcessFinished(pl4);
app3.getWaiting().waitProcessFinished(pl5);
}
private Map playerParams(String name, int energy) {
Map params = new HashMap();
params.put("playerName", name);
params.put("energy", energy);
return params;
}Vi starter testen og ser loggen:
console output
Låsningen af nøglen lock://app1/process/InitialPlayer er aktive
Lad os spille!
Låsningen af nøglen lock://app1/process/InitialPlayer er fjernet
Player2: Jeg er her!
Player3: Jeg er her!
Player4: Jeg er her!
Player5: Jeg er her!
... tilslut spiller Player2 ...
... tilslut spiller Player4 ...
... tilslut spiller Player3 ...
... tilslut spiller Player5 ...
Trin 1: Player1 >>> Player3
Trin 2: Player3 >>> Player5
Trin 3: Player5 >>> Player3
Trin 4: Player3 >>> Player4
Trin 5: Player4 >>> Player3
Trin 6: Player3 >>> Player4
Trin 7: Player4 >>> Player5
Trin 8: Player5 >>> Player2
Trin 9: Player2 >>> Player5
Trin 10: Player5 >>> Player4
Trin 11: Player4 >>> Player2
Trin 12: Player2 >>> Player4
Trin 13: Player4 >>> Player1
Trin 14: Player1 >>> Player4
Trin 15: Player4 >>> Player3
Trin 16: Player3 >>> Player1
Trin 17: Player1 >>> Player2
Trin 18: Player2 >>> Player3
Trin 19: Player3 >>> Player1
Trin 20: Player1 >>> Player5
Trin 21: Player5 >>> Player1
Trin 22: Player1 >>> Player2
Trin 23: Player2 >>> Player4
Trin 24: Player4 >>> Player5
Trin 25: Player5 >>> Player3
Trin 26: Player3 >>> Player4
Trin 27: Player4 >>> Player2
Trin 28: Player2 >>> Player5
Trin 29: Player5 >>> Player2
Trin 30: Player2 >>> Player1
Trin 31: Player1 >>> Player3
Trin 32: Player3 >>> Player4
Trin 33: Player4 >>> Player1
Trin 34: Player1 >>> Player3
Trin 35: Player3 >>> Player4
Trin 36: Player4 >>> Player3
Trin 37: Player3 >>> Player2
Trin 38: Player2 >>> Player5
Trin 39: Player5 >>> Player4
Trin 40: Player4 >>> Player5
Trin 41: Player5 >>> Player1
Trin 42: Player1 >>> Player5
Trin 43: Player5 >>> Player3
Trin 44: Player3 >>> Player5
Trin 45: Player5 >>> Player2
Trin 46: Player2 >>> Player3
Trin 47: Player3 >>> Player2
Trin 48: Player2 >>> Player5
Trin 49: Player5 >>> Player4
Trin 50: Player4 >>> Player2
Trin 51: Player2 >>> Player5
Trin 52: Player5 >>> Player1
Trin 53: Player1 >>> Player5
Trin 54: Player5 >>> Player3
Trin 55: Player3 >>> Player5
Trin 56: Player5 >>> Player2
Trin 57: Player2 >>> Player1
Trin 58: Player1 >>> Player4
Trin 59: Player4 >>> Player1
Trin 60: Player1 >>> Player4
Trin 61: Player4 >>> Player3
Trin 62: Player3 >>> Player2
Trin 63: Player2 >>> Player5
Trin 64: Player5 >>> Player4
Trin 65: Player4 >>> Player5
Trin 66: Player5 >>> Player1
Trin 67: Player1 >>> Player5
Trin 68: Player5 >>> Player3
Trin 69: Player3 >>> Player4
Trin 70: Player4 >>> Player2
Trin 71: Player2 >>> Player5
Trin 72: Player5 >>> Player2
Trin 73: Player2 >>> Player1
Trin 74: Player1 >>> Player4
Trin 75: Player4 >>> Player1
Trin 76: Player1 >>> Player2
Trin 77: Player2 >>> Player5
Trin 78: Player5 >>> Player4
Trin 79: Player4 >>> Player3
Trin 80: Player3 >>> Player1
Trin 81: Player1 >>> Player5
Trin 82: Player5 >>> Player1
Trin 83: Player1 >>> Player4
Trin 84: Player4 >>> Player5
Trin 85: Player5 >>> Player3
Trin 86: Player3 >>> Player5
Trin 87: Player5 >>> Player2
Trin 88: Player2 >>> Player3
Player2: Jeg er ude!
Trin 89: Player3 >>> Player4
... spiller Player2 er ude ...
Trin 90: Player4 >>> Player1
Trin 91: Player1 >>> Player3
Trin 92: Player3 >>> Player1
Trin 93: Player1 >>> Player4
Trin 94: Player4 >>> Player3
Trin 95: Player3 >>> Player5
Trin 96: Player5 >>> Player1
Trin 97: Player1 >>> Player5
Trin 98: Player5 >>> Player3
Trin 99: Player3 >>> Player5
Trin 100: Player5 >>> Player4
Trin 101: Player4 >>> Player5
Player4: Jeg er ude!
... spiller Player4 er ude ...
Trin 102: Player5 >>> Player1
Trin 103: Player1 >>> Player3
Trin 104: Player3 >>> Player1
Trin 105: Player1 >>> Player3
Trin 106: Player3 >>> Player5
Trin 107: Player5 >>> Player3
Trin 108: Player3 >>> Player1
Trin 109: Player1 >>> Player3
Trin 110: Player3 >>> Player5
Trin 111: Player5 >>> Player1
Trin 112: Player1 >>> Player3
Trin 113: Player3 >>> Player5
Trin 114: Player5 >>> Player3
Trin 115: Player3 >>> Player1
Trin 116: Player1 >>> Player3
Trin 117: Player3 >>> Player5
Trin 118: Player5 >>> Player1
Trin 119: Player1 >>> Player3
Trin 120: Player3 >>> Player5
Trin 121: Player5 >>> Player3
Player5: Jeg er ude!
... spiller Player5 er ude ...
Trin 122: Player3 >>> Player5
Trin 123: Player5 >>> Player1
Player5: Jeg er ude!
Trin 124: Player1 >>> Player3
... spiller Player5 er ude ...
Trin 125: Player3 >>> Player1
Trin 126: Player1 >>> Player3
Player1: Jeg er ude!
... spiller Player1 er ude ...
Trin 127: Player3 >>> Player3
Player3: Jeg er ude!
Stop!
Trin 128: Player3 >>> Player3
Player3: Jeg er ude!Af alt dette kan der drages nogle vigtige konklusioner:
- med de nødvendige værktøjer kan applikationsudviklere skabe integrationsinteraktioner mellem applikationer uden at bryde med forretningslogikken;
- kompleksiteten af den integrerende opgave, der kræver ingeniørkompetencer, kan skjules inden for rammerne, hvis dette er indgået i rammearkitekturen fra starten. Sværhedsgraden af opgaven kan dog ikke skjules, så løsningen på en svær opgave vil fremgå som sådan i koden;
- ved udvikling af integrationslogik er det vigtigt at tage højde for eventually consistency og fraværet af lineariserbarhed i ændringen af tilstanden for alle deltagere i integrationen. Dette tvinger os til at komplicere logikken for at gøre den ufølsom over for rækkefølgen af eksterne hændelser. I vores eksempel må spilleren deltage i spillet først efter at have meldt sit exit: andre spillere vil fortsætte med at afleverer bolden til ham, indtil informationen om hans exit er nået og behandlet af alle deltagere. Denne logik følger ikke de officielle regler for spillet og er en kompromisløsning inden for den valgte arkitektur.
Lad os nu tale om forskellige nuancer af vores løsning, kompromiser og andre aspekter.
Alle meddelelser er i en enkelt kø
Alle integrerede applikationer arbejder med en enkelt integrationsbus, der præsenteres som en ekstern broker, en kø BPMQueue – til beskeder og et emne BPMTopic – til signaler (begivenheder). At lede alle meddelelser gennem én kø er i sig selv et kompromis. På forretningslogikniveauet kan der nu introduceres ubegrænsede nye beskedtyper uden ændringer i systemets struktur. Dette er en væsentlig forenkling, men den indebærer visse risici, som vi i konteksten af vores typiske opgaver ikke fandt så betydelige.

Der er dog en nuance her: hver applikation filtrerer sine 'egne' meddelelser fra køen allerede ved indgangen, baseret på sit domænenavn. Domænet kan også angives i signalerne, hvis det er nødvendigt at begrænse 'synligheden' af signalet til en enkelt applikation. Dette burde øge båndbredden af busen, men forretningslogikken skal nu operere med domænenavne: for adressering af meddelelser - obligatorisk, for signaler - ønskeligt.
Sikring af pålideligheden af integrationsbussen
Pålidelighed består af flere faktorer:
- den valgte meddelelsesbroker er et kritisk komponent i arkitekturen og et enkelt fejlpunk: den skal være tilstrækkeligt fejltolerant. Der bør kun anvendes velafprøvede implementationer med god support og et stort fællesskab;
- det er nødvendigt at sikre høj tilgængelighed for meddelelsesbrokeren, hvilket betyder, at den skal være fysisk adskilt fra de integrerede applikationer (at sikre høj tilgængelighed for applikationer med forretningslogik er betydeligt mere kompliceret og dyrere);
- brokeren skal sikre 'at least once' leveringsgarantier. Dette er et obligatorisk krav for en pålidelig drift af integrationsbussen. Der er ikke behov for garantier på niveauet 'exactly once': forretningsprocesser er typisk ikke følsomme over for gentagen modtagelse af meddelelser eller begivenheder, og i særlige opgaver, hvor dette er vigtigt, er det enklere at tilføje ekstra kontrol i forretningslogikken end konstant at bruge tilstrækkeligt 'dyre' garantier;
- afsendelse af meddelelser og signaler skal involveres i den samlede transaktion med ændring af tilstanden i forretningsprocesserne og domænedataene. Det foretrukne valg vil være at anvende mønsteret , men det vil kræve en ekstra tabel i databasen og en retransmitter. I JEE-applikationer kan dette aspekt forenkles ved at bruge en lokal JTA-manager, men forbindelsen til den valgte broker skal kunne fungere i tilstand ;
- behandlere af indkommende meddelelser og begivenheder skal også arbejde med transaktionen, der ændrer tilstanden i forretningsprocessen: hvis en sådan transaktion rulles tilbage, skal modtagelsen af meddelelsen også annulleres;
- meddelelser, der ikke kunne leveres på grund af fejl, skal placeres i et separat lager (Dead Letter Queue). Vi har oprettet en særskilt platformmikrotjeneste til dette formål, som gemmer sådanne beskeder i sit lager, indekserer dem efter attributter (for hurtig gruppering og søgning) og stiller et API til rådighed for at vise, gensende til destinationen, og slette beskeder. Systemadministratorer kan arbejde med denne tjeneste via deres webgrænseflade;
- I brokerindstillingerne skal man justere antallet af genforsøgsleveringer og forsinkelser mellem dem for at mindske sandsynligheden for, at beskeder havner i DLQ (det er næsten umuligt at beregne optimale parametre, men man kan arbejde empirisk og tilpasse dem løbende);
- DLQ-lageret skal overvåges kontinuerligt, og overvågningssystemet skal underrette systemadministratorer, så der kan reagere hurtigt ved modtagelse af uafsendte beskeder. Dette vil reducere „skadeområdet“ fra en opstået fejl eller forretningslogikfejl;
- Integrationsbusser skal være ufølsomme over for midlertidig fravær af applikationer: abonnementer på emner skal være varige, og applikationens domænenavn skal være unikt, så beskederne fra køen ikke forsøges behandlet af en anden i applikationens fravær.
Sikring af trådsikkerhed i forretningslogikken
Den samme instans af forretningsprocessen kan modtage flere beskeder og hændelser samtidig, hvor behandling vil blive igangsat parallelt. Samtidig skal det for den applikationsudvikler være enkelt og trådsikkert.
Forretningslogikken i processen behandler hver ekstern hændelse, der påvirker denne forretningsproces, enkeltvis. Sådanne hændelser kan være:
- start af en instans af forretningsprocessen;
- brugerhandlinger relateret til aktiviteten inden for forretningsprocessen;
- modtagelse af en besked eller signal, som instansen af forretningsprocessen er tilmeldt;
- udløb af en timer, som er sat af instansen af forretningsprocessen;
- styringsindgribelse via API (f.eks. nødstop af processen).
Hver sådan begivenhed kan ændre tilstanden af en instans af forretningsprocessen: nogle aktiviteter kan afsluttes, mens andre kan sættes i gang, og værdierne for persistente egenskaber kan ændre sig. Afslutningen af en aktivitet kan aktivere en eller flere efterfølgende aktiviteter. Disse kan igen stoppe og afvente andre begivenheder, eller hvis de ikke har brug for yderligere data, kan de afsluttes i samme transaktion. Før transaktionen afsluttes, gemmes den nye tilstand af forretningsprocessen i databasen, hvor den vil vente på den næste eksterne begivenhed.
De persistente data for forretningsprocessen, gemt i en relationsdatabase, er en meget bekvemmelig synkroniseringspunkt for behandlingen, hvis man bruger SELECT FOR UPDATE. Hvis én transaktion har været i stand til at hente tilstanden af forretningsprocessen fra databasen for at ændre den, så kan ingen anden transaktion parallelt hente den samme tilstand til en anden ændring, og efter afslutningen af den første transaktion vil den anden med garanti modtage den allerede ændrede tilstand.
Ved at bruge pessimisteriske låsninger på sidesystemet opfylder vi alle nødvendige krav , samt bevarer muligheden for at skalere applikationen med forretningslogik ved at øge antallet af kørende instanser.
Men pessimisteriske låsninger risikerer at give os deadlocks, så SELECT FOR UPDATE bør alligevel begrænses til et rimeligt timeout ved eventuelle deadlocks i nogle påfaldende cases i forretningslogikken.
Et andet problem er synkroniseringen af starten af forretningsprocessen. Så længe der ikke er nogen instans af forretningsprocessen, er der heller ikke nogen tilstand i databasen, så den beskrevne metode er ikke anvendelig. Hvis det er nødvendigt at sikre unikhed af instansen af forretningsprocessen inden for et bestemt scope, kræves der da en synkroniseringsobjekt, der er knyttet til procesklassen og det relevante scope. For at løse dette problem anvender vi en anden mekanisme til låsning, der gør det muligt at tage lås på en vilkårlig ressource, defineret med en nøgle i URI-format, via en ekstern service.
I vores eksempler indeholder forretningsprocessen InitialPlayer en erklæring
uniqueConstraint = UniqueConstraints.singletonDerfor findes der i loggen beskeder om at tage og frigive låsen til den tilsvarende nøgle. For andre forretningsprocesser findes der ikke sådanne meddelelser: uniqueConstraint er ikke angivet.
Problemer med forretningsprocesser med vedholdende tilstand
Nogle gange hjælper den vedholdende tilstand ikke kun, men forstyrrer endda udviklingen.
Problemerne opstår, når der skal foretages ændringer i forretningslogikken og/eller forretningsprocessens model. Ikke alle sådanne ændringer viser sig at være kompatible med den gamle tilstand af forretningsprocesserne. Hvis der er mange "aktive" eksemplarer i databasen, kan inkompatible ændringer medføre en række problemer, som vi ofte har stødt på ved brugen af jBPM.
Afhængigt af dybden af ændringerne kan man handle på to måder:
- oprette en ny type forretningsproces for ikke at foretage inkompatible ændringer i den gamle og bruge den i stedet for den gamle ved opstart af nye eksemplarer. De gamle eksemplarer vil fortsætte med at fungere "som før";
- migrere den vedholdende tilstand af forretningsprocesserne ved opdatering af forretningslogikken.
Den første vej er enklere, men har sine begrænsninger og ulemper, såsom:
- duplikering af forretningslogikken i mange modeller af forretningsprocesser, hvilket øger mængden af forretningslogik;
- der kræves ofte en hurtig overgang til den nye forretningslogik (for integrationsopgaver – næsten altid);
- udvikleren ved ikke, hvornår det er muligt at fjerne forældede modeller.
I praksis bruger vi begge tilgange, men har truffet en række beslutninger for at gøre livet lettere for os:
- i databasen gemmes den vedholdende tilstand af forretningsprocessen i et letlæseligt og nemt bearbejdeligt format: i JSON-format. Dette muliggør migrationer både inden for applikationen og udenfor. I værste fald kan man endda ændre det manuelt (især nyttigt under udviklingen under fejlfinding);
- den integrerende forretningslogik bruger ikke navnene på forretningsprocesserne, så man til enhver tid kan udskifte implementeringen af en af de deltagende processer med en ny med et nyt navn (for eksempel "InitialPlayerV2"). Sammenkædningen sker gennem navnene på meddelelser og signaler;
- Processmodellen har et versionsnummer, som vi øger, hvis vi indfører inkompatible ændringer i denne model, og dette nummer gemmes sammen med tilstanden af procesinstansen;
- Den persistente tilstand af processen indlæses fra databasen først i en praktisk objektmodel, som migrationsproceduren kan arbejde med, hvis versionsnummeret for modellen er ændret;
- Migrationsproceduren placeres tæt på forretningslogikken og kaldes «lazy» for hver instans af forretningsprocessen, når den genoprettes fra databasen;
- Hvis det er nødvendigt at migrere tilstanden af alle procesinstanser hurtigt og synkront, anvendes mere klassiske databasmigrationsløsninger, men der skal arbejdes med JSON der.
Har vi brug for endnu en ramme for forretningsprocesser?
Løsningerne beskrevet i artiklen har gjort det muligt for os at forenkle vores liv betydeligt, udvide omfanget af spørgsmål, der løses på anvendelsesudviklingsniveau, og gøre ideerne om at isolere forretningslogik i mikrotjenester mere attraktive. Der er blevet udført meget arbejde for at skabe en meget 'letvægts' ramme for forretningsprocesser samt servicekomponenter til løsning af de nævnte problemer i konteksten af et bredt spektrum af anvendte opgaver. Vi har et ønske om at dele disse resultater og offentliggøre udviklingen af de generelle komponenter under en fri licens. Dette vil kræve visse anstrengelser og tid. At forstå efterspørgslen efter sådanne løsninger kunne være en yderligere motivation for os. Den foreslåede artikel giver meget lidt opmærksomhed på rammes muligheder, men nogle af dem er synlige ud fra de præsenterede eksempler. Hvis vi alligevel offentliggør vores ramme, vil der blive afsat en særskilt artikel til den. Indtil da ville vi sætte pris på, hvis du kunne give en kort feedback ved at svare på spørgsmålet:
Kun registrerede brugere kan deltage i undersøgelsen. , please.
Har vi brug for endnu en ramme for forretningsprocesser?
18,8%Ja, vi har længe ledt efter noget lignende.
12,5%Det ville være interessant at vide mere om jeres implementering, det kunne være nyttigt.
6,2%Vi bruger en af de eksisterende rammer, men overvejer at skifte.
18,8%Vi bruger en af de eksisterende rammer, vi er tilfredse.
18,8%Vi klarer os uden ramme.
25,0%Vi laver vores egen.
16 brugere har stemt. 7 brugere har undladt at stemme.
Kilde: habr.com
