BPM styl integrasie

BPM styl integrasie

hallo, Habr!

Ons maatskappy spesialiseer in die ontwikkeling van ERP-klas sagteware oplossings, waarin die grootste deel van transaksionele stelsels met 'n groot hoeveelheid besigheidslogika en werkvloei a la EDMS beset word. Moderne weergawes van ons produkte is gebaseer op JavaEE-tegnologieë, maar ons eksperimenteer ook aktief met mikrodienste. Een van die mees problematiese areas van sulke oplossings is die integrasie van verskeie subsisteme wat verband hou met aangrensende domeine. Integrasietake het ons nog altyd 'n groot kopseer gegee, ongeag die argitektoniese style, tegnologiestapels en raamwerke wat ons gebruik, maar onlangs was daar vordering met die oplossing van sulke probleme.

In die artikel wat onder u aandag gebring is, sal ek praat oor die ervaring en argitektoniese navorsing van NPO Krista in die aangewese area. Ons sal ook 'n voorbeeld van 'n eenvoudige oplossing vir 'n integrasieprobleem uit die oogpunt van 'n toepassingsontwikkelaar oorweeg en uitvind wat agter hierdie eenvoud skuil.

Vrywaring

Die argitektoniese en tegniese oplossings wat in die artikel beskryf word, word deur my aangebied op grond van persoonlike ervaring in die konteks van spesifieke take. Hierdie oplossings maak nie daarop aanspraak dat hulle universeel is nie en is dalk nie optimaal onder ander gebruikstoestande nie.

Wat het BPM daarmee te doen?

Om hierdie vraag te beantwoord, moet ons 'n bietjie delf in die besonderhede van die toegepaste probleme van ons oplossings. Die hoofgedeelte van die besigheidslogika in ons tipiese transaksionele stelsel is om data in die databasis in te voer deur gebruikerskoppelvlakke, handmatig en outomaties hierdie data na te gaan, dit deur een of ander werkvloei te stuur, dit na 'n ander stelsel / analitiese databasis / argief te publiseer, verslae te genereer. Die sleutelfunksie van die stelsel vir kliënte is dus die outomatisering van hul interne besigheidsprosesse.

Gerieflikheidshalwe gebruik ons ​​die term "dokument" in kommunikasie as een of ander abstraksie van 'n datastel, verenig deur 'n gemeenskaplike sleutel, waaraan 'n spesifieke werkvloei "geheg kan word".
Maar wat van die integrasielogika? Die taak van integrasie word immers gegenereer deur die argitektuur van die stelsel, wat in dele "gesaag" word NIE op versoek van die kliënt nie, maar onder die invloed van heeltemal verskillende faktore:

  • onder die invloed van Conway se wet;
  • as gevolg van die hergebruik van substelsels wat voorheen vir ander produkte ontwikkel is;
  • soos deur die argitek besluit, gebaseer op nie-funksionele vereistes.

Daar is 'n groot versoeking om die integrasielogika van die besigheidslogika van die hoofwerkvloei te skei om nie die besigheidslogika met integrasie-artefakte te besoedel nie en die toepassingsontwikkelaar te red om in die eienaardighede van die argitektoniese landskap van die stelsel te delf. Hierdie benadering het 'n aantal voordele, maar die praktyk toon die ondoeltreffendheid daarvan:

  • die oplossing van integrasieprobleme gly gewoonlik af na die eenvoudigste opsies in die vorm van sinchrone oproepe as gevolg van die beperkte uitbreidingspunte in die implementering van die hoofwerkvloei (meer oor die tekortkominge van sinchroniese integrasie hieronder);
  • integrasie-artefakte dring steeds die hoof besigheidslogika binne wanneer terugvoer van 'n ander substelsel vereis word;
  • die toepassingsontwikkelaar ignoreer die integrasie en kan dit maklik breek deur die werkvloei te verander;
  • die stelsel hou op om 'n enkele geheel te wees vanuit die gebruiker se oogpunt, "nate" tussen substelsels word opmerklik, oortollige gebruikeroperasies verskyn wat die oordrag van data van een substelsel na 'n ander inisieer.

Nog 'n benadering is om integrasie-interaksies as 'n integrale deel van die kernbesigheidslogika en -werkvloei te beskou. Om te verhoed dat die vaardigheidsvereistes van toepassingsontwikkelaars die hoogte inskiet, moet die skep van nuwe integrasie-interaksies maklik en natuurlik gedoen word, met minimale opsies vir die keuse van 'n oplossing. Dit is moeiliker as wat dit lyk: die instrument moet kragtig genoeg wees om die gebruiker van die nodige verskeidenheid opsies vir die gebruik daarvan te voorsien en terselfdertyd nie toelaat dat hulle in die voet geskiet word nie. Daar is baie vrae wat 'n ingenieur in die konteks van integrasietake moet beantwoord, maar waaraan 'n toepassingsontwikkelaar nie in hul daaglikse werk moet dink nie: transaksiegrense, konsekwentheid, atomisiteit, sekuriteit, skaal, vrag- en hulpbronverspreiding, roetering, rangskikking, voortplanting en skakelkontekste, ens. Dit is nodig om toepassingsontwikkelaars redelik eenvoudige besluitsjablone aan te bied, waarin antwoorde op al sulke vrae reeds versteek is. Hierdie patrone moet veilig genoeg wees: die besigheidslogika verander baie dikwels, wat die risiko verhoog om foute in te voer, die koste van foute moet op 'n redelike lae vlak bly.

Maar tog, wat het BPM daarmee te doen? Daar is baie opsies vir die implementering van werkvloei ...
Inderdaad, 'n ander implementering van besigheidsprosesse is baie gewild in ons oplossings - deur die verklarende instelling van die staatsoorgangsdiagram en die koppeling van hanteerders met besigheidslogika aan die oorgange. Terselfdertyd is die staat wat die huidige posisie van die "dokument" in die besigheidsproses bepaal, 'n eienskap van die "dokument" self.

BPM styl integrasie
Dit is hoe die proses lyk aan die begin van die projek

Die gewildheid van so 'n implementering is te danke aan die relatiewe eenvoud en spoed van die skep van lineêre besigheidsprosesse. Soos sagtewarestelsels egter meer kompleks word, groei die outomatiese deel van die besigheidsproses en word dit meer kompleks. Daar is 'n behoefte aan ontbinding, hergebruik van dele van prosesse, asook vervurkingsprosesse sodat elke tak parallel uitgevoer word. Onder sulke toestande word die instrument ongerieflik, en die toestandoorgangsdiagram verloor sy inligtingsinhoud (integrasie-interaksies word glad nie in die diagram weerspieël nie).

BPM styl integrasie
Dit is hoe die proses lyk na verskeie herhalings om die vereistes uit te klaar

Die uitweg uit hierdie situasie was die integrasie van die enjin jBPM in sommige produkte met die mees komplekse besigheidsprosesse. Op kort termyn het hierdie oplossing 'n mate van sukses gehad: dit het moontlik geword om komplekse besigheidsprosesse te implementeer terwyl 'n redelik insiggewende en bygewerkte diagram in die notasie gehandhaaf word BPMN2.

BPM styl integrasie
'n Klein deel van 'n komplekse besigheidsproses

Op die lang termyn het die oplossing nie aan die verwagtinge voldoen nie: die hoë arbeidsintensiteit van die skep van besigheidsprosesse deur middel van visuele instrumente het nie toegelaat om aanvaarbare produktiwiteitsaanwysers te bereik nie, en die instrument self het een van die mees onaangename onder ontwikkelaars geword. Daar was ook klagtes oor die interne struktuur van die enjin, wat gelei het tot die verskyning van baie "kolle" en "krukke".

Die belangrikste positiewe aspek van die gebruik van jBPM was die besef van die voordele en skade daaraan verbonde om sy eie aanhoudende toestand vir 'n besigheidsproses-instansie te hê. Ons het ook die moontlikheid gesien om 'n prosesbenadering te gebruik om komplekse integrasieprotokolle tussen verskillende toepassings te implementeer deur asinchroniese interaksies deur seine en boodskappe te gebruik. Die teenwoordigheid van 'n aanhoudende toestand speel 'n deurslaggewende rol hierin.

Op grond van bogenoemde kan ons tot die gevolgtrekking kom: Die prosesbenadering in die BPM-styl stel ons in staat om 'n wye reeks take op te los vir die outomatisering van steeds meer komplekse besigheidsprosesse, integrasieaktiwiteite harmonieus in hierdie prosesse in te pas en die vermoë te behou om die geïmplementeerde proses visueel in 'n geskikte notasie te vertoon.

Nadele van sinchrone oproepe as 'n integrasiepatroon

Sinchroniese integrasie verwys na die eenvoudigste blokkeeroproep. Een substelsel dien as die bedienerkant en stel die API met die verlangde metode bloot. 'n Ander substelsel tree op as 'n kliëntkant en maak op die regte tyd 'n oproep met die verwagting van 'n resultaat. Afhangende van die argitektuur van die stelsel, kan die kliënt- en bedienerkante óf in dieselfde toepassing en proses, óf in verskillendes gehuisves word. In die tweede geval moet u 'n mate van implementering van RPC toepas en die parameters en die resultaat van die oproep opstel.

BPM styl integrasie

So 'n integrasiepatroon het 'n redelike groot stel nadele, maar dit word baie wyd in die praktyk gebruik as gevolg van die eenvoud daarvan. Die spoed van implementering boei en laat jou dit keer op keer toepas in die toestande van "brandende" spertye, en skryf die oplossing in tegniese skuld. Maar dit gebeur ook dat onervare ontwikkelaars dit onbewustelik gebruik, eenvoudig nie die negatiewe gevolge besef nie.

Benewens die mees ooglopende toename in die konnektiwiteit van substelsels, is daar minder ooglopende probleme met "verspreiding" en "rek" transaksies. Inderdaad, as die besigheidslogika enige veranderinge maak, dan is transaksies onontbeerlik, en transaksies sluit op hul beurt sekere toepassingshulpbronne wat deur hierdie veranderinge geraak word. Dit wil sê, totdat een substelsel wag vir 'n antwoord van 'n ander, sal dit nie die transaksie kan voltooi en slotte kan vrystel nie. Dit verhoog die risiko van 'n verskeidenheid effekte aansienlik:

  • stelselreaksie gaan verlore, gebruikers wag lank vir antwoorde op versoeke;
  • die bediener hou oor die algemeen op om op gebruikersversoeke te reageer as gevolg van 'n oorvol draadpoel: meeste van die drade "staan" op die slot van die hulpbron wat deur die transaksie beset word;
  • dooiepunte begin verskyn: die waarskynlikheid van hul voorkoms hang sterk af van die duur van transaksies, die hoeveelheid sakelogika en slotte wat by die transaksie betrokke is;
  • transaksie-time-out verval foute verskyn;
  • die bediener "val" op OutOfMemory as die taak verwerking en verandering van groot hoeveelhede data vereis, en die teenwoordigheid van sinchrone integrasies maak dit baie moeilik om die verwerking in "ligter" transaksies te verdeel.

Uit 'n argitektoniese oogpunt lei die gebruik van blokkeeroproepe tydens integrasie tot 'n verlies aan kwaliteitsbeheer van individuele substelsels: dit is onmoontlik om die kwaliteitteikens van een substelsel in isolasie van die kwaliteitteikens van 'n ander substelsel te verseker. As substelsels deur verskillende spanne ontwikkel word, is dit 'n groot probleem.

Dinge word selfs meer interessant as die substelsels wat geïntegreer word in verskillende toepassings is en sinchrone veranderinge aan beide kante aangebring moet word. Hoe om hierdie veranderinge transaksioneel te maak?

As veranderinge in afsonderlike transaksies aangebring word, sal robuuste uitsonderingshantering en vergoeding verskaf moet word, en dit skakel die grootste voordeel van sinchroniese integrasies - eenvoud - heeltemal uit.

Verspreide transaksies kom ook na vore, maar ons gebruik dit nie in ons oplossings nie: dit is moeilik om betroubaarheid te verseker.

"Saga" as 'n oplossing vir die probleem van transaksies

Met die groeiende gewildheid van mikrodienste, is daar 'n toenemende vraag na Saga patroon.

Hierdie patroon los bogenoemde probleme van lang transaksies perfek op, en brei ook die moontlikhede uit om die toestand van die stelsel van die kant van besigheidslogika te bestuur: vergoeding na 'n onsuksesvolle transaksie mag nie die stelsel terugrol na sy oorspronklike toestand nie, maar bied 'n alternatief dataverwerkingsroete. Dit laat jou ook toe om nie suksesvol voltooide dataverwerkingstappe te herhaal wanneer jy probeer om die proses tot 'n "goeie" einde te bring nie.

Interessant genoeg, in monolitiese stelsels is hierdie patroon ook relevant wanneer dit kom by die integrasie van losgekoppelde substelsels en daar is negatiewe effekte wat veroorsaak word deur lang transaksies en die ooreenstemmende hulpbronslotte.

Met betrekking tot ons besigheidsprosesse in die BPM-styl, blyk dit baie maklik te wees om die Sagas te implementeer: die individuele stappe van die Sagas kan as aktiwiteite binne die besigheidsproses gestel word, en die volgehoue ​​stand van die besigheidsproses bepaal o.a. ander dinge, die interne toestand van die Sagas. Dit wil sê, ons het geen bykomende koördineringsmeganisme nodig nie. Al wat jy nodig het is 'n boodskap makelaar met ondersteuning vir "ten minste een keer" waarborge as 'n vervoer.

Maar so 'n oplossing het ook sy eie "prys":

  • besigheidslogika word meer kompleks: jy moet vergoeding uitwerk;
  • dit sal nodig wees om volle konsekwentheid te laat vaar, wat veral sensitief kan wees vir monolitiese stelsels;
  • die argitektuur word 'n bietjie meer ingewikkeld, daar is 'n bykomende behoefte aan 'n boodskapmakelaar;
  • addisionele monitering- en administrasiehulpmiddels sal vereis word (hoewel dit oor die algemeen selfs goed is: die gehalte van stelseldiens sal toeneem).

Vir monolitiese stelsels is die regverdiging vir die gebruik van "Sags" nie so voor die hand liggend nie. Vir mikrodienste en ander SOA's, waar daar heel waarskynlik reeds 'n makelaar is, en volle konsekwentheid aan die begin van die projek opgeoffer is, kan die voordele van die gebruik van hierdie patroon aansienlik swaarder weeg as die nadele, veral as daar 'n gerieflike API by die besigheid logika vlak.

Inkapseling van besigheidslogika in mikrodienste

Toe ons met mikrodienste begin eksperimenteer het, het 'n redelike vraag ontstaan: waar om die domeinbesigheidslogika te plaas in verhouding tot die diens wat domeindatabestendigheid verskaf?

Wanneer na die argitektuur van verskeie BPMS gekyk word, kan dit redelik lyk om besigheidslogika van volharding te skei: skep 'n laag platform- en domein-onafhanklike mikrodienste wat die omgewing en houer vorm vir die uitvoering van domeinbesigheidslogika, en reël domeindata-volharding as 'n aparte laag van baie eenvoudige en liggewig mikrodienste. Besigheidsprosesse in hierdie geval orkestreer die dienste van die volhardingslaag.

BPM styl integrasie

Hierdie benadering het 'n baie groot pluspunt: jy kan die funksionaliteit van die platform verhoog soveel as wat jy wil, en net die ooreenstemmende laag platform-mikrodienste sal hieruit "vet word". Besigheidsprosesse van enige domein kry onmiddellik die geleentheid om die nuwe funksionaliteit van die platform te gebruik sodra dit opgedateer is.

'n Meer gedetailleerde studie het beduidende tekortkominge van hierdie benadering aan die lig gebring:

  • 'n platformdiens wat die besigheidslogika van baie domeine gelyktydig uitvoer, hou groot risiko's in as 'n enkele punt van mislukking. Gereelde veranderinge aan besigheidslogika verhoog die risiko van foute wat lei tot stelselwye mislukkings;
  • prestasiekwessies: besigheidslogika werk met sy data deur 'n nou en stadige koppelvlak:
    • die data sal weereens saamgestel en deur die netwerkstapel gepomp word;
    • die domeindiens sal dikwels meer data terugstuur as wat die besigheidslogika vir verwerking vereis, as gevolg van onvoldoende navraagparameteriseringsvermoëns op die vlak van die diens se eksterne API;
    • verskeie onafhanklike stukke besigheidslogika kan herhaaldelik dieselfde data vir verwerking versoek (jy kan hierdie probleem versag deur sessiebone by te voeg wat data kas, maar dit bemoeilik die argitektuur verder en skep probleme van datavarsheid en ongeldigmaking van die kas);
  • transaksionele kwessies:
    • besigheidsprosesse met aanhoudende toestand wat deur die platformdiens gestoor word, is nie in ooreenstemming met domeindata nie, en daar is geen maklike maniere om hierdie probleem op te los nie;
    • die sluiting van domeindata uit die transaksie te verskuif: as die domeinbesigheidslogika veranderinge moet aanbring, is dit nodig om die moontlikheid van 'n mededingende verandering in die verwerkte data uit te sluit nadat u eers die korrektheid van die werklike data nagegaan het. Eksterne blokkering van data kan help om die probleem op te los, maar so 'n oplossing hou bykomende risiko's in en verminder die algehele betroubaarheid van die stelsel;
  • bykomende komplikasies tydens opdatering: in sommige gevalle moet u die volhardingsdiens en besigheidslogika sinchronies of in streng volgorde opdateer.

Op die ou end moes ek teruggaan na die basiese beginsels: omsluit domeindata en domeinbesigheidslogika in een mikrodiens. Hierdie benadering vereenvoudig die persepsie van die mikrodiens as 'n integrale komponent in die sisteem en gee nie aanleiding tot bogenoemde probleme nie. Dit is ook nie gratis nie:

  • API-standaardisering word vereis vir interaksie met besigheidslogika (veral om gebruikersaktiwiteite as deel van besigheidsprosesse te verskaf) en API-platformdienste; meer noukeurige aandag aan API-veranderinge, vorentoe en agtertoe versoenbaarheid word vereis;
  • dit word vereis om bykomende looptydbiblioteke by te voeg om die funksionering van die besigheidslogika as deel van elke sodanige mikrodiens te verseker, en dit gee aanleiding tot nuwe vereistes vir sulke biblioteke: ligtheid en 'n minimum van oorganklike afhanklikhede;
  • besigheidslogika-ontwikkelaars moet tred hou met biblioteekweergawes: as 'n mikrodiens vir 'n lang tyd nie gefinaliseer is nie, sal dit heel waarskynlik 'n verouderde weergawe van die biblioteke bevat. Dit kan 'n onverwagte struikelblok wees om 'n nuwe kenmerk by te voeg en kan vereis dat die ou besigheidslogika van so 'n diens na nuwe weergawes van die biblioteke gemigreer word as daar onversoenbare veranderinge tussen weergawes was.

BPM styl integrasie

'n Laag platformdienste is ook in so 'n argitektuur aanwesig, maar hierdie laag vorm nie meer 'n houer vir die uitvoering van domeinbesigheidslogika nie, maar slegs sy omgewing, wat bykomende "platform"-funksies verskaf. So 'n laag is nie net nodig om die ligtheid van domeinmikrodienste te handhaaf nie, maar ook om bestuur te sentraliseer.

Gebruikersaktiwiteite in besigheidsprosesse genereer byvoorbeeld take. Wanneer daar egter met take gewerk word, moet die gebruiker take van alle domeine in die algemene lys sien, wat beteken dat daar 'n toepaslike taakregistrasieplatformdiens moet wees, skoongemaak van domeinbesigheidslogika. Om die inkapseling van besigheidslogika in hierdie konteks te hou, is nogal problematies, en dit is nog 'n kompromie van hierdie argitektuur.

Integrasie van besigheidsprosesse deur die oë van 'n toepassingsontwikkelaar

Soos reeds hierbo genoem, moet die toepassingsontwikkelaar onttrek word van die tegniese en ingenieurskenmerke van die implementering van die interaksie van verskeie toepassings om op goeie ontwikkelingsproduktiwiteit te kan reken.

Kom ons probeer om 'n taamlik moeilike integrasieprobleem op te los, spesiaal vir die artikel uitgevind. Dit sal 'n "speletjie"-taak wees wat drie toepassings behels, waar elkeen van hulle 'n domeinnaam definieer: "app1", "app2", "app3".

Binne elke toepassing word besigheidsprosesse geloods wat deur die integrasiebus begin "bal speel". Boodskappe genaamd "Bal" sal as die bal optree.

Reëls van die spel:

  • die eerste speler is die inisieerder. Hy nooi ander spelers na die speletjie, begin die speletjie en kan dit enige tyd beëindig;
  • ander spelers verklaar hul deelname aan die spel, "maak kennis" met mekaar en die eerste speler;
  • nadat hy die bal ontvang het, kies die speler 'n ander deelnemende speler en gee die bal vir hom aan. Die totale aantal slaags word getel;
  • elke speler het "energie", wat afneem met elke aangee van die bal deur daardie speler. Wanneer die energie opraak, word die speler uit die spel geskakel, wat hul aftrede aankondig;
  • as die speler alleen gelaat word, verklaar hy dadelik sy vertrek;
  • wanneer alle spelers uitgeskakel is, verklaar die eerste speler die einde van die wedstryd. As hy die speletjie vroeër verlaat het, bly dit om die speletjie te volg om dit te voltooi.

Om hierdie probleem op te los, sal ek ons ​​DSL vir besigheidsprosesse gebruik, wat jou toelaat om die logika in Kotlin kompak te beskryf, met 'n minimum van 'n boilerplate.

In die app1-toepassing sal die besigheidsproses van die eerste speler (hy is ook die inisieerder van die speletjie) werk:

klas 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}")
}

Benewens die uitvoering van besigheidslogika, kan die bogenoemde kode 'n objekmodel van 'n besigheidsproses produseer wat as 'n diagram gevisualiseer kan word. Ons het nog nie die visualiseerder geïmplementeer nie, so ons moes tyd spandeer om te teken (hier het ek die BPMN-notasie effens vereenvoudig met betrekking tot die gebruik van hekke om die konsekwentheid van die diagram met die bogenoemde kode te verbeter):

BPM styl integrasie

app2 sal 'n ander speler se besigheidsproses insluit:

klas 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 styl integrasie

In die app3-toepassing sal ons die speler met 'n effens ander gedrag maak: in plaas daarvan om die volgende speler lukraak te kies, sal hy volgens die round-robin-algoritme optree:

klas 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}")
}

Andersins verskil die speler se gedrag nie van die vorige een nie, so die diagram verander nie.

Nou het ons 'n toets nodig om dit alles uit te voer. Ek sal slegs die kode van die toets self gee, om nie die artikel met 'n boilerplaat te bemors nie (ek het trouens die toetsomgewing wat vroeër geskep is gebruik om die integrasie van ander besigheidsprosesse te toets):

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

Voer die toets uit, kyk na die log:

konsole uitset

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

Verskeie belangrike gevolgtrekkings kan uit dit alles gemaak word:

  • indien die nodige gereedskap beskikbaar is, kan toepassingsontwikkelaars integrasie-interaksies tussen toepassings skep sonder om weg te breek van besigheidslogika;
  • die kompleksiteit (kompleksiteit) van 'n integrasietaak wat ingenieursbevoegdhede vereis, kan binne die raamwerk versteek word as dit aanvanklik in die argitektuur van die raamwerk neergelê word. Die moeilikheid van die taak (moeilikheid) kan nie weggesteek word nie, so die oplossing vir 'n moeilike taak in die kode sal daarvolgens lyk;
  • wanneer integrasielogika ontwikkel word, is dit nodig om uiteindelik konsekwentheid en die gebrek aan lineariseerbaarheid van die staatsverandering van alle integrasiedeelnemers in ag te neem. Dit dwing ons om die logika te kompliseer om dit onsensitief te maak vir die volgorde waarin eksterne gebeure plaasvind. In ons voorbeeld word die speler gedwing om aan die wedstryd deel te neem nadat hy sy uitgang uit die wedstryd aangekondig het: ander spelers sal voortgaan om die bal na hom toe te gee totdat die inligting oor sy uitgang bereik en deur alle deelnemers verwerk word. Hierdie logika volg nie uit die reëls van die spel nie en is 'n kompromie-oplossing binne die raamwerk van die gekose argitektuur.

Kom ons praat dan oor die verskillende subtiliteite van ons oplossing, kompromieë en ander punte.

Alle boodskappe in een ry

Alle geïntegreerde toepassings werk met een integrasiebus, wat as 'n eksterne makelaar aangebied word, een BPMQueue vir boodskappe en een BPMTopic-onderwerp vir seine (gebeurtenisse). Om alle boodskappe deur 'n enkele tou te stuur, is op sigself 'n kompromie. Op die besigheidslogika-vlak kan jy nou soveel nuwe soorte boodskappe bekendstel as wat jy wil sonder om veranderinge aan die stelselstruktuur aan te bring. Dit is 'n aansienlike vereenvoudiging, maar dit hou sekere risiko's in wat, in die konteks van ons tipiese take, vir ons nie so betekenisvol gelyk het nie.

BPM styl integrasie

Daar is egter een subtiliteit hier: elke toepassing filtreer "sy" boodskappe uit die tou by die ingang, volgens die naam van sy domein. Die domein kan ook in die seine gespesifiseer word as u die "omvang" van die sein tot 'n enkele toepassing moet beperk. Dit behoort die bandwydte van die bus te verhoog, maar die besigheidslogika moet nou met domeinname werk: verpligtend vir die adressering van boodskappe, wenslik vir seine.

Verseker die betroubaarheid van die integrasiebus

Betroubaarheid bestaan ​​uit verskeie dinge:

  • Die gekose boodskapmakelaar is 'n kritieke komponent van die argitektuur en 'n enkele punt van mislukking: dit moet voldoende foutverdraagsaam wees. U moet slegs beproefde implementerings gebruik met goeie ondersteuning en 'n groot gemeenskap;
  • dit is nodig om hoë beskikbaarheid van die boodskapmakelaar te verseker, waarvoor dit fisies van die geïntegreerde toepassings geskei moet word (hoë beskikbaarheid van toepassings met toegepaste besigheidslogika is baie moeiliker en duurder om te verskaf);
  • die makelaar is verplig om "ten minste een keer" afleweringswaarborge te verskaf. Dit is 'n verpligte vereiste vir betroubare werking van die integrasiebus. Daar is geen behoefte aan "presies een keer" vlakwaarborge nie: besigheidsprosesse is gewoonlik nie sensitief vir die herhaalde aankoms van boodskappe of gebeure nie, en in spesiale take waar dit belangrik is, is dit makliker om bykomende kontroles by besigheidslogika te voeg as om voortdurend te gebruik eerder "duur" " waarborge;
  • die stuur van boodskappe en seine moet betrokke wees by 'n algemene transaksie met 'n verandering in die toestand van besigheidsprosesse en domeindata. Die voorkeuropsie sal wees om die patroon te gebruik Transaksie-uitboks, maar dit sal 'n bykomende tabel in die databasis en 'n aflos vereis. In JEE-toepassings kan dit vereenvoudig word deur 'n plaaslike JTA-bestuurder te gebruik, maar die verbinding met die geselekteerde makelaar moet in modus kan werk XA;
  • hanteerders van inkomende boodskappe en gebeure moet ook werk met die transaksie om die toestand van die besigheidsproses te verander: as so 'n transaksie teruggerol word, dan moet die ontvangs van die boodskap ook gekanselleer word;
  • boodskappe wat weens foute nie afgelewer kon word nie, moet in 'n aparte winkel gestoor word D.L.Q. (Dooie letterry). Om dit te doen, het ons 'n aparte platform-mikrodiens geskep wat sulke boodskappe in sy berging stoor, hulle volgens kenmerke indekseer (vir vinnige groepering en soek), en die API blootstel vir bekyk, herstuur na die bestemmingsadres en uitvee van boodskappe. Stelseladministrateurs kan met hierdie diens werk deur hul webkoppelvlak;
  • in die makelaarinstellings moet jy die aantal afleweringsherproberings en vertragings tussen aflewerings aanpas om die waarskynlikheid dat boodskappe in die DLQ beland te verminder (dit is byna onmoontlik om die optimale parameters te bereken, maar jy kan empiries optree en dit aanpas tydens operasie);
  • die DLQ-winkel moet deurlopend gemonitor word, en die moniteringstelsel moet stelseladministrateurs in kennis stel sodat hulle so vinnig as moontlik kan reageer wanneer onafgelewerde boodskappe voorkom. Dit sal die "skadesone" van 'n mislukking of besigheidslogika-fout verminder;
  • die integrasiebus moet onsensitief wees vir die tydelike afwesigheid van toepassings: onderwerpintekeninge moet duursaam wees, en die domeinnaam van die toepassing moet uniek wees sodat iemand anders nie sy boodskap uit die tou probeer verwerk tydens die afwesigheid van die toepassing nie.

Verseker draadveiligheid van besigheidslogika

Dieselfde geval van 'n besigheidsproses kan verskeie boodskappe en gebeurtenisse gelyktydig ontvang, waarvan die verwerking parallel sal begin. Terselfdertyd, vir 'n toepassingsontwikkelaar, moet alles eenvoudig en draadveilig wees.

Die proses besigheidslogika verwerk elke eksterne gebeurtenis wat hierdie besigheidsproses afsonderlik beïnvloed. Hierdie gebeurtenisse kan wees:

  • die bekendstelling van 'n besigheidsproses-instansie;
  • 'n gebruikersaksie wat verband hou met 'n aktiwiteit binne 'n besigheidsproses;
  • ontvangs van 'n boodskap of sein waarop 'n besigheidsproses-instansie ingeteken is;
  • verstryking van die tydhouer wat deur die besigheidsproses-instansie gestel is;
  • beheer aksie via API (bv. proses afbreek).

Elke so 'n gebeurtenis kan die toestand van 'n sakeproses-instansie verander: sommige aktiwiteite kan eindig en ander begin, die waardes van aanhoudende eiendomme kan verander. Die sluiting van enige aktiwiteit kan lei tot die aktivering van een of meer van die volgende aktiwiteite. Hulle kan op hul beurt ophou wag vir ander gebeurtenisse, of, as hulle geen bykomende data benodig nie, kan hulle in dieselfde transaksie voltooi. Voordat die transaksie gesluit word, word die nuwe toestand van die besigheidsproses in die databasis gestoor, waar dit sal wag vir die volgende eksterne gebeurtenis.

Aanhoudende besigheidsprosesdata wat in 'n relasionele databasis gestoor is, is 'n baie gerieflike verwerkingssinchronisasiepunt wanneer SELECT FOR UPDATE gebruik word. As een transaksie daarin geslaag het om die toestand van die besigheidsproses van die databasis te kry om dit te verander, dan sal geen ander transaksie in parallel dieselfde toestand vir 'n ander verandering kan kry nie, en na die voltooiing van die eerste transaksie is die tweede een gewaarborg om die reeds veranderde toestand te ontvang.

Deur pessimistiese slotte aan die DBMS-kant te gebruik, voldoen ons aan al die nodige vereistes ACID, en behou ook die vermoë om die toepassing met besigheidslogika te skaal deur die aantal lopende gevalle te verhoog.

Pessimistiese slotte bedreig ons egter met dooiepunte, wat beteken dat KIES VIR BYWERKING steeds beperk moet word tot 'n redelike tydsverloop in die geval van dooiepunte in sommige ernstige sake in sakelogika.

Nog 'n probleem is die sinchronisasie van die begin van die besigheidsproses. Alhoewel daar geen sakeproses-instansie is nie, is daar ook geen toestand in die databasis nie, dus sal die beskryfde metode nie werk nie. As jy die uniekheid van 'n besigheidsproses-instansie in 'n bepaalde omvang wil verseker, benodig jy 'n soort sinchronisasie-objek wat verband hou met die prosesklas en die ooreenstemmende omvang. Om hierdie probleem op te los, gebruik ons ​​'n ander sluitmeganisme wat ons toelaat om 'n slot te neem op 'n arbitrêre hulpbron wat deur 'n sleutel in URI-formaat gespesifiseer word deur 'n eksterne diens.

In ons voorbeelde bevat die InitialPlayer-besigheidsproses 'n verklaring

uniqueConstraint = UniqueConstraints.singleton

Daarom bevat die log boodskappe oor die neem en losmaak van die slot van die ooreenstemmende sleutel. Daar is nie sulke boodskappe vir ander besigheidsprosesse nie: uniqueConstraint is nie gestel nie.

Sakeprosesprobleme met aanhoudende toestand

Soms help dit nie net om 'n aanhoudende toestand te hê nie, maar belemmer ook ontwikkeling.
Probleme begin wanneer jy veranderinge aan die besigheidslogika en/of besigheidsprosesmodel moet maak. Daar is nie gevind dat enige sodanige verandering versoenbaar is met die ou toestand van die besigheidsprosesse nie. As daar baie "lewendige" gevalle in die databasis is, kan die maak van onversoenbare veranderinge baie probleme veroorsaak, wat ons dikwels teëgekom het wanneer jBPM gebruik word.

Afhangende van die diepte van verandering, kan jy op twee maniere optree:

  1. skep 'n nuwe besigheidsprosestipe om nie onversoenbare veranderinge aan die ou een te maak nie, en gebruik dit in plaas van die ou een wanneer nuwe gevalle begin word. Ou gevalle sal voortgaan om "op die ou manier" te werk;
  2. migreer die aanhoudende toestand van besigheidsprosesse wanneer besigheidslogika opgedateer word.

Die eerste manier is eenvoudiger, maar het sy beperkings en nadele, byvoorbeeld:

  • duplisering van besigheidslogika in baie besigheidsprosesmodelle, 'n toename in die volume besigheidslogika;
  • dikwels word 'n onmiddellike oorgang na 'n nuwe besigheidslogika vereis (byna altyd in terme van integrasietake);
  • die ontwikkelaar weet nie op watter stadium dit moontlik is om verouderde modelle uit te vee nie.

In die praktyk gebruik ons ​​albei benaderings, maar het 'n aantal besluite geneem om ons lewens te vereenvoudig:

  • in die databasis word die aanhoudende toestand van die besigheidsproses in 'n maklik leesbare en maklik verwerkte vorm gestoor: in 'n JSON-formaatstring. Dit laat jou toe om migrasies beide binne die toepassing en buite uit te voer. In uiterste gevalle kan jy dit ook met handvatsels aanpas (veral nuttig in ontwikkeling tydens ontfouting);
  • die integrasiebesigheidslogika gebruik nie die name van besigheidsprosesse nie, sodat dit te eniger tyd moontlik is om die implementering van een van die deelnemende prosesse met 'n nuwe een te vervang, met 'n nuwe naam (byvoorbeeld "InitialPlayerV2"). Die binding vind plaas deur die name van boodskappe en seine;
  • die prosesmodel het 'n weergawenommer, wat ons verhoog as ons onversoenbare veranderinge aan hierdie model maak, en hierdie nommer word saam met die toestand van die prosesinstansie gestoor;
  • die aanhoudende toestand van die proses word eers vanaf die basis gelees in 'n gerieflike objekmodel waarmee die migrasieprosedure kan werk as die weergawenommer van die model verander het;
  • die migrasieprosedure word langs die besigheidslogika geplaas en word "lui" genoem vir elke geval van die besigheidsproses ten tyde van die herstel daarvan vanaf die databasis;
  • as jy die toestand van alle prosesgevalle vinnig en sinchronies moet migreer, word meer klassieke databasismigrasie-oplossings gebruik, maar jy moet daar met JSON werk.

Het ek 'n ander raamwerk vir besigheidsprosesse nodig?

Die oplossings wat in die artikel beskryf word, het ons in staat gestel om ons lewens aansienlik te vereenvoudig, die reeks probleme wat op toepassingsontwikkelingsvlak opgelos is, uit te brei en die idee om besigheidslogika in mikrodienste te skei aantrekliker te maak. Hiervoor is baie werk gedoen, 'n baie "liggewig" raamwerk vir besigheidsprosesse is geskep, asook dienskomponente vir die oplossing van die geïdentifiseerde probleme in die konteks van 'n wye reeks toegepaste take. Ons het 'n begeerte om hierdie resultate te deel, om die ontwikkeling van algemene komponente in oop toegang onder 'n gratis lisensie te bring. Dit sal 'n bietjie moeite en tyd verg. Om die vraag na sulke oplossings te verstaan, kan vir ons 'n bykomende aansporing wees. In die voorgestelde artikel word baie min aandag gegee aan die vermoëns van die raamwerk self, maar sommige daarvan is sigbaar uit die voorbeelde wat aangebied word. Indien ons nietemin ons raamwerk publiseer, sal 'n aparte artikel daaraan gewy word. Intussen sal ons dankbaar wees as jy 'n bietjie terugvoer gee deur die vraag te beantwoord:

Slegs geregistreerde gebruikers kan aan die opname deelneem. Meld aan, asseblief.

Het ek 'n ander raamwerk vir besigheidsprosesse nodig?

  • 18,8%Ja, ek soek al lank na so iets.

  • 12,5%dit is interessant om meer te wete te kom oor jou implementering, dit kan nuttig wees2

  • 6,2%ons gebruik een van die bestaande raamwerke, maar ons oorweeg dit om dit te vervang1

  • 18,8%ons gebruik een van die bestaande raamwerke, alles pas3

  • 18,8%hantering sonder raamwerk3

  • 25,0%skryf jou eie4

16 gebruikers het gestem. 7 gebruikers het buite stemming gebly.

Bron: will.com

Voeg 'n opmerking