
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.

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).

Nii nÀeb protsess vÀlja pÀrast mitmeid iteratsioone nÔudmiste tÀpsustamiseks
VĂ€ljapÀÀs sellest olukorrast oli mootorite integreerimine 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. .

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.

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 .
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.

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.

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):

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:

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.

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 , 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 ;
- 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 (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 , 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.singletonSeetÔ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:
- 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;
- 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. , 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
