BPM styl yntegraasje

BPM styl yntegraasje

Hoi, Habr!

Us bedriuw is spesjalisearre yn 'e ûntwikkeling fan software-oplossingen fan ERP-klasse, wêryn it liuw syn oandiel wurdt beset troch transaksjesystemen mei in enoarme hoemannichte saaklike logika en workflow a la EDMS. Moderne ferzjes fan ús produkten binne basearre op JavaEE-technologyen, mar wy eksperimintearje ek aktyf mei mikrotsjinsten. Ien fan 'e meast problematyske gebieten fan sokke oplossingen is de yntegraasje fan ferskate subsystemen relatearre oan oanbuorjende domeinen. Yntegraasjetaken hawwe ús altyd in geweldige hoofdpijn jûn, nettsjinsteande de arsjitektoanyske stilen, technologystapels en kaders dy't wy brûke, mar koartlyn is d'r foarútgong west yn it oplossen fan sokke problemen.

Yn it ûnder jo oandacht brochte artikel sil ik prate oer de ûnderfining en arsjitektoanysk ûndersyk fan NPO Krista yn it oanwiisde gebiet. Wy sille ek beskôgje in foarbyld fan in ienfâldige oplossing foar in yntegraasje probleem út it eachpunt fan in applikaasje ûntwikkelder en útfine wat is ferburgen efter dizze ienfâld.

Disclaimer

De arsjitektoanyske en technyske oplossingen beskreaun yn it artikel wurde troch my oanbean op basis fan persoanlike ûnderfining yn 'e kontekst fan spesifike taken. Dizze oplossingen beweare net dat se universeel binne en binne miskien net optimaal ûnder oare gebrûksbetingsten.

Wat hat BPM dêrmei te krijen?

Om dizze fraach te beantwurdzjen, moatte wy in bytsje ferdjipje yn 'e spesifiken fan' e tapaste problemen fan ús oplossingen. It haaddiel fan 'e saaklike logika yn ús typyske transaksjesysteem is it ynfieren fan gegevens yn' e database fia brûkersynterfaces, dizze gegevens manuell en automatysk kontrolearje, it trochjaan troch wat workflow, publisearje it nei in oar systeem / analytyske database / argyf, generearje rapporten. Sa is de kaaifunksje fan it systeem foar klanten de automatisearring fan har ynterne saaklike prosessen.

Foar gemak brûke wy de term "dokumint" yn kommunikaasje as wat abstraksje fan in dataset, ferienige troch in mienskiplike kaai, dêr't in spesifike workflow oan kin "taheakke".
Mar hoe sit it mei de yntegraasjelogika? Ommers, de taak fan yntegraasje wurdt generearre troch de arsjitektuer fan it systeem, dat wurdt "zaagd" yn dielen NET op fersyk fan de klant, mar ûnder de ynfloed fan folslein oare faktoaren:

  • ûnder ynfloed fan 'e wet fan Conway;
  • as gefolch fan it wergebrûk fan subsystemen dy't earder ûntwikkele binne foar oare produkten;
  • lykas besletten troch de arsjitekt, basearre op net-funksjonele easken.

D'r is in grutte ferlieding om de yntegraasjelogika te skieden fan 'e saaklike logika fan' e haadworkflow om de saaklike logika net te fersmoarjen mei yntegraasjeartefakten en de applikaasjeûntwikkelder te bewarjen fan 'e eigenaardichheden fan' e arsjitektoanyske lânskip fan it systeem. Dizze oanpak hat in oantal foardielen, mar praktyk lit syn ineffisjinsje sjen:

  • it oplossen fan yntegraasjeproblemen glydt meastentiids nei de ienfâldichste opsjes yn 'e foarm fan syngroane oproppen troch de beheinde útwreidingspunten yn' e útfiering fan 'e haadworkflow (mear oer de tekoarten fan syngroane yntegraasje hjirûnder);
  • yntegraasje artefakten noch penetrearje de wichtichste saaklike logika as feedback fan in oar subsysteem is nedich;
  • de applikaasje-ûntwikkelder negearret de yntegraasje en kin it maklik brekke troch de workflow te feroarjen;
  • it systeem hâldt op in ienich gehiel te wêzen fanút it eachpunt fan 'e brûker, "naden" tusken subsystemen wurde merkber, oerstallige brûkersoperaasjes ferskine dy't de oerdracht fan gegevens fan it iene subsysteem nei it oare begjinne.

In oare oanpak is om yntegraasje-ynteraksjes te beskôgjen as in yntegraal diel fan 'e kearnbedriuwlogika en workflow. Om de feardigenseasken fan applikaasje-ûntwikkelders te hâlden fan skyrocketing, moatte it meitsjen fan nije yntegraasje-ynteraksjes maklik en natuerlik dien wurde, mei minimale opsjes foar it kiezen fan in oplossing. Dit is dreger dan it liket: it ark moat krêftich genôch wêze om de brûker de nedige ferskaat oan opsjes foar har gebrûk te jaan en tagelyk net yn 'e foet te sjitten. D'r binne in protte fragen dy't in yngenieur moat beäntwurdzje yn 'e kontekst fan yntegraasjetaken, mar dêr't in applikaasje-ûntwikkelder net oer moat tinke yn har deistich wurk: transaksjegrinzen, konsistinsje, atomiteit, feiligens, skaalfergrutting, ferdieling fan lading en boarnen, routing, marshaling, fuortplanting en switching konteksten, ensfh It is nedich om te bieden applikaasje ûntwikkelders frij ienfâldige beslút sjabloanen, dêr't antwurden op al sokke fragen binne al ferburgen. Dizze patroanen moatte feilich genôch wêze: de saaklike logika feroaret heul faak, wat it risiko fergruttet op it ynfieren fan flaters, de kosten fan flaters moatte op in frij leech nivo bliuwe.

Mar dochs, wat hat BPM dermei te krijen? D'r binne in protte opsjes foar it ymplementearjen fan workflow ...
Ja, in oare ymplemintaasje fan saaklike prosessen is heul populêr yn ús oplossingen - troch de deklarative ynstelling fan it steatstransysjediagram en it ferbinen fan handlers mei saaklike logika oan 'e transysjes. Tagelyk is de steat dy't de hjoeddeistige posysje fan it "dokumint" yn it bedriuwsproses bepaalt, in attribút fan it "dokumint" sels.

BPM styl yntegraasje
Dit is hoe't it proses derút sjocht by it begjin fan it projekt

De populariteit fan sa'n ymplemintaasje is te tankjen oan de relative ienfâld en snelheid fan it meitsjen fan lineêre saaklike prosessen. As softwaresystemen lykwols komplekser wurde, groeit it automatisearre diel fan it bedriuwsproses en wurdt komplekser. Der is ferlet fan ûntbining, wergebrûk fan dielen fan prosessen, lykas forking prosessen sadat elke tûke wurdt útfierd parallel. Under sokke betingsten wurdt it ark ûngemaklik, en it tastânstransysjediagram ferliest har ynformaasjeynhâld (yntegraasje-ynteraksjes wurde hielendal net yn it diagram wjerspegele).

BPM styl yntegraasje
Dit is hoe't it proses derút sjocht nei ferskate iteraasjes fan it ferdúdlikjen fan 'e easken

De wei út dizze situaasje wie de yntegraasje fan de motor jBPM yn guon produkten mei de meast komplekse saaklike prosessen. Op koarte termyn hie dizze oplossing wat súkses: it waard mooglik om komplekse saaklike prosessen út te fieren mei it behâld fan in frij ynformatyf en aktueel diagram yn 'e notaasje BPMN2.

BPM styl yntegraasje
In lyts part fan in kompleks saaklik proses

Op 'e lange termyn foldie de oplossing net oan' e ferwachtingen: de hege arbeidsintensiteit fan it meitsjen fan saaklike prosessen fia fisuele ark lieten net akseptabele produktiviteitsyndikatoaren berikke, en it ark sels waard ien fan 'e meast net leuk ûnder ûntwikkelders. Der wiene ek klachten oer de ynterne struktuer fan 'e motor, dy't late ta it ferskinen fan in protte "patches" en "krukken".

It wichtichste positive aspekt fan it brûken fan jBPM wie it realisearjen fan 'e foardielen en skea fan it hawwen fan in eigen oanhâldende steat foar in saaklike proses-eksimplaar. Wy seagen ek de mooglikheid om in prosesoanpak te brûken om komplekse yntegraasjeprotokollen út te fieren tusken ferskate applikaasjes mei asynchrone ynteraksjes fia sinjalen en berjochten. De oanwêzigens fan in oanhâldende steat spilet dêr in krúsjale rol yn.

Op grûn fan it boppesteande kinne wy ​​konkludearje: De prosesoanpak yn 'e BPM-styl lit ús in breed skala oan taken oplosse foar it automatisearjen fan hieltyd kompleksere saaklike prosessen, harmonieuze yntegraasjeaktiviteiten yn dizze prosessen passe en de mooglikheid behâlde om it ymplementearre proses visueel yn in passende notaasje wer te jaan.

Neidielen fan syngroane oproppen as in yntegraasjepatroan

Syngroane yntegraasje ferwiist nei de ienfâldichste blokkearjende oprop. Ien subsysteem fungearret as de serverkant en bleatret de API mei de winske metoade. In oar subsysteem fungearret as kliïntside en makket op it krekte momint in oprop mei de ferwachting fan in resultaat. Ofhinklik fan 'e arsjitektuer fan it systeem kinne de kliïnt- en serverkanten wurde hosted yn deselde applikaasje en proses, of yn ferskate. Yn it twadde gefal moatte jo wat ymplemintaasje fan RPC tapasse en de parameters en it resultaat fan 'e oprop gearstalle.

BPM styl yntegraasje

Sa'n yntegraasjepatroan hat in frij grutte set fan neidielen, mar it wurdt yn 'e praktyk in soad brûkt troch syn ienfâld. De snelheid fan ymplemintaasje boeit en makket dat jo it wer en wer tapasse yn 'e betingsten fan "baarnende" deadlines, it skriuwen fan de oplossing yn technyske skulden. Mar it bart ek dat sûnder ûnderfining ûntwikkelders it ûnbewust brûke, gewoan net de negative gefolgen realisearje.

Neist de meast foar de hân lizzende ferheging fan de ferbining fan subsystemen, binne d'r minder dúdlike problemen mei "fersprieden" en "útrekkenje" transaksjes. Yndied, as de saaklike logika feroarings makket, dan binne transaksjes ûnmisber, en transaksjes beskoattelje op har beurt bepaalde tapassingsboarnen beynfloede troch dizze wizigingen. Dat is, oant ien subsysteem wachtet op in antwurd fan in oar, sil it de transaksje net kinne foltôgje en slûzen loslitte. Dit fergruttet it risiko fan in ferskaat oan effekten signifikant:

  • systeemresponsiviteit is ferlern, brûkers wachtsje in lange tiid op antwurden op oanfragen;
  • de tsjinner hâldt oer it algemien op mei reagearjen op brûkersoanfragen fanwegen in oerrinnende threadpool: de measte triedden "steane" op it slot fan 'e boarne dy't beset is troch de transaksje;
  • deadlocks begjinne te ferskinen: de kâns fan har foarkommen is sterk ôfhinklik fan 'e doer fan transaksjes, it bedrach fan saaklike logika en slûzen belutsen by de transaksje;
  • transaksje timeout ferfal flaters ferskine;
  • de tsjinner "falt" op OutOfMemory as de taak fereasket it ferwurkjen en feroarjen fan grutte hoemannichten gegevens, en de oanwêzigens fan syngroane yntegraasjes makket it hiel lestich te splitsen de ferwurking yn "lichtere" transaksjes.

Ut in arsjitektoanysk eachpunt liedt it gebrûk fan blokkearjen fan petearen by yntegraasje ta in ferlies fan kwaliteitskontrôle fan yndividuele subsystemen: it is ûnmooglik om de kwaliteitsdoelen fan ien subsysteem te garandearjen yn isolaasje fan 'e kwaliteitsdoelen fan in oar subsysteem. As subsystemen wurde ûntwikkele troch ferskate teams, dit is in grut probleem.

Dingen wurde noch nijsgjirriger as de subsystemen dy't wurde yntegrearre binne yn ferskate applikaasjes en syngroane feroarings moatte wurde makke oan beide kanten. Hoe kinne jo dizze wizigingen transaksjoneel meitsje?

As feroarings wurde makke yn aparte transaksjes, dan sil robúste útsûndering ôfhanneling en kompensaasje moatte wurde levere, en dit folslein elimineert it wichtichste foardiel fan syngroane yntegraasjes - ienfâld.

Ferdielde transaksjes komme ek yn 't sin, mar wy brûke se net yn ús oplossingen: it is lestich om betrouberens te garandearjen.

"Saga" as in oplossing foar it probleem fan transaksjes

Mei de tanimmende populariteit fan mikrotsjinsten is d'r in tanimmende fraach nei Saga Pattern.

Dit patroan lost de boppesteande problemen fan lange transaksjes perfekt op, en wreidet ek de mooglikheden út om de steat fan it systeem te behearjen fan 'e kant fan' e saaklike logika: kompensaasje nei in mislearre transaksje kin it systeem net weromdraaie nei syn oarspronklike steat, mar in alternatyf leverje. gegevens ferwurkjen rûte. It lit jo ek net werhelje mei súkses foltôge gegevensferwurkingsstappen as jo besykje it proses nei in "goede" ein te bringen.

Nijsgjirrich, yn monolithic systemen, dit patroan is ek relevant as it giet om de yntegraasje fan los keppele subsystemen en der binne negative effekten feroarsake troch lange transaksjes en de oerienkommende boarne slûzen.

Wat ús saaklike prosessen yn 'e BPM-styl oanbelanget, blykt it hiel maklik om Sagas út te fieren: yndividuele stappen fan 'e Saga's kinne ynsteld wurde as aktiviteiten binnen it bedriuwsproses, en de oanhâldende steat fan it bedriuwsproses bepaalt û.o. , de ynterne steat fan 'e Saga's. Dat is, wy hawwe gjin ekstra koördinaasjemeganisme nedich. Alles wat jo nedich binne is in berjochtmakelaar mei stipe foar "op syn minst ien kear" garânsjes as ferfier.

Mar sa'n oplossing hat ek in eigen "priis":

  • saaklike logika wurdt komplekser: jo moatte kompensaasje útwurkje;
  • it sil nedich wêze om folsleine konsistinsje te ferlitten, dy't benammen gefoelich kin wêze foar monolityske systemen;
  • de arsjitektuer wurdt in bytsje mear yngewikkelder, der is in ekstra ferlet fan in berjocht makelder;
  • ekstra tafersjoch- en administraasjeynstruminten sille nedich wêze (hoewol't dit yn 't algemien sels goed is: de kwaliteit fan systeemtsjinst sil tanimme).

Foar monolithyske systemen is de rjochtfeardiging foar it brûken fan "Sags" net sa dúdlik. Foar mikrotsjinsten en oare SOA's, wêr't, nei alle gedachten, d'r al in makelder is, en folsleine konsistinsje waard opoffere oan it begjin fan it projekt, kinne de foardielen fan it brûken fan dit patroan de neidielen signifikant opwekke, benammen as d'r in handige API is by de bedriuw logika nivo.

Ynkapseling fan saaklike logika yn mikrotsjinsten

Doe't wy begon te eksperimintearjen mei mikrotsjinsten, ûntstie in ridlike fraach: wêr't de domeinbedriuwlogika yn relaasje ta de tsjinst dy't persistinsje fan domeingegevens leveret?

As jo ​​sjogge nei de arsjitektuer fan ferskate BPMS, kin it lykje ridlik te skieden saaklike logika fan persistinsje: meitsje in laach fan platfoarm en domein-ûnôfhinklike microservices dy't foarmje de omjouwing en kontener foar it útfieren fan domein saaklike logika, en regeljen domein gegevens persistinsje as in aparte laach fan hiel ienfâldige en lichtgewicht microservices. Saaklike prosessen yn dit gefal orkestreare de tsjinsten fan 'e persistinsjelaach.

BPM styl yntegraasje

Dizze oanpak hat in heul grut pluspunt: jo kinne de funksjonaliteit fan it platfoarm safolle ferheegje as jo wolle, en allinich de oerienkommende laach fan platfoarmmikrotsjinsten sil "fet" wurde fan dit. Saaklike prosessen fan elk domein krije fuortendaliks de kâns om de nije funksjonaliteit fan it platfoarm te brûken sa gau as it is bywurke.

In mear detaillearre stúdzje die bliken wichtige tekoartkommingen fan dizze oanpak:

  • in platfoarmtsjinst dy't de saaklike logika fan in protte domeinen tagelyk útfiert, draacht grutte risiko's as ien punt fan mislearring. Faak feroarings oan bedriuwslogika fergrutsje it risiko fan bugs dy't liede ta systeembrede mislearrings;
  • prestaasjesproblemen: saaklike logika wurket mei har gegevens fia in smel en trage ynterface:
    • de gegevens sille wer wurde marshalled en pompt troch it netwurk stack;
    • de domeintsjinst sil faaks mear gegevens weromjaan as de saaklike logika fereasket foar ferwurking, troch ûnfoldwaande queryparameterisaasjemooglikheden op it nivo fan 'e eksterne API fan' e tsjinst;
    • ferskate ûnôfhinklike stikken fan saaklike logika kinne kearen opnij fersykje deselde gegevens foar ferwurking (jo kinne mitigate dit probleem troch taheakjen sesje beantsjes dy't cache gegevens, mar dit fierder complicates de arsjitektuer en skept problemen fan gegevens frisheid en cache invalidation);
  • transaksjonele problemen:
    • saaklike prosessen mei oanhâldende steat opslein troch it platfoarm tsjinst binne ynkonsistent mei domein gegevens, en der binne gjin maklike manieren om te lossen dit probleem;
    • it ferpleatsen fan de slûs fan domeingegevens út 'e transaksje: as de domeinbedriuwlogika wizigingen moat meitsje, nei't earst de krektens fan' e eigentlike gegevens kontrolearre is, is it nedich om de mooglikheid fan in kompetitive feroaring yn 'e ferwurke gegevens út te sluten. Eksterne blokkearjen fan gegevens kin helpe om it probleem op te lossen, mar sa'n oplossing draacht ekstra risiko's en ferleget de algemiene betrouberens fan it systeem;
  • ekstra komplikaasjes by it bywurkjen: yn guon gefallen moatte jo de persistinsjetsjinst en saaklike logika syngroan of yn strikte folchoarder bywurkje.

Uteinlik moast ik werom nei de basis: ynkapselje domeingegevens en domeinbedriuwlogika yn ien mikrotsjinst. Dizze oanpak simplifies de belibbing fan de microservice as in yntegraal komponint yn it systeem en net jaan oanlieding ta de boppesteande problemen. Dit is ek net fergees:

  • API-standerdisearring is nedich foar ynteraksje mei saaklike logika (benammen om brûkersaktiviteiten te leverjen as ûnderdiel fan saaklike prosessen) en API-platfoarmtsjinsten; mear foarsichtich omtinken foar API feroarings, foarút en efterút komptabiliteit is nedich;
  • it is ferplichte om ekstra runtime-biblioteken ta te foegjen om it funksjonearjen fan 'e saaklike logika te garandearjen as ûnderdiel fan elke sa'n mikrotsjinst, en dit jout oanlieding ta nije easken foar sokke biblioteken: ljochtheid en in minimum fan transitive ôfhinklikens;
  • saaklike logika-ûntwikkelders moatte biblioteekferzjes byhâlde: as in mikrotsjinst foar in lange tiid net finalisearre is, dan sil it nei alle gedachten in ferâldere ferzje fan 'e bibleteken befetsje. Dit kin in ûnferwachte obstakel wêze foar it tafoegjen fan in nije funksje en kin fereaskje dat de âlde saaklike logika fan sa'n tsjinst wurdt migrearre nei nije ferzjes fan 'e bibleteken as d'r ynkompatibele feroarings wiene tusken ferzjes.

BPM styl yntegraasje

In laach fan platfoarm tsjinsten is ek oanwêzich yn sa'n arsjitektuer, mar dizze laach foarmet net langer in kontener foar it útfieren fan domein saaklike logika, mar allinnich syn omjouwing, it bieden fan auxiliary "platfoarm" funksjes. Sa'n laach is net allinich nedich om de ljochtheid fan domeinmikrotsjinsten te behâlden, mar ek om behear te sintralisearjen.

Bygelyks, brûkersaktiviteiten yn saaklike prosessen generearje taken. As jo ​​​​lykwols wurkje mei taken, moat de brûker taken sjen fan alle domeinen yn 'e algemiene list, wat betsjut dat d'r in passende taakregistraasjeplatfoarmtsjinst wêze moat, wiske fan domeinbedriuwlogika. It hâlden fan de ynkapseling fan saaklike logika yn dizze kontekst is frij problematysk, en dit is in oar kompromis fan dizze arsjitektuer.

Yntegraasje fan saaklike prosessen troch de eagen fan in applikaasjeûntwikkelder

Lykas hjirboppe al neamd, moat de applikaasje-ûntwikkelder wurde abstrahearre fan 'e technyske en technyske skaaimerken fan' e ymplemintaasje fan 'e ynteraksje fan ferskate applikaasjes om te kinnen rekkenje op goede ûntwikkelingsproduktiviteit.

Litte wy besykje in nochal lestich yntegraasjeprobleem op te lossen, spesjaal útfûn foar it artikel. Dit sil in "spultsje" taak wêze mei trije applikaasjes, wêrby't elk fan harren wat domeinnamme definiearret: "app1", "app2", "app3".

Binnen elke applikaasje wurde saaklike prosessen lansearre dy't begjinne te "baljen" fia de yntegraasjebus. Berjochten mei de namme "Bal" sille fungearje as de bal.

Rules of the game:

  • de earste spiler is de inisjatyfnimmer. Hy noeget oare spilers út foar it spul, begjint it spul en kin it op elk momint einigje;
  • oare spilers ferklearje har dielname oan it spultsje, "bekend meitsje" mei elkoar en de earste spiler;
  • nei ûntfangst fan de bal, de spiler kiest in oare dielnimmende spiler en jout de bal oan him. It totale oantal passaazjes wurdt teld;
  • eltse spiler hat "enerzjy", dy't ôfnimt mei eltse pass fan de bal troch dy spiler. As de enerzjy rint út, de spiler wurdt eliminearre út it spul, oankundigje harren pinsjoen;
  • as de spiler allinnich oerbliuwt, ferklearret hy daliks syn fertrek;
  • doe't alle spilers wurde eliminearre, de earste spiler ferklearret de ein fan it spul. As hy it spultsje earder ferliet, dan bliuwt it om it spultsje te folgjen om it te foltôgjen.

Om dit probleem op te lossen, sil ik ús DSL brûke foar saaklike prosessen, wêrtroch jo de logika yn Kotlin kompakt kinne beskriuwe, mei in minimum fan in boilerplate.

Yn 'e app1-applikaasje sil it saaklike proses fan' e earste spiler (hy is ek de inisjatyfnimmer fan it spultsje) wurkje:

klasse InitialPlayer

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

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

class PlayersList : ArrayList<PlayerInfo>()

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

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

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

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

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

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

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

Neist it útfieren fan saaklike logika kin de boppesteande koade in objektmodel fan in saaklik proses produsearje dat kin wurde visualisearre as in diagram. Wy hawwe de visualizer noch net ymplementearre, dus wy moasten wat tiid besteegje oan tekenjen (hjir haw ik de BPMN-notaasje wat ferienfâldige oangeande it gebrûk fan poarten om de konsistinsje fan it diagram te ferbetterjen mei de boppesteande koade):

BPM styl yntegraasje

app2 sil it bedriuwsproses fan in oare spiler omfetsje:

klasse RandomPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

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

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

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

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

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

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

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

Diagram:

BPM styl yntegraasje

Yn 'e app3-applikaasje sille wy de spiler in wat oars gedrach meitsje: ynstee fan willekeurich de folgjende spiler te kiezen, sil hy hannelje neffens it round-robin-algoritme:

klasse RoundRobinPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

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

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

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

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

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

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

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

Oars, it gedrach fan de spiler net ferskille fan de foarige, sadat it diagram net feroaret.

No hawwe wy in test nedich om it allegear út te fieren. Ik sil allinich de koade fan 'e test sels jaan, om it artikel net mei in boilerplate te rommeljen (yn feite haw ik de earder makke testomjouwing brûkt om de yntegraasje fan oare saaklike prosessen te testen):

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

Run de test, sjoch nei it log:

console útfier

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

Ut dit alles kinne ferskate wichtige konklúzjes lutsen wurde:

  • as de nedige ark beskikber binne, kinne applikaasje-ûntwikkelders yntegraasje-ynteraksjes meitsje tusken applikaasjes sûnder ôf te brekken fan saaklike logika;
  • de kompleksiteit (kompleksiteit) fan in yntegraasjetaak dy't technyske kompetinsjes fereasket, kin binnen it ramt ferburgen wurde as dy yn earste ynstânsje fêstlein is yn 'e arsjitektuer fan it ramt. De swierrichheid fan 'e taak (swierrichheid) kin net ferburgen wurde, sadat de oplossing foar in lestige taak yn' e koade sil lykje;
  • by it ûntwikkeljen fan yntegraasje logika, is it nedich om rekken hâlden wurde úteinlik gearhing en it gebrek oan linearizability fan de steat feroaring fan alle yntegraasje dielnimmers. Dit twingt ús om de logika te komplisearjen om it ûngefoelich te meitsjen foar de folchoarder wêryn eksterne eveneminten foarkomme. Yn ús foarbyld wurdt de spiler twongen om diel te nimmen oan it spultsje nei't hy syn útgong fan it spul oankundiget: oare spilers sille trochgean mei de bal nei him trochjaan oant de ynformaasje oer syn útgong berikt en wurdt ferwurke troch alle dielnimmers. Dizze logika folget net út 'e regels fan it spul en is in kompromisoplossing binnen it ramt fan' e keazen arsjitektuer.

Dan litte wy prate oer de ferskate subtiliteiten fan ús oplossing, kompromissen en oare punten.

Alle berjochten yn ien wachtrige

Alle yntegrearre applikaasjes wurkje mei ien yntegraasje bus, dat wurdt presintearre as in eksterne makelder, ien BPMQueue foar berjochten en ien BPMTopic ûnderwerp foar sinjalen (eveneminten). It trochjaan fan alle berjochten troch ien wachtrige is op himsels in kompromis. Op it nivo fan saaklike logika kinne jo no safolle nije soarten berjochten yntrodusearje as jo wolle sûnder feroaringen oan te meitsjen oan 'e systeemstruktuer. Dit is in wichtige ferienfâldiging, mar it hat bepaalde risiko's, dy't, yn 'e kontekst fan ús typyske taken, ús net sa wichtich like.

BPM styl yntegraasje

D'r is lykwols ien subtiliteit hjir: elke applikaasje filtert "syn" berjochten út 'e wachtrige by de yngong, troch de namme fan har domein. Ek kin it domein wurde oantsjutte yn 'e sinjalen, as jo de "omfang" fan it sinjaal moatte beheine ta ien applikaasje. Dit moat de bânbreedte fan de bus fergrutsje, mar de saaklike logika moat no operearje mei domeinnammen: ferplicht foar it adressearjen fan berjochten, winsklik foar sinjalen.

It garandearjen fan de betrouberens fan 'e yntegraasjebus

Betrouberens bestiet út ferskate dingen:

  • De keazen berjochtmakelaar is in kritysk ûnderdiel fan 'e arsjitektuer en in ienich punt fan mislearring: it moat genôch fouttolerant wêze. Jo moatte allinich tiid-teste ymplemintaasjes brûke mei goede stipe en in grutte mienskip;
  • it is nedich om hege beskikberens fan 'e berjochtmakelaar te garandearjen, wêrfoar it fysyk skieden wurde moat fan' e yntegreare applikaasjes (hege beskikberens fan applikaasjes mei tapaste saaklike logika is folle dreger en djoerder om te leverjen);
  • de makelder is ferplichte om "op syn minst ien kear" leveringsgarânsjes te jaan. Dit is in ferplichte eask foar betroubere wurking fan de yntegraasje bus. D'r is gjin ferlet fan garânsjes foar "krekt ien kear" nivo: saaklike prosessen binne meastentiids net gefoelich foar de werhelle komst fan berjochten of eveneminten, en yn spesjale taken wêr't dit wichtich is, is it makliker om ekstra kontrôles ta te foegjen oan bedriuwslogika dan om konstant te brûken earder "djoere" " garânsjes;
  • it ferstjoeren fan berjochten en sinjalen moatte wurde belutsen by in mienskiplike transaksje mei in feroaring yn 'e steat fan saaklike prosessen en domein gegevens. De foarkar opsje soe wêze om it patroan te brûken Transaksje Outbox, mar it sil fereaskje in ekstra tabel yn de databank en in estafette. Yn JEE-applikaasjes kin dit ferienfâldige wurde troch in lokale JTA-behearder te brûken, mar de ferbining mei de selekteare broker moat yn modus kinne wurkje XA;
  • hannelers fan ynkommende berjochten en eveneminten moatte ek wurkje mei de transaksje fan it feroarjen fan de steat fan it saaklike proses: as sa'n transaksje wurdt weromrôle, dan moat de ûntfangst fan it berjocht ek wurde annulearre;
  • berjochten dy't troch flaters net levere wurde koenen wurde opslein yn in aparte winkel D.L.Q. (Deade letterwachtrige). Om dit te dwaan, hawwe wy in aparte platfoarmmikroservice makke dy't sokke berjochten yn har opslach opslacht, se yndeksearret troch attributen (foar fluch groepearje en sykjen), en de API bleatsteld foar besjen, opnij ferstjoere nei it bestimmingsadres en berjochten wiskje. Systeembehearders kinne mei dizze tsjinst wurkje fia har webynterface;
  • yn 'e makelderynstellingen moatte jo it oantal werhellingen en fertragingen tusken leveringen oanpasse om de kâns te ferminderjen dat berjochten yn' e DLQ komme (it is hast ûnmooglik om de optimale parameters te berekkenjen, mar jo kinne empirysk hannelje en oanpasse tidens operaasje);
  • de DLQ-winkel moat kontinu kontrolearre wurde, en it monitoaringssysteem moat systeembehearders ynformearje, sadat se sa gau mooglik reagearje kinne as net-levere berjochten foarkomme. Dit sil ferminderje de "skea sône" fan in flater of saaklike logika flater;
  • de yntegraasjebus moat ûngefoelich wêze foar it tydlike ûntbrekken fan applikaasjes: ûnderwerpabonneminten moatte duorsum wêze, en de domeinnamme fan 'e applikaasje moat unyk wêze, sadat in oar net besykje syn berjocht út 'e wachtrige te ferwurkjen by it ûntbrekken fan 'e applikaasje.

Garandearjen fan threadfeiligens fan saaklike logika

Itselde eksimplaar fan in saaklik proses kin ferskate berjochten en eveneminten tagelyk ûntfange, wêrfan de ferwurking parallel begjint. Tagelyk, foar in applikaasje-ûntwikkelder, moat alles ienfâldich en thread-feilich wêze.

De proses bedriuwslogika ferwurket elk ekstern barren dat dit saaklike proses yndividueel beynfloedet. Dizze eveneminten kinne wêze:

  • lansearring fan in saaklike proses eksimplaar;
  • in brûker aksje relatearre oan in aktiviteit binnen in saaklik proses;
  • ûntfangst fan in berjocht of sinjaal dêr't in saaklike proses eksimplaar is ynskreaun;
  • ferrinnen fan de timer ynsteld troch de saaklike proses eksimplaar;
  • kontrôle aksje fia API (bgl. proses ôfbrekke).

Elk sa'n evenemint kin de steat fan in saaklike proses-eksimplaar feroarje: guon aktiviteiten kinne einigje en oaren begjinne, de wearden fan persistente eigenskippen kinne feroarje. It sluten fan elke aktiviteit kin resultearje yn it aktivearjen fan ien of mear fan 'e folgjende aktiviteiten. Dy kinne op syn beurt ophâlde te wachtsjen op oare eveneminten, of, as se gjin ekstra gegevens nedich binne, kinne se yn deselde transaksje foltôgje. Foar it sluten fan de transaksje wurdt de nije steat fan it bedriuwsproses opslein yn 'e databank, wêr't it sil wachtsje op it folgjende eksterne barren.

Persistente saaklike prosesgegevens opslein yn in relationele databank is in heul handich ferwurkingssyngronisaasjepunt by it brûken fan SELECT FOR UPDATE. As ien transaksje it slagge om de steat fan it bedriuwsproses fan 'e database te krijen om it te feroarjen, dan sil gjin oare transaksje yn parallel deselde steat kinne krije foar in oare feroaring, en nei it foltôgjen fan' e earste transaksje is de twadde garandearre te ûntfangen de al feroare steat.

Mei help fan pessimistyske slûzen oan de DBMS kant, wy foldogge oan alle nedige easken acid, en behâlde ek de mooglikheid om de applikaasje te skaaljen mei saaklike logika troch it fergrutsjen fan it oantal rinnende eksimplaren.

Pessimistyske slûzen driigje ús lykwols mei deadlocks, wat betsjut dat SELECT FOR UPDATE noch moat wurde beheind ta wat ridlike time-out yn gefal fan deadlocks op guon ûngewoane gefallen yn bedriuwslogika.

In oar probleem is de syngronisaasje fan it begjin fan it bedriuwsproses. Wylst d'r gjin saaklike proses is, is d'r ek gjin steat yn 'e databank, dus de beskreaune metoade sil net wurkje. As jo ​​de unykheid fan in saaklike proses-eksimplaar yn in bepaalde omfang garandearje wolle, dan hawwe jo in soarte fan syngronisaasjeobjekt nedich ferbûn mei de prosesklasse en de oerienkommende omfang. Om dit probleem op te lossen, brûke wy in oar slotmeganisme wêrmei wy in slot kinne nimme op in willekeurige boarne oantsjutte troch in kaai yn URI-formaat fia in eksterne tsjinst.

Yn ús foarbylden befettet it bedriuwsproses fan InitialPlayer in ferklearring

uniqueConstraint = UniqueConstraints.singleton

Dêrom befettet it log berjochten oer it nimmen en loslitten fan it slot fan 'e oerienkommende kaai. D'r binne gjin sokke berjochten foar oare saaklike prosessen: uniqueConstraint is net ynsteld.

Bedriuwsprosesproblemen mei oanhâldende steat

Soms helpt it hawwen fan in oanhâldende steat net allinich, mar hinderet ek de ûntwikkeling echt.
Problemen begjinne as jo wizigingen moatte meitsje oan 'e bedriuwslogika en / of bedriuwsprosesmodel. Net sa'n feroaring wurdt fûn kompatibel te wêzen mei de âlde steat fan 'e saaklike prosessen. As d'r in protte "live" eksimplaren binne yn 'e databank, dan kin it meitsjen fan ynkompatibele wizigingen in protte problemen feroarsaakje, dy't wy faaks tsjinkamen by it brûken fan jBPM.

Ofhinklik fan 'e djipte fan feroaring kinne jo op twa manieren hannelje:

  1. meitsje in nij bedriuwsprosestype om net ynkompatibele feroaringen te meitsjen oan 'e âlde, en brûk it ynstee fan' e âlde by it begjinnen fan nije eksimplaren. Alde eksimplaren sille "de âlde manier" wurkje;
  2. migrearje de oanhâldende steat fan saaklike prosessen by it bywurkjen fan saaklike logika.

De earste manier is ienfâldiger, mar hat syn beheiningen en neidielen, bygelyks:

  • duplikaasje fan saaklike logika yn in protte saaklike prosesmodellen, in tanimming fan it folume fan bedriuwslogika;
  • faak is in direkte oergong nei in nije bedriuwslogika nedich (hast altyd yn termen fan yntegraasjetaken);
  • de ûntwikkelder wit net op hokker punt it mooglik is om ferâldere modellen te wiskjen.

Yn 'e praktyk brûke wy beide oanpakken, mar hawwe in oantal besluten makke om ús libben te ferienfâldigjen:

  • yn 'e databank wurdt de oanhâldende steat fan it saaklike proses opslein yn in maklik lêsbere en maklik ferwurke foarm: yn in JSON-formaatstring. Hjirmei kinne jo migraasjes útfiere sawol binnen de applikaasje as bûten. Yn ekstreme gefallen kinne jo it ek oanpasse mei handgrepen (benammen nuttich yn ûntwikkeling by debuggen);
  • de yntegraasje bedriuwslogika brûkt de nammen fan bedriuwsprosessen net, sadat it op elk momint mooglik is om de ymplemintaasje fan ien fan 'e dielnimmende prosessen te ferfangen troch in nije, mei in nije namme (bygelyks "InitialPlayerV2"). De bining komt foar troch de nammen fan berjochten en sinjalen;
  • it proses model hat in ferzje nûmer, dat wy increment as wy meitsje ynkompatibele feroarings oan dit model, en dit nûmer wurdt opslein tegearre mei de steat fan it proses eksimplaar;
  • de oanhâldende steat fan it proses wurdt lêzen fan de basis earst yn in handich foarwerp model dat de migraasje proseduere kin wurkje mei as it ferzjenûmer fan it model is feroare;
  • de migraasjeproseduere wurdt neist de saaklike logika pleatst en wurdt "lui" neamd foar elke eksimplaar fan it bedriuwsproses op it momint fan syn restauraasje út 'e databank;
  • as jo moatte migrearje de steat fan alle proses eksimplaren fluch en syngroane, mear klassike database migraasje oplossings wurde brûkt, mar jo moatte wurkje mei JSON dêr.

Haw ik in oar ramt nedich foar saaklike prosessen?

De oplossingen beskreaun yn it artikel lieten ús ús libben signifikant ferienfâldigje, it oanbod fan problemen oplost op it nivo fan applikaasjeûntwikkeling útwreidzje, en it idee fan it skieden fan saaklike logika yn mikrotsjinsten oantrekliker meitsje. Hjirfoar is in protte wurk dien, in heul "lichtgewicht" ramt foar saaklike prosessen is makke, lykas tsjinstkomponinten foar it oplossen fan 'e identifisearre problemen yn' e kontekst fan in breed skala oan tapaste taken. Wy hawwe in winsk om dizze resultaten te dielen, om de ûntwikkeling fan mienskiplike komponinten yn iepen tagong te bringen ûnder in fergese lisinsje. Dit sil wat muoite en tiid fereaskje. De fraach nei sokke oplossingen begripe kin in ekstra stimulâns foar ús wêze. Yn it foarstelde artikel wurdt hiel lyts omtinken jûn oan de mooglikheden fan it ramt sels, mar guon fan harren binne sichtber út de presintearre foarbylden. As wy ús ramt dochs publisearje, wurdt der in apart artikel oan wijd. Yn 'e tuskentiid sille wy tankber wêze as jo in bytsje feedback litte troch de fraach te beantwurdzjen:

Allinnich registrearre brûkers kinne meidwaan oan 'e enkête. Ynlogge, asjebleaft.

Haw ik in oar ramt nedich foar saaklike prosessen?

  • 18,8%Ja, ik ha al lang nei sokssawat socht.

  • 12,5%it is nijsgjirrich om mear te learen oer jo ymplemintaasje, it kin nuttich wêze2

  • 6,2%wy brûke ien fan 'e besteande kaders, mar wy tinke oer it ferfangen1

  • 18,8%wy brûke ien fan 'e besteande kaders, alles past3

  • 18,8%omgean sûnder ramt3

  • 25,0%skriuw jo eigen4

16 brûkers stimden. 7 brûkers ûntholden har.

Boarne: www.habr.com

Add a comment