
Tere, Habr!
Meie ettevĂ”te keskendub ERP-taseme tarkvaralahenduste vĂ€ljatöötamisele, kusjuures suurem osa neist lahendustest pĂ”hineb suurte Ă€riloogika ja dokumendihaldusega tehingusĂŒsteemidel, nagu nĂ€iteks elektroonilised dokumendihaldussĂŒsteemid. Meie toodete kaasaegsed versioonid pĂ”hinevad JavaEE tehnoloogiatel, kuid me katsetame ka aktiivselt mikroteenustega. Ăks suurimaid vĂ€ljakutseid sellistes lahendustes on erinevate alamsĂŒsteemide integreerimine, mis kuuluvad seotud domeenidesse. IntegreerimisĂŒlesanded on meile alati peavalu valmistanud, olenemata meie kasutatavatest arhitektuuristiilidest, tehnoloogiakomplektidest ja raamistikutest, kuid viimastel aegadel on selliste probleemide lahendamisel nĂ€htavad edusammud.
Pakutavas artiklis rÀÀgin NPO "Krista" kogemustest ja arhitektuurilistest uurimistest antud valdkonnas. Samuti vaatleme nĂ€idet lihtsast lahendusest integreerimisĂŒlesandele rakendusprogrammeera perspektiivist ja selgitame vĂ€lja, mis peitub selle lihtsuse taga.
Vastutusest loobumine
Artiklis kirjeldatud arhitektuuri- ja tehnilised lahendused pĂ”hinevad minu isiklikul kogemusel konkreetsete ĂŒlesannete kontekstis. Need lahendused ei pretendeeri universaalsusele ja vĂ”ivad teistes kasutustingimustes osutuda mitte-optimaalseteks.
Mis seos on BPM-il?
Sellele kĂŒsimusele vastamiseks tuleb natuke sĂŒveneda meie lahenduste rakendusprobleemide iseloomu. Enamiku meie tĂŒĂŒpilise tehingusĂŒsteemi Ă€ri loogikast moodustab andmete sisestamine andmebaasi kasutajaliideste kaudu, nende andmete kĂ€sitsi ja automatiseeritud kontrollimine, nende suunamine teatud töövoo kaudu, avaldamine teise sĂŒsteemi / analĂŒĂŒtilisse baasi / arhiivi, aruannete koostamine. Seega on sĂŒsteemi peamine funktsioon tellijate jaoks nende siseĂ€ri protsesside automatiseerimine.
Kasutusmugavuse huvides kasutame suhtlemisel mĂ”istet âdokumentâ kui teatud andmekogumi abstraktsiooni, mis on seotud ĂŒhise vĂ”tmega, millele saab âkinni sidudaâ teatud töövoo.
Aga kuidas on lood integreerimisloogikaga? Integreerimise ĂŒlesanne tekib sĂŒsteemi arhitektuurist, mis on jagatud osadeks, mitte tellija soovil, vaid hoopis teiste tegurite mĂ”jul:
- Conway seaduse mÔjul;
- varasemate toodete jaoks vĂ€ljatöödeldud alam-sĂŒsteemide taaskasutamise tulemusena;
- arhitekti otsuse tulemusena, lÀhtudes mittefunktsionaalsetest nÔudmistest.
On suur kiusatus eraldada integreerimisloogika pĂ”hitegevuse Ă€ri loogikast, et mitte reostada Ă€ri loogikat integreerimisartefaktidega ja vabastada arendaja vajadusest sĂŒĂŒvida sĂŒsteemi arhitektuuri eripĂ€radesse. Sellel lĂ€henemisel on mitmeid eeliseid, kuid praktika nĂ€itab selle ebaefektiivsust:
- integreerimisprobleemide lahendamine viib tavaliselt kĂ”ige lihtsamate lahendusteni, nagu sĂŒnkroonilised kutsed, kuna pĂ”hitegevuse teostuses on laienduspunktide arv piiratud (sĂŒnkroonse integreerimise puudustest â veidi allpool);
- integreerimisartefaktid jĂ”uavad ikkagi pĂ”hitegevuse Ă€ri loogikasse, kui on vajalik tagasiside teisest alam-sĂŒsteemist;
- rakenduse arendaja ignoreerib integreerimist ja vÔib seda kergesti rikkuda, muutes töövoogu;
- sĂŒsteem lakkab olemast terviklik kasutaja vaatepunktist, muutuvad nĂ€htavaks "Ă”mblused" alamsĂŒsteemide vahel, tekivad liigsed kasutaja toimingud, mis kĂ€ivitavad andmete edastamise ĂŒhelt alamsĂŒsteemilt teisele.
Teine lĂ€henemine on integreerivate interaktsioonide kĂ€sitlemine kui lahutamatu osa pĂ”hiettevĂ”tte loogikast ja töökĂ€igust. Selleks, et rakenduste arendajate kvalifikatsiooninĂ”uded ei tĂ”useks kosmosesse, tuleks uusi integratsiooni interaktsioone luua lihtsalt ja sujuvalt, vĂ”imaldades minimaalset valiku tegemise vĂ”imalust. Selle saavutamine on keerulisem, kui tundub: tööriist peab olema piisavalt vĂ”imas, et pakkuda kasutajale vajalikku valikut ja samas mitte vĂ”imaldada "ennast jalga tulistada". On palju kĂŒsimusi, millele insener peab vastama integreetimise ĂŒlesannete kontekstis, kuid mille ĂŒle rakenduste arendaja oma igapĂ€evases töös mĂ”tlema ei peaks: tehingu piirid, jĂ€rjepidevus, aatomlikkus, turvalisus, skaleerimine, koormuse ja ressursside jaotus, marsruutimine, marĆĄaldamine, konteksti levitamine ja vahetamine jne. Tuleb pakkuda rakenduste arendajatele piisavalt lihtsaid lahenduste malle, mis sisaldavad juba vastuseid kĂ”ikidele sarnastele kĂŒsimustele. Need mallid peavad olema piisavalt turvalised: Ă€riloogika muutub vĂ€ga sageli, mis suurendab vigade tekkimise riski, vigade hind peab jÀÀma piisavalt madalaks.
Aga ikkagi, mis kasu on BPM-ist? On ju olemas palju vÔimalusi töövoo rakendamiseks...
TĂ”epoolest, meie lahendustes on vĂ€ga populaarne teine Ă€ri protsesside rakendamine â deklaratiivse ĂŒlemineku diagrammi mÀÀratlemine ja Ă€riloogika kĂ€itlejate ĂŒhendamine ĂŒleminekute ajal. Selle kĂ€igus on olek, mis mÀÀratleb "dokumentide" praeguse positsiooni Ă€ri protsessis, ise "dokumentide" atribuut.

Nii nÀeb vÀlja protsess projekti alguses
Sellise rakenduse populaarsus tuleneb suhtelisest lihtsusest ja lineaarsete Ă€ri protsesside loomise kiirusest. Kuid pideva tarkvarasĂŒsteemide keerukuse suurenemisega laieneb ja keerukamaks muutub ka automatiseeritud Ă€ri protsessi osa. MĂ”nikord on vajalik dekompositsioon, protsesside osade taaskasutamine ning protsesside hargnemine, et iga haru saaks samaaegselt tĂ€idetud. Sellistes tingimustes muutub tööriist ebamugavaks ja olekudiagramm kaotab informatiivsuse (integratsioonimanipulatsioonid ei kajastu diagrammil ĂŒldse).

Nii nÀeb protsess vÀlja pÀrast mitmeid nÔudmiste tÀpsustamise iteratsioone
VĂ€ljapÀÀs sellest olukorrast oli mootori integreerimine mĂ”nedesse toodetesse, millel on kĂ”ige keerukamad Ă€ri-protsessid. LĂŒhiajaliselt oli see lahendus teatud edu saavutanud: tekkis vĂ”imalus keerukate Ă€ri-protsesside rakendamiseks, sĂ€ilitades samal ajal piisavalt informatiivse ja ajakohase diagrammi BPMN2 vormingus. .

VÀike osa keerukast Àri-protsessist
Pikaajaliselt ei tĂ€itnud lahendus ootusi: visuaalsete tööriistade kaudu Ă€ri-protsesside loomise kĂ”rge töömaht ei vĂ”imaldanud saavutada rahuldavaid tootlikkuse nĂ€itajaid, ja ise tööriist muutus arendajate seas ĂŒheks kĂ”ige vĂ€hem armastatuks. Mootori struktuuri kohta oli samuti kriitikat, mis viis paljude "tĂ€ienduste" ja "toetuste" tekkimiseni.
jBPM-i kasutamise peamine positiivne aspekt on arusaam oma Ă€ritegevuse protsessi pĂŒsiva oleku olemasolu kasudest ja kahjustustest. Samuti nĂ€gime, et protsessipĂ”hise lĂ€henemise kasutamine vĂ”imaldab rakendada keerulisi integratsiooniprotokolle erinevate rakenduste vahel, kasutades asĂŒnkroonseid suhtlusi signaalide ja sĂ”numite kaudu. PĂŒsiva oleku olemasolu mĂ€ngib selles olulist rolli.
Ătlematagi on selge, protsessipĂ”hine lĂ€henemine BPM-stiilis vĂ”imaldab meil lahendada laia valikut ĂŒha keerulisemate Ă€ritegevuse automatiseerimise ĂŒlesandeid, harmooniliselt integreerida neid protsesse ja sĂ€ilitada vĂ”imalus visualiseerida teostatud protsessi sobivas nootatsioonis.
SĂŒnkroonsete kĂ”nede puudused kui integratsioonimustrid
SĂŒnkroonne integreerimine tĂ€hendab lihtsamat blokeerivat vĂ€ljakutset. Ăks alamsĂŒsteem töötab serveripoolena ja pakub vajalikku meetodit API-s. Teine alamsĂŒsteem töötab kliendipoolena ja kutsub vajalikul hetkel ĂŒles ootama vastust. SĂ”ltuvalt sĂŒsteemi arhitektuurist vĂ”ivad kliendi- ja serveripoolsed osad paikneda kas ĂŒhes rakenduses ja protsessis vĂ”i eraldi. Teisel juhul tuleb kasutada mĂ”nda RPC rakendust ning tagada parameetrite ja vĂ€ljakutse tulemuse marshelinge.

Sellisel integreerimismustril on ĂŒsna suur puuduste kogum, kuid seda kasutatakse praktikas laialdaselt tĂ€nu selle lihtsusele. Teostamise kiirus on veenev ja sunnib seda korduvalt kasutama «Àgedate» tĂ€htaegade tingimustes, kirjutades lahenduse tehnilisse vĂ”lga. Samuti juhtub, et kogenematud arendajad rakendavad seda teadmata, lihtsalt ei mĂ”ista negatiivseid tagajĂ€rgi.
Peale kĂ”ige ilmsĂ€miseid alamsĂŒsteemide ĂŒhenduvuse suurendamist on ka vĂ€hem ilmseid probleeme, mis on seotud tehingute "tĂŒkeldamise" ja "venitamisega". Kui Ă€rilogika teeb mingeid muudatusi, siis ei saa tehinguteta hakkama, ja tehingud, omakorda, blokeerivad rakenduse teatud ressursse, mis neid muudatusi mĂ”jutavad. See tĂ€hendab, et seni, kuni ĂŒks alamsĂŒsteem ootab vastust teiselt, ei suuda see tehingut lĂ”petada ja blokeeringuid eemaldada. See suurendab mĂ€rkimisvÀÀrselt erinevate efektide tekkimise riski:
- sĂŒsteemi reageerimisvĂ”ime kaob, kasutajad ootavad pikka aega vastuseid pĂ€ringutele;
- server lĂ”petab tĂ€ielikult vastamise kasutajate pĂ€ringutele, kuna niidi bassein on ĂŒlevoolanud: enamik niite on "seisakusse" sattunud tehingu tĂ”ttu blokeeritud ressursside tĂ”ttu;
- algavad deadlock'id: nende tekkimise tÔenÀosus sÔltub tugevasti tehingute kestusest, tehingusse kaasatud Àrilogikast ja blokeeringutest;
- ilmuvad tehingu ajaĂŒletusvead;
- Server "fĂŒlleb" OutOfMemory, kui ĂŒlesanne nĂ”uab suurte andmemahtude töötlemist ja muutmist, ning sĂŒnkrontootmisprotsesside olemasolu teeb vĂ€ga keeruliseks töötlemise jagamise kergemateks tehinguteks.
Arhitektuurilisest vaatest toob blokeerivate kĂ”nede kasutamine integratsioonis kaasa kontrollimise kaotsimineku ĂŒksikute alamsĂŒsteemide kvaliteedi ĂŒle: ei ole vĂ”imalik tagada ĂŒhe alamsĂŒsteemi kvaliteedi sihttasemeid eraldi teise alamsĂŒsteemi kvaliteedi nĂ€itajatest. Kui alamsĂŒsteeme arendavad erinevad meeskonnad, on see suur probleem.
Asjad muutuvad veelgi huvitavamaks, kui integreeritavad alamsĂŒsteemid asuvad erinevates rakendustes ja on vaja teha sĂŒnkroneeritud muudatusi mĂ”lemalt poolt. Kuidas tagada nende muudatuste tehinguline jĂ€rjepidevus?
Kui muudatused tehakse eraldi tehingutena, tuleb tagada tĂ”rgete ja kompensatsioonide usaldusvÀÀrne töötlemine, mis tĂŒhistab tĂ€ielikult sĂŒnkrontootmisprotsesside peamise eelise â lihtsuse.
Meeles on ka jaotatud tehingud, kuid me ei kasuta neid oma lahendustes: usaldusvÀÀrsuse tagamine on keeruline.
«Saga» kui lahendus tehingute probleemile
Mikroteenuste kasvava populaarsuse tÔttu on see muutumas jÀrjest nÔutumaks .
See muster lahendab eespool mainitud pikaajaliste tehingute probleeme ning laiendab sĂŒsteemi oleku haldamise vĂ”imalusi Ă€riloogika poolt: ebaĂ”nnestunud tehingu korral vĂ”ib kompensatsioon mitte tagasi pöörata sĂŒsteemi algsesse seisundisse, vaid tagada alternatiivse andmete töötlemise marsruudi. See vĂ”imaldab ka korduvatel katsedel andmete töötlemise samme, millel on positiivne lĂ”pp, mitte uuesti sooritada.
Intrigeeriv on see, et monoliitsetes sĂŒsteemides on see muster samuti asjakohane, kui tegemist on halvasti seotud alamsĂŒsteemide integreerimisega ja tĂ€heldatakse negatiivseid efekte, mis tulenevad pikaajalistest tehingutest ja vastavatest ressursside lukustustest.
Meie BPM-stiilis Ă€ritavade kohaselt on "Saga" rakendamine ÀÀrmiselt lihtne: ĂŒksikute "Saga" samme saab mÀÀratleda tegevustena Ă€riprotsessis ning Ă€riprotsessi pĂŒsiv olek mÀÀrab ka "Saga" sisemise oleku. See tĂ€hendab, et me ei vaja mingeid tĂ€iendavaid koordineerimismehhanisme. Ainult sĂ”numiteenuse pakkujat, millel on "at least once" garantii, on vaja transpordiks.
Kuid ka sellise lahenduse puhul on oma "hind":
- Àri loogika muutub keerulisemaks: tuleb arvestada kompensatsioonide töötlemisega;
- tuleb loobuda tĂ€iendavast jĂ€rjepidevusest, mis vĂ”ib olla eriti tundlik monoliitsete sĂŒsteemide puhul;
- veidi keerulisemaks muutub arhitektuur, tekib tÀiendav vajadus sÔnumiteenuse pakkuja jÀrgi;
- vajalikud on tĂ€iendavad monitooringu ja haldamise vahendid (kuigi see on tegelikult isegi hea: sĂŒsteemi teeninduse kvaliteet paraneb).
MonoliitsĂŒsteemide puhul pole "SAGA" kasutamise mĂ”ttekus nii ilmne. Mikroteenuste ja teiste SOA puhul, kus tĂ”enĂ€oliselt on juba olemas vahendaja ning tĂ€ielik jĂ€rjepidevus on ohverdatud juba projekti alguses, vĂ”ib selle malliga töötamise kasu mĂ€rkimisvÀÀrselt ĂŒletada puudusi, eriti kui olemas on mugav API Ă€riloogika tasemel.
Ăriloogika kapseldamine mikroteenustes
Kui me hakkasime mikroteenustega katsetama, tekkis mĂ”istlik kĂŒsimus: kuhu asetada domeeni Ă€riloogika seoses teenustega, mis tagavad domeeni andmete sĂ€ilimise?
Vaadates erinevate BPMS-ide arhitektuuri, vÔib tunduda mÔistlik eraldada Àriloogika persisteerimisest: luua kihte platvormipÔhiseid ja domeenist sÔltumatuid mikroteenuseid, mis kujundavad keskkonna ja konteineri domeeni Àriloogika tÀitmiseks, ning domeeni andmete persisteerimise korraldada eraldi kihiga lihtsate ja kergete mikroteenuste kaudu. Sel juhul teostavad Àriprotsessid persisteerimiskihiga teenuste orkestreerimist.

Selle lÀhenemise suur eelis on see, et platvormi funktsionaalsust saab pidevalt laiendada ning ainsana suureneb selle tÔttu vastava platvormi mikroteenuste kiht. Iga domeeni Àriprotsessidel on kohe vÔimalus kasutada platvormi uut funktsionaalsust, nii kui see on uuendatud.
TĂ€psem analĂŒĂŒs tĂ”i esile selle lĂ€henemise olulised puudused:
- Platvormiteenus, mis tĂ”lgendab Ă€ri loogikat paljude domeenide jaoks, kannab endas suuri riske kui ainus tĂ”rkepunkt. Sage Ă€ri loogika muutmine suurendab vigade tekkimise ohtu, mis vĂ”ivad viia sĂŒsteemis laienevate katkemisteni;
- JÔudluse probleemid: Àri loogika töötleb oma andmeid kitsaste ja aeglaste liidesega;
- Andmeid tuleb korduvalt marshaldada ja edastada lÀbi vÔrgu kihi;
- Domeeniteenus annab sageli rohkem andmeid, kui Àri loogika töötlemiseks vajalik, kuna vÀlise API teenuse tasemel puuduvad piisavad parameetrite mÀÀramise vÔimalused;
- Mitu sĂ”ltumatut Ă€riĂŒksuse osa vĂ”ivad uuesti kĂŒsida samu andmeid töötlemiseks (seda probleemi saab leevendada sessioonikomponentide lisamisega, mis salvestavad andmeid, kuid see teeb arhitektuuri keerulisemaks ning tekitab andmete ajakohasuse ja vahemĂ€lu tĂŒhistamise probleeme);
- Tehingu probleemid:
- Ăriprotsessid, millel on pĂŒsiv olek, mille salvestamise eest vastutab platvormiteenus, ei ĂŒhti domeeniandmetega, ja selle probleemi lihtsaid lahendusi ei paista;
- Domeeniandmete lukustamise viimine tehingust vĂ€lja: kui domeeni Ă€riĂŒksusele on vajalik andmete muutmine, tuleb eelnevalt kontrollida, kas andmed on Ă”iged, vĂ€ltides samal ajal vĂ”imalust, et töödeldavaid andmeid muudetakse konkurentsitena. VĂ€line andmelukustus vĂ”ib aidata probleemi lahendada, kuid sellisel lahendusel on tĂ€iendavad riskid ning see vĂ€hendab sĂŒsteemi ĂŒldist usaldusvÀÀrsust;
- TĂ€iendavad keerukused uuendamisel: mĂ”nel juhul tuleb sĂ€ilitamise ja Ă€ri loogika teenuseid uuendada sĂŒnkroonselt vĂ”i range jĂ€rjestuse kohaselt.
LĂ”ppkokkuvĂ”ttes tuli tagasipöörduda juurte juurde: kapseldada domeenide andmed ja domeeni Ă€riloogika ĂŒhte mikroteenusesse. Selline lĂ€henemine muudab mikroteenuse tajumise tervikliku komponendina sĂŒsteemis lihtsamaks ja ei tekita ĂŒlaltoodud probleeme. Selle saavutamine ei ole samuti tasuta:
- on vajalik API standardiseerimine Ă€riloogikaga suhtlemiseks (eriti kasutajate tegevuste tagamiseks Ă€riprotsessides) ja platvormiteenuste API-de jaoks; vajatakse tĂ€helepanelikku lĂ€henemist API muutustele, otse ja tagasipidi ĂŒhilduvusele;
- on vajalik tÀiendavate runtime-raamatukogude lisamine Àriloogika funktsioneerimise tagamiseks igas sellises mikroteenuses, mis tekitab uued nÔudmised nendele raamatukogudele: kerge kaal ja minimaalne sÔltuvuste hulk;
- Ăriloogika arendajad peavad jĂ€lgima teekide versioone: kui mĂ”nda mikroteenust pikka aega ei ole tĂ€iustatud, siis on tĂ”enĂ€oliselt seal vananenud teekide versioon. See vĂ”ib muutuda ĂŒllatavaks takistuseks uue funktsiooni lisamisel ja vĂ”ib nĂ”uda vana Ă€riloogika ĂŒleminekut teenuse uutele teekide versioonidele, kui versioonide vahel oli ĂŒhilduvusprobleeme.

Sellises arhitektuuris on olemas ka platvormiteenuste kiht, kuid see kiht ei moodusta enam konteinerit domeeni Àriloogika tÀitmiseks, vaid ainult selle keskkonna, pakkudes abifunktsioone. Selline kiht on vajalik mitte ainult domeenipÔhiste mikroteenuste kerguse sÀilitamiseks, vaid ka haldamise tsentraliseerimiseks.
NĂ€iteks genereerivad kasutajate tegevusi Ă€ri protsessides ĂŒlesanded. Siiski, töötades ĂŒlesannetega, peab kasutaja nĂ€gema ĂŒlesandeid kĂ”igist domeenidest ĂŒldnimekirjas, seega peab olema vastav platvormi teenus ĂŒlesannete registreerimiseks, mis on puhastatud domeeni Ă€ri loogikast. Ări loogika kapseldamise sĂ€ilitamine sellises kontekstis on piisavalt keeruline ning see on veel ĂŒks kompromiss antud arhitektuuris.
Ări protsesside integreerimine rakendusarendaja pilgu lĂ€bi
Nagu juba varem öeldud, peab rakendusarendaja olema eraldatud tehnilistest ja inseneritehnilistest aspektidest mitme rakenduse vahelise suhtluse rakendamisel, et saaks loota hea arendustoodangule.
Proovime lahendada piisavalt keerulise integreerimisĂŒlesande, mis on spetsiaalselt kirjutatud artikli jaoks. See on "mĂ€nguline" ĂŒlesanne, kus osaleb kolm rakendust, millest igaĂŒhel on oma domeeninimi: "app1", "app2", "app3".
Iga rakenduses kĂ€ivitatakse Ă€ri protsessid, mis hakkavad âpalli mĂ€ngimaâ kaudu integreerimisbusse. Pallina esindavad sĂ”numid nimega âBallâ.
MĂ€ngureeglid:
- esimene mĂ€ngija â algataja. Ta kutsub teisi mĂ€ngijaid mĂ€ngu, alustab mĂ€ngu ja vĂ”ib selle igal hetkel lĂ”petada;
- teised mĂ€ngijad kuulutavad oma osaluse mĂ€ngus, âtutvuvadâ ĂŒksteisega ja esimese mĂ€ngijaga;
- palli vastu vĂ”tnud mĂ€ngija valib teise osaleva mĂ€ngija ja edastab talle palli. Hoitakse arvestust edastuste koguarvu ĂŒle;
- igal mĂ€ngijal on âenergiaâ, mis vĂ€heneb iga palli edastamisega selle mĂ€ngija poolt. Kui energia on otsas, kukub mĂ€ngija mĂ€ngust vĂ€lja, kuulutades oma lahkumise;
- kui mĂ€ngijast jÀÀb ĂŒksi, kuulutab ta kohe lahkumist;
- kui kÔik mÀngijad on mÀngust vÀlja langenud, kuulutab esimene mÀngija mÀngu lÔppenuks. Kui ta on varem mÀngust vÀljunud, jÀlgib ta mÀngu lÔpetamist.
Selle ĂŒlesande lahendamiseks kasutan meie DSL-i Ă€ri protsesside jaoks, mis vĂ”imaldab loogikat kompaktsemalt Kotlinis kirjeldada, minimaalse boilerplate'iga.
Rakenduses app1 töötab esimese mÀngija (kes on mÀngu algataja) Àri protsess:
klass AlgneMĂ€ngija
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 klass protsessi eksemplarist: kapseldab selle sisemist olekut
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 deklaratsioon: luuakse ĂŒks kord, kasutatakse kĂ”igi
// 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
// Tegevuste kuulutamine, millest Àri-protsess koosneb
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "${players.size} mĂ€ngijat on ĂŒhendatud. Alustame?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... liitus mÀngijaga ${signal.data} ...")
}
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 ehitame protsessi graafiku kuulutatud 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 tegevusele tÀiendavad töötlajad logimiseks
sendNewGameSignal.onExit { println("Alustame mÀngu!") }
sendStopGameSignal.onExit { println("LÔpp!") }
sendPlayerOut.onExit { println("$playerName: 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 ettevĂ”tte loogika tĂ€itmisele suudab antud kood esitada Ă€riprotsessi objekti mudeli, mida saab visualiseerida diagrammina. Visualiseerijat me veel ellu ei viinud, seega pidime veidi aega kulutama joonistamisele (siin olen ma BPMN-i nomenklatuuri veidi 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Àngija kÀitumist: jÀrgmise mÀngija valimine toimub ringikaupa algoritmi alusel, mitte juhuslikult.
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}")
}Muu kÀitumine mÀngijal ei erine eelmisest, seega diagramm ei muutu.
NĂŒĂŒd on vajalik test, et kĂ”ik see hankida. TTuletan vaid testikoodi, et mitte artiklit ĂŒle koormata boilerplate'iga (tegelikult kasutasin testimisĂŒmbrust, mis loodi varem teiste Ă€riprotsesside integreerimise 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 on vaja natuke oodata, kuni mĂ€ngijad "tuttavaks" saavad.
// Ootamine sleep'i kaudu - halb lahendus, aga kÔige lihtsam.
// Ărge tehke seda 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
VÔti lock://app1/process/InitialPlayer on blokeeritud
MĂ€ngime!
VÔti lock://app1/process/InitialPlayer on vabastatud
MĂ€ngija2: Olen kohal!
MĂ€ngija3: Olen kohal!
MĂ€ngija4: Olen kohal!
MĂ€ngija5: Olen kohal!
... 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 lahkunud!
Samm 89: MĂ€ngija3 >>> MĂ€ngija4
... mÀngija MÀngija2 on vÀlja ...
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 lahkunud!
... mÀngija MÀngija4 on vÀlja ...
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 lahkunud!
... mÀngija MÀngija5 on vÀlja ...
Samm 122: MĂ€ngija3 >>> MĂ€ngija5
Samm 123: MĂ€ngija5 >>> MĂ€ngija1
MĂ€ngija5: Olen lahkunud!
Samm 124: MĂ€ngija1 >>> MĂ€ngija3
... mÀngija MÀngija5 on vÀlja ...
Samm 125: MĂ€ngija3 >>> MĂ€ngija1
Samm 126: MĂ€ngija1 >>> MĂ€ngija3
MĂ€ngija1: Olen lahkunud!
... mÀngija MÀngija1 on vÀlja ...
Samm 127: MĂ€ngija3 >>> MĂ€ngija3
MĂ€ngija3: Olen lahkunud!
Samm 128: MĂ€ngija3 >>> MĂ€ngija3
... mÀngija MÀngija3 on vÀlja ...
MĂ€ngija3: Olen lahkunud!
LÔpp!
Samm 129: MĂ€ngija3 >>> MĂ€ngija3
MÀngija3: Olen lahkunud!Kogu sellest on vÔimalik teha mitu olulist jÀreldust:
- vajalike tööriistade olemasolul saavad rakendusarendajad luua rakenduste vahelisi integreerimistegevusi, katkestamata Àriloogikat;
- integreerimiste ĂŒlesande keerukust (complexity), mis nĂ”uab insenerioskusi, on vĂ”imalik peita raamistikku, kui see on algselt arhitektuuri sisse ehitatud. Ălesande raskust (difficulty) ei saa peita, seega keerulise ĂŒlesande lahendus koodis nĂ€eb vastavalt vĂ€lja;
- Integratsiooni loogika vĂ€ljatöötamisel on oluline arvestada eventual consistency ja kĂ”igi integreerimise osaliste oleku muutuste mitte-lineaarsete omadustega. See sunnib loogikat keeruliseks muutma, et see ei sĂ”ltuks vĂ€liste sĂŒndmuste ajalisest jĂ€rjekorrast. Meie nĂ€ites peab mĂ€ngija osalema mĂ€ngus alles pĂ€rast seda, kui ta on vĂ€ljakuulutamisest teatanud: teised mĂ€ngijad jĂ€tkavad palli edastamist, kuni teave tema lahkumisest jĂ”uab ja töödeldakse kĂ”igi osaliste poolt. See loogika ei tulene mĂ€ngureeglitest ja on kompromiss valitud arhitektuuri raames.
JĂ€rgmine samm on arutada meie lahenduse erinevaid nĂŒansse, kompromisse ja muid aspekte.
KĂ”ik sĂ”numid - ĂŒhes jĂ€rjekorras
KĂ”ik integreeritud rakendused töötavad ĂŒhise integratsiooniviidikuga, mis on esitatud kui vĂ€line vahendaja, ĂŒks BPMQueue jĂ€rjekord â sĂ”numite jaoks ja ĂŒks BPMTopic teema â signaalide (sĂŒndmuste) jaoks. KĂ”ik sĂ”numid ĂŒhe jĂ€rjekorra kaudu suunamine on endas juba kompromiss. Ărilogika tasandil on nĂŒĂŒd vĂ”imalik luua nii palju uusi sĂ”numitĂŒĂŒpe, ilma et oleks vaja sĂŒsteemi struktuuri muuta. See on suur lihtsustamine, kuid sellega kaasnevad teatud riskid, mis meie tĂŒĂŒpiliste ĂŒlesannete kontekstis ei tundu liiga mĂ€rkimisvÀÀrsed.

Siin on siiski ĂŒks nĂŒanss: iga rakendus filtreerib oma sĂ”numid jĂ€rjekorrast juba sissepÀÀsu juures oma domeeninime jĂ€rgi. Domeen vĂ”ib olla mĂ€rgitud ka signaalides, kui on vaja piirata signaali ânĂ€havustâ ainult ĂŒhele rakendusele. See peaks suurendama viidiku lĂ€bilaskevĂ”imet, kuid nĂŒĂŒd peab Ă€rilooming opereerima domeeninimede jĂ€rgi: sĂ”numite adresseerimine on kohustuslik, signaalide puhul soovitatav.
Integreerimisviidiku töökindluse tagamine
Töökindlus sÔltub mitmest aspektist:
- valitud sĂ”numivahetaja on arhitektuuri kriitiliselt oluline komponent ja ĂŒksik tĂ”rkekoht: see peab olema piisavalt tĂ”rketaluv. Tuleb kasutada ainult ajaliselt tĂ”estatud lahendusi, millel on hea tugi ja suur kogukond;
- on vajalik tagada sĂ”numivahetaja kĂ”rge kĂ€ttesaadavus, milleks peab see olema fĂŒĂŒsiliselt eraldatud integreeritavatest rakendustest (rakenduste, kus on Ă€riloogika, kĂ”rge kĂ€ttesaadavuse tagamine on oluliselt keerulisem ja kallim);
- sĂ”numivahetaja peab tagama âvĂ€hemalt kordâ kohaletoimetamise garantii. See on hĂ€davajalik nĂ”ue integreerimisbusse usaldusvÀÀrseks tööks. âTĂ€pselt kordâ garanteerimisel ei ole vajadust: Ă€riprotsessid ei ole tavaliselt tundlikud sĂ”numite vĂ”i sĂŒndmuste korduvate kohaletoimetamiste suhtes, ja erisoovides, kus see on oluline, on lihtsam lisada Ă€riloogikasse tĂ€iendav kontroll kui pidevalt kasutada piisavalt âkalleidâ garantiisid;
- sĂ”numite ja signaalide saatmine tuleb hĂ”lmata Ă€riprotsesside ja domeeniandmete oleku muutmisega seotud terviktransaktsiooni. Soovitatav variant on kasutada mustrit , kuid see nĂ”uab andmebaasis tĂ€iendavat tabelit ja edastajat. JEE-rakendustes saab seda aspekti lihtsustada kohaliku JTA halduriga, kuid valitud vahendajaga ĂŒhendamine peab suudma töötada ;
- sissetulevate sĂ”numite ja sĂŒndmuste töötlejad peavad samuti töötama Ă€riprotsessi oleku muutmise tehinguga: kui selline tehing tĂŒhistatakse, peab ka sĂ”numi vastuvĂ”tt tĂŒhistama;
- sĂ”numid, mida ei Ă”nnestunud vigade tĂ”ttu edastada, tuleb koguda eraldi salvestusse (Dead Letter Queue). Selleks oleme loonud eraldi platvormi mikroteenuse, mis salvestab sellised sĂ”numid oma salvestusse, indekseerib need atribuutide jĂ€rgi (kiireks grupeerimiseks ja otsimiseks) ning pakub API-d vaatamiseks, uuesti saatmiseks sihtkohta, sĂ”numite kustutamiseks. SĂŒsteemi administraatorid saavad seda teenust kasutada oma veebi liidese kaudu;
- Brokeri seadetes tuleb kohandada korduste arvu ja viivitust edastuste vahel, et vÀhendada tÔenÀosust, et sÔnumid satuvad DLQ-sse (optimaalsete parameetrite arvutamine on peaaegu vÔimatu, kuid neid on vÔimalik katsetades kohandada);
- DLQ ladustamist tuleb pidevalt jĂ€lgida ning jĂ€lgimisse sĂŒsteem peab teavitama sĂŒsteemi administraatoreid, et nad saaks kiirelt reageerida, kui tekivad mitte-edastatud sĂ”numid. See aitab vĂ€hendada tekkiva rikke vĂ”i Ă€ri loogika vea 'kahjustuse ala';
- Integreerimispink peab olema ajutise rakenduse puudumise suhtes leige: tellimused teemale peavad olema vastupidavad ning rakenduse domeeninimi peab olema unikaalne, et rakenduse puudumise ajal ei prooviks keegi teine selle teema sÔnumeid jÀrjekorrast töödelda.
Ăriloogika juhtimise kindlustamine
Ăhele ja samale Ă€ritsĂŒkli eksemplarile vĂ”ib korraga saabuda mitu sĂ”numit ja sĂŒndmust, mille töötlemine kĂ€ivitub paralleelselt. Samal ajal peab rakenduse arendajale olema kĂ”ik lihtne ja tĂ”rkevaba.
Protsessi Ă€ri loogika töötleb igat vĂ€lissĂŒndmust, mis mĂ”jutab seda Ă€ritsĂŒklit, eraldi. Sellised sĂŒndmused vĂ”ivad olla:
- Ă€ritsĂŒkli eksemplari kĂ€ivitamine;
- kasutaja tegevus, mis on seotud Ă€ritsĂŒkli sees oleva tegevusega;
- sĂ”numi vĂ”i signaali saamine, millele Ă€ritsĂŒkli eksemplar on allakirjutanud;
- Ă€ritsĂŒkli eksemplari seadistatud taimeri aktiveerimine;
- juhtiv mÔju API kaudu (nÀiteks protsessi hÀdapÀrane katkestamine).
Iga selline sĂŒndmus vĂ”ib muuta Ă€ritegevuse protsessi eksemplari olekut: mĂ”ned tegevused vĂ”ivad lĂ”ppeda ja teised alata, vĂ”ib muutuda pĂŒsivate omaduste vÀÀrtus. Iga tegevuse lĂ”petamine vĂ”ib aktiveerida ĂŒhe vĂ”i mitu jĂ€rgmist tegevust. Need omakorda vĂ”ivad peatuda teiste sĂŒndmuste ootamisel vĂ”i, kui neid ei vajata tĂ€iendavaid andmeid, vĂ”ivad nad lĂ”petada samas tehingus. Enne tehingu lĂ”petamist salvestatakse Ă€ritegevuse protsessi uus olek andmebaasi, kus see ootab jĂ€rgmise vĂ€list sĂŒndmust.
Ăritegevuse protsessi pĂŒsivad andmed, mis on salvestatud relatsioonilisse andmebaasi, on vĂ€ga mugav sĂŒmbioosi töötlemise punkt, kui kasutada SELECT FOR UPDATE. Kui ĂŒhel tehingul Ă”nnestub saada Ă€ritegevuse protsessi olek andmebaasist selle muutmiseks, ei saa ĂŒkski teine tehing sama olekut samal ajal teisele muutmiseks saada, ja pĂ€rast esimesena lĂ”petatud tehingut saab teine garanteeritult juba muudetud oleku.
Kasutades andmebaasi serveri poolseid pessimistlikke lukustusi, tÀidame kÔik vajalikud nÔuded , samal ajal sÀilitades rakenduse ÀriÔiguse skaleeritavuse, suurendades kÀimasolevate instantside arvu.
Kuid pessimistlikud lukustused Àhvardavad meid surnud lÔksudega, seega tuleks SELECT FOR UPDATE ikkagi mÔistlikult ajutiselt piirata, et vÀltida surnud lÔkse ÀÀrmuslikes ÀriÔiguse juhtumites.
Veel ĂŒks probleem on Ă€riprotsessi kĂ€ivitamise sĂŒnkroniseerimine. Kuni Ă€riprotsessi instants ei ole olemas, pole ka selle olekut andmebaasis, seega ei sobi kirjeldatud meetod. Kui on vajalik tagada Ă€riprotsessi instantsi unikaalsus teatud ulatuses, siis on vajalik mingi sĂŒnkroniseerimisobjekt, mis on seotud protsessiklassi ja vastava ulatusega. Selle probleemi lahendamiseks kasutame teistsugust lukustamismehhanismi, mis vĂ”imaldab lukustada suvalist ressurssi, mÀÀratletud URI formaadis vĂ”ti, vĂ€lise teenuse kaudu.
Meie nÀidetes sisaldab Àriprotsess InitialPlayer kuulutust
uniqueConstraint = UniqueConstraints.singletonSeetÔttu logis on sÔnumid vastava vÔtme lukustamise ja vabastamise kohta. Teiste Àriuudiste jaoks selliseid sÔnumeid ei ole: uniqueConstraint ei ole mÀÀratud.
Ăriprotsesside probleemid pĂŒsiva olekuga
MĂ”nikord aitab pĂŒsiva olekuga toimetamine, kuid samas vĂ”ib see arenduses ka vĂ€ga segada.
Probleemid algavad siis, kui on vaja muuta Ă€ri loogikat ja/vĂ”i Ă€ri protsessi mudelit. Mitte iga selline muudatus ei osutu vanade Ă€ri protsesside seisukohaga ĂŒhilduvaks. Kui andmebaasis on palju 'elavaid' eksemplare, siis ĂŒhilduvate muudatuste tegemine vĂ”ib tekitada palju ebameeldivusi, millega oleme sageli silmitsi seisnud jBPM-i kasutamisel.
SĂ”ltuvalt muudatuste sĂŒgavusest saab tegutseda kahel viisil:
- luua uue Ă€ri protsessi tĂŒĂŒbi, et mitte teha vanadesse ĂŒhilduvaid muudatusi ja kasutada seda uute eksemplaride kĂ€ivitamisel vanade asemel. Vanad eksemplarid jĂ€tkavad 'vana' jĂ€rgi töötamist;
- migreerida Ă€ri protsesside pĂŒsivat olekut Ă€ri loogika ajakohastamisel.
Esimene lÀhenemine on lihtsam, kuid sellel on oma piirangud ja puudused, nÀiteks:
- Àri loogika dubleerimine paljudes Àriprotsesside mudelites, Àri loogika mahu suurenemine;
- tihti on vajalik kohene ĂŒleminek uuele Ă€riloogikale (integratsioonĂŒlesannete puhul â peaaegu alati);
- arendaja ei tea, millal on vÔimalik eemaldada aegunud mudelid.
Praktikas kasutame mÔlemat lÀhenemist, kuid oleme teinud mitmeid otsuseid, et elu lihtsamaks teha:
- andmebaasis sĂ€ilitatakse Ă€riprotsessi pĂŒsiv olek arusaadavas ja kergesti töödeldavas vormis: JSON-formaadis reas. See vĂ”imaldab migratsioone nii rakenduse sees kui ka vĂ€ljaspool. ĂĂ€rmuslikul juhul saab ka kĂ€sitsi kohandada (eriti kasulik arendamise ja tĂ”rkeotsingu ajal);
- integreeritud Ă€ri loogika ei kasuta Ă€riprotsesside nimesid, et igal ajal saaks asendada ĂŒhe osaleva protsessi rakenduse uue, uue nimega (nĂ€iteks âInitialPlayerV2â). Side toimub sĂ”numite ja signaalide nimede kaudu;
- protsessi mudelil on versiooni number, mida me suurendame, kui teeme sellesse mudelisse ĂŒhilduvaid muudatusi, ning see number salvestatakse koos protsessi eksemplari olekuga;
- protsessi pĂŒsiv olek loetakse algselt andmebaasist mugavasse objekti mudelisse, millega migreerimisprotseduur saab töötada, kui mudeli versiooni number on muutunud;
- migreerimisprotseduur paikneb Àri- loogika lÀhedal ja kutsutakse vÀlja "lÔdvalt" iga Àri- protsessi eksemplari jaoks protsessi taastamise hetkel andmebaasist;
- kui on vaja kĂ”igi protsessi eksemplaride olek kiiresti ja sĂŒnkroonselt migreerida, rakendatakse klassikalisi lahendusi andmebaasi migreerimiseks, kuid seal tuleb töötada JSON-iga.
Kas on vaja veel ĂŒhte Ă€riprotsesside raamistikku?
Artiklis toodud lahendused on aidanud meil oluliselt lihtsustada igapĂ€evaelu, laiendada rakenduste arendamise valdkonda ja muuta Ă€ri loogika eraldamise ideed mikroteenusteks atraktiivsemaks. Selle saavutamiseks on tehtud palju tööd, loodud vĂ€ga "kergekaaluline" raamistik Ă€ri protsesside jaoks ning teeninduskomponendid mĂ€rkidaud probleemide lahendamiseks laias rakenduste valdkonnas. Meil on soov nende tulemusi jagada, tuua ĂŒldkomponendid avatud ligipÀÀsu alla vabaks litsentsiks. See nĂ”uab teatud pingutusi ja aega. Arusaam selliste lahenduste nĂ”udlusest vĂ”iks olla meile tĂ€iendav stiimul. Pakutud artiklis on raamistikule endale vĂ€ga vĂ€he tĂ€helepanu pööratud, kuid mĂ”ningaid vĂ”imalusi on nĂ€ha toodud nĂ€idetes. Kui me siiski avaldame oma raamistikku, pĂŒhendame sellele eraldi artikli. Samuti oleksime tĂ€nulikud, kui jĂ€taksite vĂ€ikese tagasiside, vastates kĂŒsimusele:
Ainult registreeritud kasutajad saavad kĂŒsitluses osaleda. , palun.
kas on veel vaja ĂŒhte raamistikku Ă€ri protsesside jaoks?
18,8%jah, oleme ammu otsinud midagi sellist
12,5%huvitav teada saada rohkem teie rakenduse kohta, see vÔib osutuda kasulikuks
6,2%kasutame ĂŒht olemasolevat raamistikku, kuid kaalume vahetamist
18,8%kasutame ĂŒht olemasolevat raamistikku, kĂ”ik sobib
18,8%saame hakkama ilma raamistikuta
25,0%kirjutame ise
HÀÀletas 16 kasutajat. 7 kasutajat jÀi erapooletuks.
Allikas: habr.com
