BPM stiilis integreerimine

BPM stiilis integreerimine

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.

BPM stiilis integreerimine
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).

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

VĂ€ljapÀÀs sellest olukorrast oli mootori integreerimine jBPM 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. BPMN2.

BPM stiilis integreerimine
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.

BPM stiilis integreerimine

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

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.

BPM stiilis integreerimine

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.

BPM stiilis integreerimine

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

BPM stiilis integreerimine

Rakendus app2 sisaldab teise mÀngija Àriprotsessi:

class RandomPlayer

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

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

class PlayersList: ArrayList()

class RandomPlayer : ProcessImpl(randomPlayerModel) {

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

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

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

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

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

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

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

Diagramm:

BPM stiilis integreerimine

Rakenduses app3 muudame mÀ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.

BPM stiilis integreerimine

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 Tegevus Outbox, 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 XA;
  • 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 DLQ (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 ACID, 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.singleton

SeetÔ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:

  1. 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;
  2. 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. Logige sisse, 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

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster