BPM-stiilis integreerimine

BPM-stiilis integreerimine

Tere, Habr!

Meie ettevĂ”te on spetsialiseerunud ERP-taseme tarkvaralahenduste arendamisele, mille suur osa koosneb suurtest tehingusĂŒsteemidest, kus on palju Ă€riloogikat ja dokumendihaldust nagu EDD. Meie tooteid toetavad kaasaegsed JavaEE tehnoloogiad, kuid katsetame aktiivselt ka mikroteenuseid. Üks suurimaid probleemkohti sellistes lahendustes on erinevate all sĂŒsteemide integreerimine, mis kuuluvad ĂŒksteisega seotud domeenidesse. Integreerimise ĂŒlesanded on alati valmistanud meile suurt peavalu, olenemata kasutatud arhitektuuristiilidest, tehnoloogiakogumitest ja raamistikest, kuid viimasel ajal on selliste probleemide lahendamisel toimunud edusamme.

Pakutavas artiklis tutvustan NPO "Krista" kokemusi ja arhitektuurilisi vÀljakutseid mainitud valdkonnas. Samuti vaatame, kuidas lahendada lihtsat integreerimisteemat rakendus arendaja vaatenurgast ja saame teada, mis selle lihtsuse taga peitub.

MĂ€rkus

Artiklis kirjeldatud arhitektuuri- ja tehnilised lahendused pĂ”hinevad minu isiklikul kogemusel konkreetsete ĂŒlesannete kontekstis. Need lahendused ei pretendeeri universaalsusele ja vĂ”ivad osutuda ebasobivateks teistes kasutustingimustes.

Kuidas siin BPMiga seondub?

Sellele kĂŒsimusele vastamiseks tuleb pisut sĂŒveneda meie lahenduste rakenduslike ĂŒlesannete spetsiifikat. Suur osa Ă€riloogikast meie tĂŒĂŒpilises tehingusĂŒsteemis hĂ”lmab andmete sisestamist Andmebaasi kasutajaliideste kaudu, nende andmete kĂ€sitsi ja automatiseeritud kontrollimist, nende lĂ€biviimist mingisuguste töövoogude kaudu, nende avaldamist teise sĂŒsteemi / analĂŒĂŒsibaasi / arhiivi, aruannete koostamist. Seega on sĂŒsteemi vĂ”tme funktsioon klientide jaoks nende sise Ă€ri protsesside automatiseerimine.

Meie mugavuse huvides kasutame kommunikatsioonis mĂ”istet "dokument" kui mingit abstraktsiooni andmekogumist, mis on seotud ĂŒhise vĂ”tmega, millele saab "siduda" teatud töövoo.
Kuid mis siis integratsiooniloogikaga? LĂ”ppude lĂ”puks esindab integreerimise ĂŒlesanne sĂŒsteemi arhitektuuri, mis on jagatud osadeks mitte kliendi nĂ”udmiste jĂ€rgi, vaid mĂ”jutatud tĂ€iesti muudest teguritest:

  • Conway seaduse mĂ”jul;
  • korduvkasutuse tulemusena, varasemalt teistele toodetele vĂ€lja töötatud alamsĂŒsteemide;
  • arhitekti otsusel, lĂ€htudes mittefunktsionaalsetest nĂ”uetest.

On suur kiusatus eraldada integreerimisloogika peamise töövoo Ă€ri loogikast, et mitte saastada Ă€ri loogikat integreerimisartefaktide poolt ning vabastada arendaja vajadusest sĂŒveneda sĂŒsteemi arhitektuuri eripĂ€radesse. Sellel lĂ€henemisel on mitmeid eeliseid, kuid praktika nĂ€itab selle ebaefektiivsust:

  • integreerimisprobleemide lahendamine jookseb tavaliselt kĂ”ige lihtsamateni, milleks on sĂŒnkroonsed kutsed, tulenevalt peamise töövoo piiratud laienduspunktidest (sĂŒnkroonse integreerimise puudustest – veidi allpool);
  • integreerimisartefaktid tungivad ikkagi peamisse Ă€ri loogikasse, kui on vajalik tagasiside teisest alamsĂŒsteemist;
  • arendaja ignoreerib integreerimist ja vĂ”ib selle kergesti rikkuda, muutes töövoogu;
  • sĂŒsteem lĂ”petab kasutaja silmis ĂŒhtsena eksisteerimise, alamsĂŒsteemide vahel on mĂ€rgatavad "Ă”mblused", millega kaasnevad liigsed kasutaja toimingud, mis algatavad andmete edastamise ĂŒhest alamsĂŒsteemist teise.

Teine lĂ€henemine on vaadata integreerimise koostoimeid kui lahutamatut osa peamisest Ă€riloogikast ja töövoost. Et rakendusarendajate kvalifikatsiooninĂ”uded ei tĂ”useks taevasse, peaks uute integreerimisvĂ”imaluste loomine toimuma kergelt ja vabalt, minimaalsete lahenduste valimise vĂ”imalustega. Seda on keerulisem saavutada, kui tundub: tööriist peab olema piisavalt vĂ”imas, et anda kasutajale vajalik hulk rakendamise vĂ”imalusi, kuid samas ei luba endale „ennast jalga tulistada“. On palju kĂŒsimusi, millele insener peab vastama integreerimise ĂŒlesannete kontekstis, kuid mille ĂŒle ei pea rakendusarendaja igapĂ€evases töös mĂ”tlema: tehingute piirid, jĂ€rjepidevus, atomaarne olemus, turvalisus, skaleerimine, koormuste ja ressursside jaotamine, marsruutimine, marsaldamine, kontekstide levitamine ja vahetamine jne. Tuleb pakkuda rakendusarendajatele piisavalt lihtsaid lahenduste malli, kuhu on juba peidetud vastused kĂ”ikidele sellistele kĂŒsimustele. Need mallid peavad olema piisavalt turvalised: Ă€riloogika muutub vĂ€ga sageli, mis suurendab vigade toimumise riske, vigade hind peab jÀÀma piisavalt madalaks.

Aga mis on BPMi seos sellega? Ju on palju erinevaid töövoo rakendamise vĂ”imalusi

TĂ”epoolest, meie lahendustes on vĂ€ga populaarne teine Ă€ri­protsesside rakendamise versioon – lĂ€bi deklaratiivse olekute ĂŒlemineku diagrammi mÀÀratlemise ja Ă€riloogika töötlejate ĂŒhendamise ĂŒleminekutele. Sellega mÀÀrab olek, mis mÀÀrab „dokumendi” praeguse positsiooni Ă€ri­protsessis, ise „dokumendi” atribuudi.

BPM-stiilis integreerimine
Nii nÀeb protsess vÀlja projekti alguses

Selle rakenduse populaarsus tuleneb suhtelisest lihtsusest ja kiirusest, millega saab luua lineaarseid Ă€riprotsesse. Kuid programmide sĂŒsteemide pideva keerukuse kasvades muutub automatiseeritud osa Ă€riprotsessist mahukamaks ja keerukamaks. Tekkib vajadus dekompositsiooni, protsessi osade uuesti kasutamise ning protsesside harude jagamise jĂ€rele, et iga haru saaks tĂ€idetud samaaegselt. Sellega seoses muutub tööriist ebamugavaks ning olekute diagramm kaotab oma informatiivsuse (integreerimisseosed ei kajastu ĂŒldse diagrammil).

BPM-stiilis integreerimine
Nii nÀeb protsess vÀlja pÀrast mitmeid iteratsioone nÔudmiste tÀpsustamiseks

VĂ€ljapÀÀs sellest olukorrast oli mootorite integreerimine jBPM teatud toodetesse, kus on kĂ”ige keerukamad Ă€riprotsessid. LĂŒhiajalises perspektiivis oli see lahendus teatud edu saavutanud: tekkis vĂ”imalus keerukate Ă€riprotsesside elluviimiseks, sĂ€ilitades samal ajal piisavalt informatiivse ja ajakohase diagrammi BPMN2 tĂ€histuses. VĂ€ike osa keerukast Ă€riprotsessist.

BPM-stiilis integreerimine
Pikaajalises perspektiivis ei Ă”igustanud lahendus ootusi: Ă€riprotsesside loomise kĂ”rge töömahukus visuaalsete tööriistade kaudu ei vĂ”imaldanud saavutada vastuvĂ”etavaid tootlikkuse nĂ€itajaid ning see tööriist sai ĂŒheks kĂ”ige vĂ€hem armastatuks arendajate seas. Ka mootorite sisemise struktuuri kohta oli kriitikat, mis viis mitmesuguste "plekkide" ja "toega" tekkimiseni.

jBPM rakendamise peamine positiivne aspekt oli teadlikkus oma pĂŒsiva seisundi olemasolu kasust ja kahjust Ă€riprotsessi eksemplaris. Samuti nĂ€gime vĂ”imalust kasutada protsessipĂ”hist lĂ€henemist keerukate integreerimisprotokollide elluviimiseks erinevate rakenduste vahel, rakendades asĂŒnkroonseid suhtlusi mĂ€rkide ja sĂ”numite kaudu. PĂŒsiva seisundi olemasolu mĂ€ngib selles olulist rolli.

KÀesoleva teabe pÔhjal vÔib jÀreldada:

protsessipĂ”hine lĂ€henemine BPM-i stiilis vĂ”imaldab meil lahendada laia valikut ĂŒlesandeid, et automatiseerida pidevalt keerukamaid Ă€riprotsesse, harmooniliselt integreerida nendesse protsessidesse integratsioonitegevusi ning sĂ€ilitada vĂ”imalus visuaalselt esitada ellu viidud protsessi sobivas sĂŒmboolikas. protsessipĂ”hine lĂ€henemine BPM stiilis vĂ”imaldab meil lahendada laia valikut keerukate Ă€rioperatsioonide automatiseerimise ĂŒlesandeid, harmooniliselt integreerida neid protsesse integreerimisaktiivsustega ning sĂ€ilitada vĂ”imaluse visualiseerida teostatud protsessi sobivas notationis.

SĂŒnkroonsed kutsed kui integratsioonimuster

SĂŒnkroonne integreerimine tĂ€histab lihtsaimat blokeerivat kutset. Üks alam sĂŒsteem tegutseb serveripoolena ja pakub vajaliku meetodi API-d. Teine alam sĂŒsteem tegutseb kliendipoolena ning teeb kutse Ă”igele hetkele oodates tulemusi. SĂ”ltuvalt sĂŒsteemi arhitektuurist vĂ”ivad kliendi ja serveri pooled asuda kas ĂŒhes rakenduses ja protsessis vĂ”i erinevates. Teisel juhul on vajalik rakendada teatud RPC teostust ja tagada kutse parameetrite ja tulemuste marshalimine.

BPM-stiilis integreerimine

Sellel integratsioonimustril on piisavalt suur puuduste kogum, kuid seda kasutatakse praktikas vĂ€ga laialdaselt oma lihtsuse tĂ”ttu. Teostuse kiirus on ahvatlev ja sunnib seda jĂ€lle ja jĂ€lle rakendama „pĂ”letavate” tĂ€htaegade korral, kirjutades lahenduse tehnilisse vĂ”lga. Kuid on ka juhtumeid, kui kogenematud arendajad rakendavad seda alateadlikult, lihtsalt ei mĂ”istes negatiivseid tagajĂ€rgi.

Peale kĂ”ige ilmsemate alamsĂŒsteemide sidususe suurenemise on ka vĂ€hem ilmsed probleemid „venitamise” ja „lĂ”hkumise” tehingutega. TĂ”epoolest, kui Ă€riloogika teeb mingisuguseid muudatusi, siis ei saa tehingutest mööda hiilida, ja tehingud omakorda blokeerivad teatud rakenduse ressursse, mis on nende muudatustega seotud. See tĂ€hendab, et kuni ĂŒks alam sĂŒsteem ei saa teisest vastust, ei saa ta tehingut lĂ”petada ja blokeeringut eemaldada. See suurendab oluliselt erinevate efektide tekkimise riski:

  • sĂŒsteemi reageerimine kaob, kasutajad ootavad pikka aega vastuseid pĂ€ringutele;
  • server lĂ”petab tĂ€ielikult vastamise kasutajate pĂ€ringutele, kuna voolu bassein on ĂŒletĂ€idetud: enamus voole on „seiskunud” tehingu tĂ”ttu blokeeritud ressursil;
  • ilmnevad deadlockid: nende ilmumise tĂ”enĂ€osus sĂ”ltub tugevalt tehingute kestusest, tehingusse kaasatud Ă€riloogika hulgast ja blokeeringutest;
  • ilmnevad tehingu ajaĂŒlekande vead;
  • server „kukub” OutOfMemory tĂ”ttu, kui ĂŒlesanne nĂ”uab suurte andmemahtude töötlemist ja muutmist, ning sĂŒnkroonsete integreerimiste olemasolu raskendab oluliselt töötlemise jagamist „kergematele” tehingutele.

Arhitektuuri seisukohalt toob blokeerivate vĂ€ljakutsete kasutamine integreerimisel kaasa ĂŒksikute alam-sĂŒsteemide kvaliteedi juhtimise kaotamise: on vĂ”imatu tagada ĂŒhe alam-sĂŒsteemi kvaliteedi sihttasemeid, kui need ei ole seotud teiste alam-sĂŒsteemide kvaliteedi nĂ€itajatega. Kui alam-sĂŒsteeme arendavad erinevad meeskonnad, on see suur probleem.

Asjad muutuvad veelgi huvitavamaks, kui integreeritavad alam-sĂŒsteemid asuvad erinevates rakendustes ja peab ĂŒheaegselt tegema muudatusi mĂ”lemalt poolt. Kuidas tagada nende muudatuste tehingulisus?

Kui muudatusi tehakse eraldi tehingutena, tuleb tagada usaldusvÀÀrne erandite töötlemine ja kompensatsioon, mis kaotab tĂ€ielikult sĂŒnkroonsete integratsioonide pĂ”hieelise – lihtsuse.

PÀhe tulevad ka jaotatud tehingud, kuid me ei kasuta neid oma lahendustes: usaldusvÀÀrsuse tagamine on keeruline.

„Saga” kui lahendus tehingute probleemile

Mikroteenuste populaarsuse suurenemisega kasvab ka Saga Muster.

See muster lahendab hĂ€sti eespool mainitud pikaajaliste tehingute probleemid, samuti laiendab sĂŒsteemi oleku haldamise vĂ”imalusi Ă€ri loogika poolt: ebaĂ”nnestunud tehingu jĂ€relkompensatsioon ei pea sĂŒsteemi algseisundisse tagasi viima, vaid vĂ”ib tagada alternatiivse andmete töötlemise marsruudi. See vĂ”imaldab ka edukalt lĂ”petatud andmete töötlemise samme mitte korrata, kui protsessi pĂŒĂŒda viia „head” lĂ”pule.

Huvitav on see, et monoliitsetes sĂŒsteemides on see muster samuti aktuaalne, kui tegemist on nĂ”rgalt seotud alam-sĂŒsteemide integreerimisega ning pikaajaliste tehingute ja vastavate ressursside blokeeringute negatiivsed mĂ”jud on tĂ”eliselt tuntavad.

Seoses meie BPM-tĂŒĂŒpi Ă€riprotsessidega on „Sagate” rakendamine vĂ€ga lihtne: eraldi „Saga” etapid vĂ”ivad olla mÀÀratud tegevustena Ă€riprotsessis ning Ă€riprotsessi pĂŒsiv olek mÀÀrab muu hulgas „Saga” sisemise oleku. See tĂ€hendab, et meil ei ole vaja tĂ€iendavat koordineerimismehhanismi. Vajalik on vaid sĂ”numi vaheline broker, millel on „at least once” garantiide tugi transpordina.

Kuid isegi sellel lahendusel on oma „hind”:

  • Ă€ri loogika muutub keerulisemaks: tuleb töötada hĂŒvitistega;
  • on vajalik loobuda tĂ€ielikust jĂ€rjepidevusest, mis vĂ”ib olla eriti tundlik monoliitsetes sĂŒsteemides;
  • arhitektuur muutub veidi keerulisemaks, tekib tĂ€iendav vajadus sĂ”numite brokera jĂ€rele;
  • vajalikud lisamonitorimise ja haldamise vahendid (kuigi ĂŒldiselt on see isegi hea: teenuse kvaliteet paranee).

Monoliitsete sĂŒsteemide puhul ei ole «Saga» kasutamise Ă”igustus nii ilmne. Mikroteenuste ja teiste SOA-de puhul, kus tĂ”enĂ€oliselt on juba olemas broker ning tĂ€ielik jĂ€rjepidevus on ohverdatud projekti alguses, vĂ”ib selle mustri kasutamisest saadav kasu oluliselt ĂŒletada puudusi, eriti kui olemas on mugav API Ă€ri loogika tasandil.

Äri loogika kapseldamine mikroteenustes

Kui me hakkasime katsetama mikroteenustega, tekkis Ă”igustatud kĂŒsimus: kuhu paigutada domeeni Ă€ri loogika seoses teenusega, mis tagab domeeni andmete pĂŒsimise?

Vaadates erinevate BPMS-ide arhitektuuri, vĂ”ib tunduda mĂ”istlik eraldada Ă€ri loogika pĂŒsimisest: luua platvormi ja domeeni sĂ”ltumatute mikroteenuste kiht, mis moodustab keskkonna ja konteineri domeeni Ă€ri loogika tĂ€itmiseks, ning korraldada domeeni andmete pĂŒsimine eraldi vĂ€ga lihtsa ja kerge mikroteenuste kihina. Sellisel juhul teevad Ă€ri protsessid teenuste orkestrimise pĂŒsimise kihile.

BPM-stiilis integreerimine

Sellel lĂ€henemisel on vĂ€ga suur eelis: platvormi funktsionaalsust saab lĂ”putult kasvatada, ja «pakseneb» sellest ainult vastav platvormi mikroteenuste kiht. Äri protsessid mis tahes domeenist saavad kohe kasutada platvormi uut funktsionaalsust, niipea kui see on uuendatud.

Rohkem detailsed analĂŒĂŒsid nĂ€itasid sellise lĂ€henemise olulisi puudusi:

  • platvormi teenus, mis tĂ€idab Ă€ri loogikat paljude domeenide jĂ€rele, kannab endas suuri riske kui ainus rikke punkt. Äri loogika sagedased muutused suurendavad vigade tekkimise riski, mis vĂ”ivad pĂ”hjustada sĂŒsteemi ĂŒle katkestusi;
  • jĂ”udlusprobleemid: Ă€ri loogika töötab oma andmetega lĂ€bi kitsaste ja aeglaste liideste:
    • andmed marsalitakse ja edastatakse veel kord lĂ€bi vĂ”rgu stacki;
    • domeeniteenused annavad sageli rohkem andmeid, kui Ă€riloogika töötlemiseks vajalik, kuna vĂ€lise API teenuse pĂ€ringute parameetrite vĂ”imalused on piiratud;
    • mitmed sĂ”ltumatud Ă€riloogika osad vĂ”ivad samaid andmeid töötlemiseks korduvalt kĂŒsida (seda probleemi saab leevendada lisades seansikomponente, mis ĐșĐ”ŃˆĐžŃ€ŃƒŃŽŃ‚ andmeid, kuid see keerukalt keerab arhitektuuri ja loob andmete aktuaalsuse ja vahemĂ€lu kehtetuks muutumise probleeme);
  • tehingute probleemid:
    • Ă€riprotsessid pĂŒsiva olekuga, mille salvestamise eest vastutab platvormiteenus, on kooskĂ”lastamatud domeeniandmetega, ja lihtsaid lahendusi sellele probleemile ei paista;
    • domeeniandmete lukustamise vĂ€ljaviimine tehingust: kui domeeni Ă€riloogika vajab muudatusi, tuleb enne kontrollida aktuaalsete andmete kehtivust, et vĂ€listada samaaegne muutmine töödeldud andmete. VĂ€liseks andmete lukustamiseks lahendus vĂ”ib aidata probleemi lahendada, kuid see lahendus toob endaga kaasa tĂ€iendavaid riske ja vĂ€hendab sĂŒsteemi ĂŒldist usaldusvÀÀrsust;
  • tĂ€iendavad keerukused uuendamisel: mĂ”ningatel juhtudel tuleb sĂ€ilitamise ja Ă€riloogika teenuseid sĂŒnkroonselt vĂ”i rangelt jĂ€rjestikku uuendada.

LĂ”puks tuli tagasi algusesse: kapseldada domeeniandmed ja domeeni Ă€riloogika ĂŒhte mikrosĂŒsteemi. See lĂ€henemine lihtsustab mikrosĂŒsteemi tajumist sĂŒsteemi tervikliku koostisosana ja ei tekita eespool mainitud probleeme. See ei ole samuti tasuta:

  • vajatakse API standardiseerimist Ă€riloogikaga suhtlemiseks (eriti kasutajate tegevuste tagamiseks Ă€riprotsesside raames) ja platvormiteenuste API-d; on vajalik tĂ€helepanelikum suhtumine API muutmise, otse- ja tagasipöördumise ĂŒhilduvusse;
  • vajatakse tĂ€iendavate runtime-teekide lisamist Ă€ri loogika toetamiseks igas sellises mikrosĂŒsteemis, mis toob kaasa uued nĂ”uded nendele teekidele: kerge kaal ja minimaalsete ĂŒleminevate sĂ”ltuvuste olemasolu;
  • Ärimeetodite arendajad peavad jĂ€lgima raamatute versioone: kui mĂ”nda mikroteenust ei ole pikka aega arendatud, siis on tĂ”enĂ€oliselt sellel raamatute vanem versioon. See vĂ”ib muutuda ootamatuks takistuseks uue funktsiooni lisamisel ja vĂ”ib nĂ”uda vanade Ă€riideede migreerimist teenuse uutele raamatute versioonidele, kui versioonide vahel on olnud ĂŒhilduvuse muutusi.

BPM-stiilis integreerimine

Selles arhitektuuris on ka platvormiteenuste kiht, kuid see kiht ei moodusta domeeni Àriideede tÀitmise konteinerit, vaid vaid selle keskkonna, pakkudes abifunktsioone. Selline kiht on vajalik mitte ainult domeeni mikroteenuste kerguse sÀilitamiseks, vaid ka juhtimise tsentraliseerimiseks.

NĂ€iteks kasutaja tegevused Ă€ri protsessides genereerivad ĂŒlesandeid. Kuid töötades ĂŒlesannetega, peab kasutaja nĂ€gema ĂŒlesandeid kĂ”igist domeenidest ĂŒldloendis, seega peab olema vastav platvormiteenus ĂŒlesannete registreerimiseks, mis on puhastatud domeeni Ă€riideedest. Äriideede kapseldamise sĂ€ilitamine sellises kontekstis on ĂŒsna keeruline ja see on veel ĂŒks kompromiss selle arhitektuuri puhul.

Äriprotsesside integreerimine rakenduse arendaja silmade lĂ€bi

Nagu eelnevalt mainitud, peab rakenduse arendaja olema abstraktne tehniliste ja inseneritehniliste detailide suhtes, et loota heale arendustootlikkusele.

Proovime lahendada ĂŒsna keerulise integreerimisteema, mis on spetsiaalselt vĂ€lja mĂ”eldud artikli jaoks. See on 'mĂ€nguline' ĂŒlesanne, kus osalevad kolm rakendust, millest igaĂŒhel on oma domeeninimi: 'app1', 'app2', 'app3'.

Iga rakenduse sees kÀivitatakse Àri protsessid, mis hakkavad 'palliga mÀngima' integreerimisbusse kaudu. Palli rollis on sÔnumid nimega 'Ball'.

MĂ€ngureeglid:

  • esimene mĂ€ngija – algataja. Ta kutsub teisi mĂ€ngijaid mĂ€ngu, alustab mĂ€ngu ja vĂ”ib seda igal ajal lĂ”petada;
  • teised mĂ€ngijad kuulutavad oma osalemisest mĂ€ngus, 'tutvuvad' ĂŒksteisega ja esimese mĂ€ngijaga;
  • palli vastu vĂ”tnud mĂ€ngija valib teise osaleva mĂ€ngija ja annab talle palli edasi. Peetakse arvestust ĂŒldiste edastuste arvu ĂŒle;
  • iga mĂ€ngijal on "energia", mis vĂ€heneb iga kord, kui see mĂ€ngija palli edasi annab. Kui energia otsa saab, loobub mĂ€ngija mĂ€ngust, tehes selle teatavaks;
  • kui mĂ€ngija jÀÀb ĂŒksi, teatab ta kohe loobumisest;
  • kui kĂ”ik mĂ€ngijad on mĂ€ngust eemaldunud, teatab esimene mĂ€ngija mĂ€ngu lĂ”petamisest. Kui ta lahkus varem, peab ta mĂ€ngu jĂ€lgima, et seda lĂ”petada.

Selle ĂŒlesande lahendamiseks kasutan meie Ă€riprotsesside DSL-i, mis vĂ”imaldab loogikat Kotlinis kompaktse ja minimaalse boilerplate'iga kirjeldada.

rakenduses app1 töötab esimese mÀngija Àri protsess (kes on mÀngu algataja):

class InitialPlayer

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

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

class PlayersList : ArrayList()

// See klassi protsessi eksemplar: kapseldab selle siseolekud
class InitialPlayer : ProcessImpl(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// See on protsessi mudeli deklareerimine: loomine toimub ĂŒks kord, kasutatakse kĂ”ikide
// vastava klassi protsessi eksemplaride poolt
val initialPlayerModel = processModel(name = "InitialPlayer",
                                                     version = 1) {

    // Reeglite kohaselt on esimene mÀngija mÀngu algataja ja peab olema ainus
    uniqueConstraint = UniqueConstraints.singleton

    // Deklareerime tegevused, millest koosneb Àri-protsess
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Liitus ${players.size} mÀngijat. Alustame?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... mÀngija ${signal.data} liitus ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... mÀngija ${signal.data} on vÀljas ...")
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("Ball") {
        shotCounter = it
    }

    // NĂŒĂŒd konstrueerime protsessi graafi deklareeritud tegevustest
    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)
    }

    // Lisame tegevustele tÀiendavad töötlejad logimiseks
    sendNewGameSignal.onExit { println("Alustame mÀngu!") }
    sendStopGameSignal.onExit { println("Paus!") }
    sendPlayerOut.onExit { println("$playerName: Ma olen vÀljas!") }
}

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

Lisaks Ă€rilogika tĂ€itmisele suudab antud kood genereerida Ă€riprotsessi objekti mudeli, mida saab visualiseerida diagrammina. Visualiseerijat me veel ei ole rakendanud, seetĂ”ttu tuli natuke aega joonistamisega tegeleda (siin olen BPMN sĂŒntaksit osaliselt lihtsustanud, et parandada diagrammi ĂŒhtsust antud koodiga):

BPM-stiilis integreerimine

Rakendus app2 sisaldab teise mÀngija Àriprotsessi:

class RandomPlayer

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

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

class PlayersList: ArrayList()

class RandomPlayer : ProcessImpl(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(name = "RandomPlayer", 
                                                   version = 1) {

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

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

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

private fun MessageSendInstance.selectNextPlayer() {
    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}")
}

Diagramm:

BPM-stiilis integreerimine

Rakenduses app3 muudame mÀngijat veidi teistsuguseks: jÀrgmise mÀngija juhusliku valimise asemel jÀrgib ta round-robin algoritmi:

class RoundRobinPlayer

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

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

class PlayersList: ArrayList()

class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {

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

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

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

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

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

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

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

Muudes osades ei erine mÀngija kÀitumine eelmisest, seega diagramm ei muutu.

NĂŒĂŒd on vaja testi, et seda kĂ”ike kĂ€ivitada. Toome vĂ€lja ainult testi koodi, et mitte artiklit boorimĂ€nguga ĂŒle koormata (tegelikult kasutasin ma testikeskkonda, mille olin varem loonud teiste Ă€ritegevuste integratsiooni testimiseks):

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");
    // NĂŒĂŒd tuleb natuke oodata, kuni mĂ€ngijad "tuttavaks" saavad.
    // Ootamine sleep'i kaudu on halb lahendus, aga kÔige lihtsam. 
    // Ärge tehke niimoodi tĂ”sistes testides!
    Thread.sleep(1000);
    // KÀivitame mÀngu, sulgedes kasutaja tegevuse
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

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

KĂ€ivitame testi, vaatame logi:

konsooli vÀljund

Lukku vÔtme lock://app1/process/InitialPlayer on vÔetud.
MĂ€ngime!
Lukku vÔtme lock://app1/process/InitialPlayer on eemaldatud.
MĂ€ngija2: Olen siin!
MĂ€ngija3: Olen siin!
MĂ€ngija4: Olen siin!
MĂ€ngija5: Olen siin!
... liitu mÀngijaga MÀngija2 ...
... liitu mÀngijaga MÀngija4 ...
... liitu mÀngijaga MÀngija3 ...
... liitu mÀngijaga MÀngija5 ...
Samm 1: MĂ€ngija1 >>> MĂ€ngija3
Samm 2: MĂ€ngija3 >>> MĂ€ngija5
Samm 3: MĂ€ngija5 >>> MĂ€ngija3
Samm 4: MĂ€ngija3 >>> MĂ€ngija4
Samm 5: MĂ€ngija4 >>> MĂ€ngija3
Samm 6: MĂ€ngija3 >>> MĂ€ngija4
Samm 7: MĂ€ngija4 >>> MĂ€ngija5
Samm 8: MĂ€ngija5 >>> MĂ€ngija2
Samm 9: MĂ€ngija2 >>> MĂ€ngija5
Samm 10: MĂ€ngija5 >>> MĂ€ngija4
Samm 11: MĂ€ngija4 >>> MĂ€ngija2
Samm 12: MĂ€ngija2 >>> MĂ€ngija4
Samm 13: MĂ€ngija4 >>> MĂ€ngija1
Samm 14: MĂ€ngija1 >>> MĂ€ngija4
Samm 15: MĂ€ngija4 >>> MĂ€ngija3
Samm 16: MĂ€ngija3 >>> MĂ€ngija1
Samm 17: MĂ€ngija1 >>> MĂ€ngija2
Samm 18: MĂ€ngija2 >>> MĂ€ngija3
Samm 19: MĂ€ngija3 >>> MĂ€ngija1
Samm 20: MĂ€ngija1 >>> MĂ€ngija5
Samm 21: MĂ€ngija5 >>> MĂ€ngija1
Samm 22: MĂ€ngija1 >>> MĂ€ngija2
Samm 23: MĂ€ngija2 >>> MĂ€ngija4
Samm 24: MĂ€ngija4 >>> MĂ€ngija5
Samm 25: MĂ€ngija5 >>> MĂ€ngija3
Samm 26: MĂ€ngija3 >>> MĂ€ngija4
Samm 27: MĂ€ngija4 >>> MĂ€ngija2
Samm 28: MĂ€ngija2 >>> MĂ€ngija5
Samm 29: MĂ€ngija5 >>> MĂ€ngija2
Samm 30: MĂ€ngija2 >>> MĂ€ngija1
Samm 31: MĂ€ngija1 >>> MĂ€ngija3
Samm 32: MĂ€ngija3 >>> MĂ€ngija4
Samm 33: MĂ€ngija4 >>> MĂ€ngija1
Samm 34: MĂ€ngija1 >>> MĂ€ngija3
Samm 35: MĂ€ngija3 >>> MĂ€ngija4
Samm 36: MĂ€ngija4 >>> MĂ€ngija3
Samm 37: MĂ€ngija3 >>> MĂ€ngija2
Samm 38: MĂ€ngija2 >>> MĂ€ngija5
Samm 39: MĂ€ngija5 >>> MĂ€ngija4
Samm 40: MĂ€ngija4 >>> MĂ€ngija5
Samm 41: MĂ€ngija5 >>> MĂ€ngija1
Samm 42: MĂ€ngija1 >>> MĂ€ngija5
Samm 43: MĂ€ngija5 >>> MĂ€ngija3
Samm 44: MĂ€ngija3 >>> MĂ€ngija5
Samm 45: MĂ€ngija5 >>> MĂ€ngija2
Samm 46: MĂ€ngija2 >>> MĂ€ngija3
Samm 47: MĂ€ngija3 >>> MĂ€ngija2
Samm 48: MĂ€ngija2 >>> MĂ€ngija5
Samm 49: MĂ€ngija5 >>> MĂ€ngija4
Samm 50: MĂ€ngija4 >>> MĂ€ngija2
Samm 51: MĂ€ngija2 >>> MĂ€ngija5
Samm 52: MĂ€ngija5 >>> MĂ€ngija1
Samm 53: MĂ€ngija1 >>> MĂ€ngija5
Samm 54: MĂ€ngija5 >>> MĂ€ngija3
Samm 55: MĂ€ngija3 >>> MĂ€ngija5
Samm 56: MĂ€ngija5 >>> MĂ€ngija2
Samm 57: MĂ€ngija2 >>> MĂ€ngija1
Samm 58: MĂ€ngija1 >>> MĂ€ngija4
Samm 59: MĂ€ngija4 >>> MĂ€ngija1
Samm 60: MĂ€ngija1 >>> MĂ€ngija4
Samm 61: MĂ€ngija4 >>> MĂ€ngija3
Samm 62: MĂ€ngija3 >>> MĂ€ngija2
Samm 63: MĂ€ngija2 >>> MĂ€ngija5
Samm 64: MĂ€ngija5 >>> MĂ€ngija4
Samm 65: MĂ€ngija4 >>> MĂ€ngija5
Samm 66: MĂ€ngija5 >>> MĂ€ngija1
Samm 67: MĂ€ngija1 >>> MĂ€ngija5
Samm 68: MĂ€ngija5 >>> MĂ€ngija3
Samm 69: MĂ€ngija3 >>> MĂ€ngija4
Samm 70: MĂ€ngija4 >>> MĂ€ngija2
Samm 71: MĂ€ngija2 >>> MĂ€ngija5
Samm 72: MĂ€ngija5 >>> MĂ€ngija2
Samm 73: MĂ€ngija2 >>> MĂ€ngija1
Samm 74: MĂ€ngija1 >>> MĂ€ngija4
Samm 75: MĂ€ngija4 >>> MĂ€ngija1
Samm 76: MĂ€ngija1 >>> MĂ€ngija2
Samm 77: MĂ€ngija2 >>> MĂ€ngija5
Samm 78: MĂ€ngija5 >>> MĂ€ngija4
Samm 79: MĂ€ngija4 >>> MĂ€ngija3
Samm 80: MĂ€ngija3 >>> MĂ€ngija1
Samm 81: MĂ€ngija1 >>> MĂ€ngija5
Samm 82: MĂ€ngija5 >>> MĂ€ngija1
Samm 83: MĂ€ngija1 >>> MĂ€ngija4
Samm 84: MĂ€ngija4 >>> MĂ€ngija5
Samm 85: MĂ€ngija5 >>> MĂ€ngija3
Samm 86: MĂ€ngija3 >>> MĂ€ngija5
Samm 87: MĂ€ngija5 >>> MĂ€ngija2
Samm 88: MĂ€ngija2 >>> MĂ€ngija3
MÀngija2: Olen vÀljas!
Samm 89: MĂ€ngija3 >>> MĂ€ngija4
... mÀngija MÀngija2 on vÀljas ...
Samm 90: MĂ€ngija4 >>> MĂ€ngija1
Samm 91: MĂ€ngija1 >>> MĂ€ngija3
Samm 92: MĂ€ngija3 >>> MĂ€ngija1
Samm 93: MĂ€ngija1 >>> MĂ€ngija4
Samm 94: MĂ€ngija4 >>> MĂ€ngija3
Samm 95: MĂ€ngija3 >>> MĂ€ngija5
Samm 96: MĂ€ngija5 >>> MĂ€ngija1
Samm 97: MĂ€ngija1 >>> MĂ€ngija5
Samm 98: MĂ€ngija5 >>> MĂ€ngija3
Samm 99: MĂ€ngija3 >>> MĂ€ngija5
Samm 100: MĂ€ngija5 >>> MĂ€ngija4
Samm 101: MĂ€ngija4 >>> MĂ€ngija5
MÀngija4: Olen vÀljas!
... mÀngija MÀngija4 on vÀljas ...
Samm 102: MĂ€ngija5 >>> MĂ€ngija1
Samm 103: MĂ€ngija1 >>> MĂ€ngija3
Samm 104: MĂ€ngija3 >>> MĂ€ngija1
Samm 105: MĂ€ngija1 >>> MĂ€ngija3
Samm 106: MĂ€ngija3 >>> MĂ€ngija5
Samm 107: MĂ€ngija5 >>> MĂ€ngija3
Samm 108: MĂ€ngija3 >>> MĂ€ngija1
Samm 109: MĂ€ngija1 >>> MĂ€ngija3
Samm 110: MĂ€ngija3 >>> MĂ€ngija5
Samm 111: MĂ€ngija5 >>> MĂ€ngija1
Samm 112: MĂ€ngija1 >>> MĂ€ngija3
Samm 113: MĂ€ngija3 >>> MĂ€ngija5
Samm 114: MĂ€ngija5 >>> MĂ€ngija3
Samm 115: MĂ€ngija3 >>> MĂ€ngija1
Samm 116: MĂ€ngija1 >>> MĂ€ngija3
Samm 117: MĂ€ngija3 >>> MĂ€ngija5
Samm 118: MĂ€ngija5 >>> MĂ€ngija1
Samm 119: MĂ€ngija1 >>> MĂ€ngija3
Samm 120: MĂ€ngija3 >>> MĂ€ngija5
Samm 121: MĂ€ngija5 >>> MĂ€ngija3
MÀngija5: Olen vÀljas!
... mÀngija MÀngija5 on vÀljas ...
Samm 122: MĂ€ngija3 >>> MĂ€ngija5
Samm 123: MĂ€ngija5 >>> MĂ€ngija1
MÀngija5: Olen vÀljas!
Samm 124: MĂ€ngija1 >>> MĂ€ngija3
... mÀngija MÀngija5 on vÀljas ...
Samm 125: MĂ€ngija3 >>> MĂ€ngija1
Samm 126: MĂ€ngija1 >>> MĂ€ngija3
MÀngija1: Olen vÀljas!
... mÀngija MÀngija1 on vÀljas ...
Samm 127: MĂ€ngija3 >>> MĂ€ngija3
MÀngija3: Olen vÀljas!
Samm 128: MĂ€ngija3 >>> MĂ€ngija3
... mÀngija MÀngija3 on vÀljas ...
MÀngija3: Olen vÀljas!
Peatada!
Samm 129: MĂ€ngija3 >>> MĂ€ngija3
MÀngija3: Olen vÀljas!

KÔigest sellest vÔib teha mitu olulist jÀreldust:

  • vajadusel vajalikud tööriistad, saavad rakenduse arendajad luua rakenduste vahel integreerivaid suhtlusi ilma Ă€riloogikast lahkumata;
  • integreerimise ĂŒlesande keerukust, mis nĂ”uab insenerioskusi, saab varjata raamistiku sees, kui see algselt on arhitektuuri sisse ehitatud. Ülesande raskust ei saa varjata, seetĂ”ttu nĂ€eb keerulise ĂŒlesande lahendamine koodis vastavalt vĂ€lja;
  • integreerimisloogika arendamisel tuleb arvesse vĂ”tta lĂ”puks jĂ€rjepidevust ja kĂ”igi osaliste oleku muutuste mitte-lineaarset kĂ€itumist. See sunnib loogikat keerukamaks muutma, et see ei oleks tundlik vĂ€listest sĂŒndmustest tuleneva esinemisjĂ€rjepidevuse suhtes. Meie nĂ€ites peab mĂ€ngija hakkama mĂ€ngus osalema alles pĂ€rast seda, kui ta on teatanud mĂ€ngust lahkumisest: teised mĂ€ngijad jĂ€tkavad palli edastamist, kuni teave tema lahkumisest jĂ”uab ja töödeldakse kĂ”ikide osaliste poolt. See loogika ei tulene mĂ€ngureeglitest ja on kompromisslahendus valitud arhitektuuri raames.

JĂ€rgmisena rÀÀgime meie lahenduse erinevatest nĂŒanssidest, kompromissidest ja muudest aspektidest.

KĂ”ik sĂ”numid – ĂŒhes jĂ€rjekorras

KĂ”ik integreeritavad rakendused töötavad ĂŒhe integreerimisbusina, mis esindab end vĂ€lise vahendajana, ĂŒhe jĂ€rjekorra BPMQueue – sĂ”numite jaoks ja ĂŒhe teema BPMTopic – signaalide (sĂŒndmuste) jaoks. KĂ”ik sĂ”numid, mis lĂ€bivad ĂŒhe jĂ€rjekorra, on iseenesest kompromiss. Äri-, loogika tasemel on nĂŒĂŒd vĂ”imalik tĂ€iesti uut tĂŒĂŒpi sĂ”numeid sissetuua, ilma et peaks muudatusi sĂŒsteemi struktuuris tegema. See on mĂ€rkimisvÀÀrne lihtsustamine, kuid sellega kaasnevad teatud riskid, mis meie tĂŒĂŒpiliste ĂŒlesannete kontekstis tunduvad meile mitte nii olulised.

BPM-stiilis integreerimine

Siiski on siin ĂŒks nĂŒanss: iga rakendus filtreerib „oma” sĂ”numid jĂ€rjekorrast juba sisendiperioodil, oma domeeninime jĂ€rgi. Domeen vĂ”ib olla mÀÀratud ka signaalides, kui on vaja piirata signaali „nĂ€kku” ainult ĂŒhe rakendusega. See peaks tĂ”stma busside lĂ€bilaskevĂ”imet, kuid Ă€riloogika peab nĂŒĂŒd opereerima domeeninimedega: sĂ”numite adresseerimisel – kohustuslik, signaalide puhul – soovituslik.

Integreerimisbussi usaldusvÀÀrsuse tagamine

UsaldusvÀÀrsus koosneb mitmest aspektist:

  • valitud sĂ”numi vahendaja – kriitilise tĂ€htsusega komponent arhitektuuris ja ainus rikkepunkt: see peab olema piisavalt riketekindel. Tuleb kasutada ainult ajaga tĂ”estatud lahendusi, millel on hea tugi ja suur kogukond;
  • on oluline tagada sĂ”numi vahendaja kĂ”rge saadavus, mistĂ”ttu peab see olema fĂŒĂŒsiliselt eraldatud integreeritud rakendustest (rakenduste kĂ”rge saadavuse tagamine, millel on Ă€riloogika, on mĂ€rgatavalt keerulisem ja kallim);
  • vahendaja peab tagama „at least once” kohaletoimetamise garantii. See on kohustuslik nĂ”ue integreerimisbussi usaldusvÀÀrseks toimimiseks. „Exactly once” taseme garantiide kasutamine ei ole vajalik: Ă€ri protsessid ei ole tavaliselt vĂ€ga tundlikud sĂ”numite vĂ”i sĂŒndmuste korduva saabumise suhtes, ja erijuhudes, kus see on oluline, on lihtsam lisada tĂ€iendav kontroll Ă€riloogikasse, kui pidevalt kasutada piisavalt „kalleid” garantiisid;
  • sĂ”numite ja signaalide edastamine tuleb kaasata Ă€riprotsesside ja domeendataandete oleku muutmisega seotud ĂŒldisesse tehingusse. Eelistatav variant on kasutada mustrit Transactional Outbox, kuid see nĂ”uab andmebaasis lisalaudade olemasolu ja edastajat. JEE-rakendustes saab seda hetke lihtsustada kohaliku JTA halduriga, kuid valitud vahendajaga ĂŒhendamine peab suutma töötada XA;
  • sisse tulevate sĂ”numite ja sĂŒndmuste töötlejad peavad samuti töötama Ă€riprotsessi oleku muutmise tehinguga: kui selline tehing tĂŒhistatakse, tuleb ka sĂ”numi vastuvĂ”tt tĂŒhistada;
  • sĂ”numid, mida ei Ă”nnestunud kohaletoimetada vigade tĂ”ttu, tuleb koguda eraldi salvestusse DLQ (Dead Letter Queue). Meie jaoks lĂ”ime eraldi platvormi mikroteenuse, mis salvestab sellised sĂ”numid oma andmehoidlasse, indekseerib need atribuudi alusel (kiire grupeerimise ja otsingu jaoks) ning pakub API-d nende vaatamiseks, uuesti saatmiseks sihtkohta ja sĂ”numite kustutamiseks. SĂŒsteemi administraatorid saavad seda teenust kasutada oma veebiliidese kaudu;
  • brokeeri seadistustes tuleb kohandada edastamise korduste arvu ja viivitusi edastamise vahel, et vĂ€hendada sĂ”numite jĂ”udmise tĂ”enĂ€osust DLQ-sse (optimaalsete parameetrite arvutamine on praktiliselt vĂ”imatu, kuid neid saab kohandada empiiriliselt ja kohandada nende kasutuse ajal);
  • DLQ andmehoidlat peab pidevalt jĂ€lgima ja jĂ€lgimisseade peab teavitama sĂŒsteemi administraatoreid, et reageerida vĂ”imalikult kiiresti, kui ebaĂ”nnestunud sĂ”numeid ilmneb. See vĂ€hendab tekkinud tĂ”rke vĂ”i Ă€riloogika vea „mĂ”jupiirkonda“;
  • integreerimisbuss peaks olema ajutiste rakenduste puudumise suhtes tundetu: tellimused teema suhtes peavad olema pĂŒsivad ja rakenduse domeeninimi peab olema ainulaadne, et rakenduse puudumise ajal ei prooviks keegi teine tema sĂ”numeid jĂ€rjekorrast töödelda.

Äriloogika vooluahelate tagamine

Sama Ă€riprotsessi eksemplar vĂ”ib saada korraga mitu sĂ”numit ja sĂŒndmust, mille töötlemine kĂ€ivitub paralleelselt. Samal ajal peab rakenduse arendaja jaoks olema kĂ”ik lihtne ja vooluahelateohutu.

Äriprotsessi Ă€riloogika töötleb iga vĂ€list sĂŒndmust, mis mĂ”jutab seda Ă€riprotsessi, eraldi. Sellised sĂŒndmused vĂ”ivad olla:

  • Ă€riprotsessi eksemplari kĂ€ivitamine;
  • kasutaja tegevus, mis on seotud tegevusega Ă€riprotsessis;
  • sĂ”numi vĂ”i signaali saabumine, millele Ă€riprotsessi eksemplar on tellitud;
  • Ă€riprotsessi eksemplaril seadistatud taimeri aktiveerimine;
  • kontrolliv mĂ”ju API kaudu (nĂ€iteks protsessi erakorraline katkestamine).

Iga selline sĂŒndmus vĂ”ib muuta Ă€riprotsessi instantsi olekut: mĂ”ned tegevused vĂ”ivad lĂ”ppeda ja teised alata, pĂŒsivaatluste vÀÀrtused vĂ”ivad muutuda. Mis tahes tegevuse lĂ”petamine vĂ”ib aktiveerida ĂŒhe vĂ”i mitu jĂ€rgmist tegevust. Need vĂ”ivad omakorda peatuda teiste sĂŒndmuste ootel vĂ”i, kui nad ei vaja tĂ€iendavaid andmeid, vĂ”ivad nad lĂ”ppeda samas tehingus. Enne tehingu sulgemist salvestatakse uus Ă€riprotsessi olek andmebaasi, kus see ootab jĂ€rgmise vĂ€list sĂŒndmuse toimumist.

Äriprotsessi pĂŒsivad andmed, salvestatud relatsioonilisse andmebaasi, on vĂ€ga mugav töötlemise sĂŒnkroonimise punkt, kui kasutada SELECT FOR UPDATE. Kui ĂŒhe tehingu jooksul suudab Ă€riprotsessi olekut andmebaasist muuta, siis ei saa ĂŒhtegi teist tehingut samal ajal saadud olekut teise muutmise jaoks saada, ja pĂ€rast esimese tehingu lĂ”petamist garanteeritakse, et teine saab juba muudetud oleku.

Kasutades pessimistic lukustusi andmebaasi poolel, tÀidame kÔik vajalikud nÔuded ACID, samuti sÀilitame rakenduse Àriloogika skaleeritavuse, suurendades kÀimasolevate instantside arvu.

Kuid pessimistic lukustused Àhvardavad meid deadlock'idega, seega tuleks SELECT FOR UPDATE siiski piirata mÔistliku ajutise katkestusega, et vÀltida deadlock'e mÔnedes Àriloogika ÀÀrmuslikes juhtumites.

Veel ĂŒks probleem on Ă€riprotsessi kĂ€ivitamise sĂŒnkroniseerimine. Seni, kuni ei ole Ă€riprotsessi instantsi, ei ole ka selle olekut andmebaasis, seega ei sobi kirjeldatud meetod. Kui on vaja tagada Ă€riprotsessi instantsi unikaalsus teatud ulatuses, on vajalik mĂ”ni sĂŒnkroniseerimise objekt, mis on seotud protsessi klassi ja vastava ulatusega. Selle probleemi lahendamiseks kasutame teistsugust lukustuste mehhanismi, mis vĂ”imaldab lukustada mis tahes ressursi, mille vĂ”ti on mÀÀratud URI formaadis, lĂ€bi vĂ€lishoiu.

Meie nÀidetes Àriprotsess InitialPlayer sisaldab vÀlja kuulutamist

uniqueConstraint = UniqueConstraints.singleton

SeetÔttu logis on teadetetekstid vastava vÔtme lukustamise ja vabastamise kohta. Muude Àriprotsesside puhul selliseid teadeteteke ei ole: uniqueConstraint ei ole mÀÀratud.

Äriprotsesside probleemid pĂŒsiva olekuga

MĂ”nikord on pĂŒsiva oleku olemasolu mitte ainult abiks, vaid see segab ka arendust.
Probleemid algavad siis, kui on vaja teha muudatusi Ă€riloogikas ja/vĂ”i Ă€riprotsessi mudelis. Mitte iga selline muudatus ei osutu ĂŒhilduvaks vana Ă€riprotsessi olekuga. Kui andmebaasis on palju "elu" eksemplare, vĂ”ib kokku sobimatute muudatuste tegemine pĂ”hjustada palju ebamugavusi, millega oleme sageli kokku puutunud jBPM-i kasutamisel.

SĂ”ltuvalt muudatuste sĂŒgavusest saab tegutseda kahel viisil:

  1. luua uus Ă€riprotsessi tĂŒĂŒp, et mitte teha sooja muudatust vanas ja kasutada seda uue kĂ€ivitamisel vanade asemel. Vanad eksemplarid jĂ€tkavad "vana" jĂ€rgi töötamist;
  2. migratsioon pĂŒsivat olekut Ă€riprotsesside uuendamisel Ă€riloogikas.

Esimene tee on lihtsam, kuid sellel on oma piirangud ja puudused, nÀiteks:

  • Ă€riloogika dubleerimine paljudes Ă€riprotsessi mudelites, Ă€ri loogika mahu suurenemine;
  • tihti on vajalik kiire ĂŒleminek uuele Ă€riloogikale (integreerimise ĂŒlesannete puhul – peaaegu alati);
  • arendaja ei tea, millal vĂ”ib vanad mudelid eemaldada.

Praktikas kasutame mÔlemat lÀhenemist, kuid oleme teinud mitmeid otsuseid, et endale elu lihtsustada:

  • andmebaasis sĂ€ilitatakse Ă€riprotsessi pĂŒsiv olek kergesti loetavas ja kergesti töödeldavas vormis: JSON-formaadis rea siseselt. See vĂ”imaldab teostada migreerimisi nii rakenduse sees kui ka vĂ€ljaspool. ÄÀrmuslikel juhtudel saab ka kĂ€sitsi kohandada (eriti kasulik arenduse ja silumise ajal);
  • integreerimise Ă€riloogika ei kasuta Ă€riprotsesside nimetusi, et igal hetkel saaks asendada ĂŒhe osaleva protsessi realiseerimise uuega, uue nimega (nĂ€iteks "InitialPlayerV2"). Sidumine toimub teadete ja signaalide nimede kaudu;
  • Protsessi mudelil on versiooninumber, mida suurendame, kui teeme sellesse mudelisse ĂŒhilduvaid muudatusi, ja see number salvestatakse koos protsessieksemplari olekuga;
  • Protsessi pĂŒsiv olek loetakse alguses andmebaasist mugavasse objektimudelisse, millega migreerimisprotseduur saab töötada, kui mudeli versiooninumber on muutunud;
  • Migreerimisprotseduur asub lĂ€hedal Ă€riloogikale ja kutsutakse esile «laisk» igas Ă€riprotsessi eksemplaris selle taastamisel andmebaasist;
  • Kui tuleb kiiresti ja sĂŒnkroonselt migreerida kĂ”ikide protsessi eksemplaride olek, rakendatakse traditsioonilisemaid lahendusi andmebaasi migreerimiseks, kuid seal tuleb töötada JSON-iga.

Kas on vaja veel ĂŒhte Ă€riprotsesside raamistikku?

Artiklis kirjeldatud lahendused on andnud meile vĂ”imaluse oma elu mĂ€rgatavalt lihtsustada, laiendada kĂŒsimuste ringi, mida saab kĂ€sitleda rakenduslikul tasemel, ning muuta Ă€riloogika eraldamise ideed mikroteenustesse atraktiivsemaks. Selleks on tehtud palju tööd, loodud vĂ€ga „kerge” raamistik Ă€riprotsesside jaoks, samuti teenindavad komponendid nimetatud probleemide lahendamiseks laias rakenduslike ĂŒlesannete ringis. Meil on soov jagada neid tulemusi, viia ĂŒldkomponentide arendus avatud juurde pÀÀsuga vabade litsentsidega. Selleks on vajalikud teatud pingutused ja aeg. Arusaamine selliste lahenduste nĂ”udmisest vĂ”iks olla meie jaoks tĂ€iendav stiimul. Soovitatud artiklis on vĂ€ga vĂ€he tĂ€helepanu pööratud raamistikule endale, kuid mĂ”ned selle omadused on nĂ€htavad esitatud nĂ€idetest. Kui me siiski oma raamistikku avaldame, siis pĂŒhendame sellele eraldi artikli. Seniks oleme tĂ€nulikud, kui jĂ€tate lĂŒhikese tagasiside, vastates kĂŒsimusele:

Ainult registreeritud kasutajad saavad kĂŒsitluses osaleda. Logige sisse, palun.

Kas on vaja veel ĂŒhte Ă€riprotsesside raamistikku?

  • 18,8%Jah, oleme kaua otsinud midagi sellist3

  • 12,5%On huvitav rohkem teada teie rakendusest, vĂ”ib-olla lĂ€heb vaja2

  • 6,2%Kasutame ĂŒhte olemasolevat raamistikku, aga kaalume vahetamist1

  • 18,8%Kasutame ĂŒhte olemasolevat raamistikku, kĂ”ik on hea3

  • 18,8%Saame ilma raamistikuta hakkama3

  • 25,0%Kirjutame oma4

HÀÀletas 16 kasutajat. JÀid neutraalseks 7 kasutajat.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster