Integrimi në stilin BPM

Integrimi në stilin BPM

Përshëndetje, Habr!

Kompania jonë specializohet në zhvillimin e zgjidhjeve softuerike të klasës ERP, ku pjesa më e madhe janë sistemet transaksionale me një volum të madh logjike biznesi dhe dokumentacioni si sistemi i menaxhimit të dokumenteve. Versionet moderne të produkteve tona bazohen në teknologjitë JavaEE, por ne gjithashtu eksperimentohemi aktivisht me mikroshërbime. Një nga vendet më problematike të këtyre zgjidhjeve është integrimi i nën-sistemeve të ndryshme që i përkasin domain-ve përkatëse. Detyrat e integrimit gjithmonë na kanë shkaktuar shumë dhimbje koke, pavarësisht nga stilet arkitektonike, staket teknologjike dhe kornizat që kemi përdorur, megjithatë kohët e fundit ka pasur progres në zgjidhjen e këtyre problemeve.

Në artikullin që ju ofroj, do të flas për përvojën dhe kërkimet arkitektonike të NPO «Krista» në këtë fushë të shënuar. Po ashtu, do të shqyrtojmë një shembull të një zgjidhjeje të thjeshtë të detyrës së integrimit nga këndvështimi i zhvilluesit aplikativ dhe do të zbulojmë se çfarë ka prapa kësaj thjeshtësie.

Kërcënimi

Zgjidhjet arkitektonike dhe teknike të përshkruara në artikullin e propozuar janë bazuar në përvojën time personale në kontekstin e detyrave specifike. Këto zgjidhje nuk pretendojnë për universalesh dhe mund të ngelin jo optimale në kushte të tjera përdorimi.

Çfarë ka të bëjë BPM me këtë?

Për të përgjigjur këtë pyetje, duhen të thellohemi pak në specifikën e detyrave aplikative të zgjidhjeve tona. Pjesa kryesore e logjikës biznesore në sistemin tonë tipik transaksional është futja e të dhënave në Baza të Dhënash përmes ndërfaqeve përdoruesve, verifikimi manual dhe automatizuar i këtyre të dhënave, kalimi i tyre në një proces pune, publikimi në një sistem tjetër / bazë analitike / arkiv, formimi i raporteve. Kështu, funksioni kyç i sistemit për klientët është automatizimi i proceseve të tyre të brendshme biznesore.

Për lehtësi, ne përdorim në komunikim termin «document» si një abstraksion të një grupi të dhënash të bashkuara nga një çelës i zakonshëm, të cilit mund t'i «privohet» një proces pune të caktuar.
Por si të sillemi me logjikën e integrimit? Sepse detyra e integrimit lind nga arkitektura e sistemit, e cila është «ndarë» në pjesë JO sipas kërkesës së klientit, por nën ndikimin e faktorëve të tjerë:

  • në përputhje me ligjin e Conway-së;
  • si pasojë e ripërdorimit të nën-sistemeve, të zhvilluara më parë për produkte të tjera;
  • për vendimin e arkitektit, duke u bazuar në kërkesat jo-funksionale.

Ka një tundim të madh për të ndarë logjikën e integrimit nga logjika biznesore e punës kryesore, për të mos ndotur logjikën biznesore me artefakte integrimi dhe për t'i shpëtuar zhvilluesit aplikativ nga nevoja për të kujtuar veçoritë e peizazhit arkitektonik të sistemit. Ky qasje ka disa përfitime, megjithatë, praktika tregon se është e paefektshme:

  • zgjidhjet e detyrave të integrimit zakonisht zhvillohen në variantet më të thjeshta në formën e thirrjeve sinkrone për shkak të kufizimit të pikave të zgjerimit në realizimin e punës kryesore (për disavantazhet e integrimit sinkron - pak më poshtë);
  • artefaktet e integrimit prapë depërtojnë në logjikën kryesore të biznesit, kur kërkohet feedback nga një nën-sistem tjetër;
  • zhvilluesi aplikativ injoron integrimin dhe mund ta prishë lehtë atë, duke ndryshuar punën kryesore;
  • sistemi në fund të fundit nuk është një tërë nga pikëpamja e përdoruesit, bëhen të dukshme "ndarjet" midis nën-sistemeve, shfaqen operacione të tepërta të përdoruesit që iniciatojnë transferimin e të dhënave nga një nën-sistem në tjetrin.

Një qasje tjetër është shqyrtimi i ndërveprimeve integruese si një pjesë të pazëvendësueshme të logjikës kryesore të biznesit dhe workflow-it. Për të parandaluar që kërkesat për kualifikimin e zhvilluesve të aplikacioneve të arrijnë përmasa të pakontrollueshme, krijimi i ndërveprimeve të reja integruese duhet të realizohet lehtë dhe pa frikë, me mundësi të minimalizuara për zgjedhjen e mënyrës së zgjidhjes. Kjo është më e vështirë sesa duket: instrumenti duhet të jetë mjaft i fuqishëm për të ofruar përdoruesit një shumëllojshmëri opsionesh për përdorimin e tij dhe në të njëjtën kohë të mos lejojë që përdoruesi të gjejë veten në situata të dëmshme. Ekzistojnë shumë pyetje që inxhinieri duhet të përgjigjet në kontekst të detyrave të integrimit, por për të cilat zhvilluesi i aplikacioneve nuk duhet të mendojë në punën e tij të përditshme: kufijtë e transaksioneve, konsistenca, atomariteti, siguria, shkallëzimi, shpërndarja e ngarkesave dhe burimeve, routing, marshalling, shpërndarja dhe kalimi i konteksteve, etj. Duhet të ofrohen zhvilluesve të aplikacioneve modele zgjidhjesh mjaft të thjeshta, në të cilat janë të fshehura përgjigjet për të gjitha këto pyetje. Këto modele duhet të jenë mjaft të sigurta: logjika e biznesit ndryshon shumë shpesh, gjë që rrit rrezikun e inkurajimit të gabimeve, dhe kostoja e gabimeve duhet të mbetet në nivele të ulta.

Por çfarë lidhjeje ka kjo me BPM? Ekzistojnë shumë variante për realizimin e workflow-it...
Vërtet, në zgjidhjet tona është shumë popullore një realizim tjetër i proceseve të biznesit – përmes caktimit deklarativ të diagramës së kalimeve të gjendjeve dhe lidhjes së trajtuesve me logjikën e biznesit në kalime. Në këtë rast, gjendja që përcakton pozitat aktuale të "dokumentit" në procesin e biznesit është një atribut i vetë "dokumentit".

Integrimi në stilin BPM
Kështu duket procesi në fillim të projektit

Popullariteti i kësaj realizimi është i shkaktuar nga thjeshtësia e relativshme dhe shpejtësia e krijimit të proceseve të biznesit linear. Megjithatë, ndërsa sistemet e softuerit bëhen gjithnjë e më të komplikuara, pjesa automatike e procesit të biznesit zgjeron dhe komplikohet. Do të ketë nevojë për dekompozim, riciklim të pjesëve të proceseve, si dhe për degëzim të proceseve në mënyrë që çdo degë të ekzekutohet paralelisht. Nën këto kushte, instrumenti bëhet i papërshtatshëm, dhe diagrami i kalimeve të gjendjeve humbet informativen (ndërveprimet integruese nuk pasqyrohen fare në diagram).

Integrimi në stilin BPM
Ky është procesi pas disa iteracioneve të saktësimit të kërkesave.

Zgjidhja nga kjo situatë ishte integrimi i motorit jBPM në disa produkte me proceset e biznesit më të komplikuara. Në perspektivën afatshkurtër, kjo zgjidhje pati një sukses të caktuar: u krijua mundësia për të realizuar procese të komplikuara të biznesit me mbajtjen e një diagrami mjaft informativ dhe aktual në notimin BPMN2.

Integrimi në stilin BPM
Një pjesë e vogël e procesit të komplikuar të biznesit

Në perspektivën afatgjatë, zgjidhja nuk justifikoi pritjet: kërkesat e larta për punë gjatë krijimit të proceseve të biznesit përmes mjeteve vizuale nuk lejuan arritjen e treguesve të pranueshëm të produktivitetit, dhe vetë instrumenti u bë një nga më të pazakontët midis zhvilluesve. Kishte gjithashtu kritika ndaj struktures së brendshme të motorit, që çoi në shfaqjen e shumë "patch" dhe "ndihmave".

Momenti kryesor pozitiv i përdorimit të jBPM ishte njohja e përfitimit dhe dëmshkimit nga pranimi i një gjendjeje persistente për ekzemplarët e procesit të biznesit. Po ashtu, ne pamë mundësinë e aplikimit të qasjes procesore për realizimin e protokolleve të ndërlidhjes komplekse midis aplikacioneve të ndryshme duke përdorur ndërveprime asinkrone përmes sinjaleve dhe mesazheve. Prania e një gjendjeje persistente luan një rol të rëndësishëm në këtë.

Bazuar në atë që u tha, mund të nxjerrim përfundimin: qasja procesore në stilin BPM na lejon të zgjidhim një gamë të gjerë detyrash për automatizimin e proceseve të biznesit që vazhdimisht po komplikohet, të inkorporojmë aktivitetet integruese në këto procese dhe të ruajmë mundësinë e paraqitjes vizuale të procesit të realizuar në notimin e përshtatshëm për këtë.

Disavantazhet e thirrjeve sinkrone si një model integrimi

Integrimi sinkron kupton thirrjen më të thjeshtë bllokuese. Një nënsi është ana e serverit dhe ofron një API me metoden e nevojshme. Nënsi tjetër është ana e klientit dhe, në momentin e duhur, kryen thirrjen duke pritur rezultatin. Në varësi të arkitekturës së sistemit, ana e klientit dhe e serverit mund të vendosen ose në një aplikacion dhe proces të vetëm, ose në të ndryshme. Në rastin e dytë, nevojitet të aplikohet një realizim i caktuar i RPC dhe të sigurohet marshalling i parametrave dhe të rezultatit të thirrjes.

Integrimi në stilin BPM

Ky model integrimi ka një numër të konsiderueshëm disavantazhesh, por përdoret shumë në praktikë për shkak të thjeshtësisë së tij. Shpejtësia e implementimit është joshëse dhe e shtyn atë të aplikohet përsëri dhe përsëri në kushte me afate 'të nxehta', duke regjistruar zgjidhjen në borxhin teknik. Por ndodhin edhe raste kur zhvilluesit e pamësuar e përdorin atë pa e kuptuar, thjesht nuk dyshojnë për pasojat negative.

Përveç rritjes më të dukshme të lidhshmërisë së nënsive, ka edhe probleme më pak të dukshme me 'zgjerimin' dhe 'shtrirjen' e transaksioneve. Në të vërtetë, nëse logjika e biznesit bën disa ndryshime, atëherë nuk mund të shmangen transaksionet, dhe transaksionet, nga ana tjetër, bllokojnë disa burime të aplikacionit që preken nga këto ndryshime. Kështu, derisa një nënsi të mos presë përgjigjen nga tjetra, ajo nuk do të jetë në gjendje të përfundojë transaksionin dhe të heqë bllokimet. Kjo rrit ndjeshëm rrezikun e shfaqjes së efekteve të ndryshme:

  • humbet përgjigjshmëria e sistemit, përdoruesit presin për një kohë të gjatë për përgjigjet ndaj kërkesave;
  • serveri ndalon së përgjigjuri ndaj kërkesave të përdoruesve për shkak të mbushjes së pool-it të tije: shumica e thirrjeve janë 'ndaluar' për shkak të bllokimit të burimit të zënë nga transaksioni;
  • fillojnë të shfaqen dødllokë: probabiliteti i shfaqjes së tyre varet shumë nga kohëzgjatja e transaksioneve, numri i logjikës së biznesit të përfshirë në transaksion dhe bllokimet;
  • shfaqen gabimet e skadimit të kohës së transaksionit;
  • serveri 'bjerë' për shkak të OutOfMemory, nëse detyra kërkon përpunimin dhe ndryshimin e sasisë së madhe të të dhënave, dhe pranija e integrimeve sinkrone e komplikon thelbësisht ndarjen e përpunimit në transaksione më 'të lehta'.

Nga një pikëpamje arkitektonike, përdorimi i thirrjeve bllokuese gjatë integrimit çon në humbjen e kontrollit mbi cilësinë e sistemeve të veçanta: nuk është e mundur të sigurohen treguesit e caktuar të cilësisë për një sistem në izolim nga treguesit e cilësisë së sistemit tjetër. Nëse sistemet zhvillohen nga grupe të ndryshme, kjo është një problem i madh.

Gjithçka bëhet edhe më interesante nëse sistemet e integruara ndodhen në aplikacione të ndryshme dhe duhet të bëhen ndryshime sinkrone nga të dy anët. Si mund të sigurojmë transaksionalitetin e këtyre ndryshimeve?

Nëse ndryshimet bëhen me transaksione të veçanta, atëherë do të nevojitet të sigurohet përpunimi i besueshëm i përjashtimeve dhe kompensimit, dhe kjo plotësisht anulon përparësinë kryesore të integrimeve sinkrone – thjeshtësinë.

Po ashtu na vjen në mendje transaksionet e shpërndara, por ne nuk i përdorim ato në zgjidhjet tona: është e vështirë të sigurohet besueshmëria.

«Saga» si një zgjidhje për problemin e transaksioneve

Me rritjen e popullaritetit të mikroshërbimeve, po fiton gjithnjë e më shumë kërkesë Saga Pattern.

Ky model zgjidh shkurtimisht problemet e përmendura më sipër të transaksioneve të gjata, si dhe zgjeron mundësitë për menaxhimin e gjendjes së sistemit nga logjika biznesore: kompensimi pas një transaksioni të dështuar mund të mos e rikthejë sistemin në gjendjen fillestare, por të ofrojë një rrugë alternative për përpunimin e të dhënave. Kjo gjithashtu lejon që të mos përsëriten hapat e përpunimit të të dhënave që janë përfunduar me sukses gjatë përpjekjeve të përsëritura për të çuar procesin në një përfundim «të mirë».

Çfarë është interesante, në sistemet monolitike ky model është gjithashtu i rëndësishëm, kur flitet për integrimin e sistemeve të lidhura dobët dhe kur vërehen efekte negative të shkaktuara nga transaksionet e gjata dhe bllokimet përkatëse të burimeve.

Në lidhje me proceset tona të biznesit në stilin BPM, implementimi i «Sagave» doli të ishte shumë i lehtë: hapat e veçantë të «Sagës» mund të përcaktohen si aktivitete brenda procesit të biznesit, dhe gjendja persistente e procesit të biznesit përcakton gjithashtu gjendjen e brendshme të «Sagës». Pra, nuk na nevojitet asnjë mekanizëm koordinimi të additional. Do të nevojitet vetëm një broker mesazhesh me mbështetje për garancitë «të paktën një herë» si transport.

Por edhe ky zgjidhje ka një «çmim» të saj:

  • logjikë biznesi bëhet më e komplikuar: duhet të përpunohen kompensimet;
  • do të nevojitet të heqim dorë nga konsistenca e plotë, që mund të jetë veçanërisht delikate për sistemet monolite;
  • arkitektura paksa komplikohet, duke krijuar një nevojë shtesë për një broker mesazhesh;
  • do të kërkohen mjete të tjera monitorimi dhe administrimi (pavarësisht se në përgjithësi, kjo është një gjë e mirë: cilësia e shërbimit të sistemit do të përmirësohet).

Për sistemet monolite, justifikimi i përdorimit të "Saga" nuk është aq i qartë. Për mikroshërbimet dhe SOA të tjera, ku ndoshta tashmë ka një broker, dhe konsistenca e plotë është sakrifikuar që në fillim të projektit, përfitimi nga përdorimi i këtij modeli mund të tejkalojë ndjeshëm disavantazhet, sidomos nëse ka një API të përshtatshme në nivelin e logjikës biznesore.

Inkasulimi i logjikës biznesore në mikroshërbime

Kur filluam të eksperimentojmë me mikroshërbime, u shfaq një pyetje e arsyeshme: ku të vendosim logjikën biznesore të domeneve në lidhje me shërbimin që ofron persistencën e të dhënave të domeneve?

Duke shikuar arkitekturën e BPMS-ve të ndryshme, mund të duket e arsyeshme të ndahen logjikat biznesore nga persistenza: të krijohet një shtresë mikroshërbimesh të platformës dhe që nuk varen nga domenet, duke formuar një ambient dhe kontejner për ekzekutimin e logjikës biznesore të domeneve, ndërsa persistenca e të dhënave të domeneve të formohet si një shtresë e veçantë me mikroshërbime shumë të thjeshta dhe të lehta. Proceset e biznesit në këtë rast kryejnë orkestrimin e shërbimeve të shtresës së persistencës.

Integrimi në stilin BPM

Ky qasje ka një avantazh shumë të madh: është e mundur të shtohet sa më shumë funksionalitet në platformë, dhe "të mbushet" për këtë do të jetë vetëm shtresa përkatëse e mikroshërbimeve të platformës. Proceset e biznesit nga çdo domene menjëherë kanë mundësinë të shfrytëzojnë funksionalitetin e ri të platformës, sapo ajo të azhurnohet.

Një analizë më e hollësishme zbuloi disavantazhe të rëndësishme të këtij qasje:

  • shërbimi i platformës, i cili ekzekuton logjikën biznesore në shumë domene, bart rreziqe të mëdha si një pikë e vetme dështimi. Ndryshimet e shpeshta në logjikën biznesore rrisin rrezikun e shfaqjes së gabimeve, të cilat çojnë në dështime që përhapen në të gjithë sistemin;
  • problemet e performancës: logjika biznesore punon me të dhënat e saj përmes një ndërfaqeje të ngushtë dhe të ngadaltë:
    • të dhënat do të marshallohet dhe do të përshkohen përsëri përmes shtresës rrjetore;
    • shërbimi i domainit shpesh do të dorëzojë më shumë të dhëna se sa kërkohet nga logjika biznesore për përpunim, për shkak të mundësive të pamjaftueshme të parametrizimit të pyetjeve në nivelin e API të jashtëm të shërbimit;
    • disa pjesë të pavarura të logjikës biznesore mund të kërkojnë përsëri të njëjtat të dhëna për përpunim (këto probleme mund të zbuten me shtimin e komponentëve sesionalë që ruajnë të dhënat, por kjo e komplikon më tej arkitekturën dhe krijon probleme përkatësie të të dhënave dhe invalidimin e cache-së);
  • problemet e transaksionit:
    • proceset biznesore me një gjendje të qëndrushme, ruajtja e së cilës merret në përsipër nga shërbimi i platformës, do të mos pajtohen me të dhënat e domainit, dhe nuk parashikohet ndonjë rrugë e thjeshtë për zgjidhjen e kësaj probleme;
    • çlirimi i bllokimit të të dhënave të domainit jashtë transaksionit: nëse logjika e domeneve kërkon të bëjë ndryshime, pas verifikimit të saktësisë së të dhënave aktuale, është e nevojshme të përjashtohet mundësia e ndryshimit konkurues të të dhënave që po përpunohen. Bllokimi i jashtëm i të dhënave mund të ndihmojë në zgjidhjen e problemit, por ky zgjidhje sjell rreziqe të tjera dhe zvogëlon besueshmërinë e përgjithshme të sistemit;
  • vështirësi të tjera gjatë azhurnimit: në disa raste, shërbimi i qëndrueshmërisë dhe logjika e biznesit duhet të azhurnohen sinkron ose në një rend të rreptë.

Në fund të fundit, duhej të ktheheshim te fillimet: të inkorporonim të dhënat e domeneve dhe logjikën e domeneve në një mikrosherbim të vetëm. Ky qasje e thjeshton perceptimin e mikrosherbimit si një komponent tërësor në sistem dhe nuk gjeneron problemet e mësipërme. Kjo gjithashtu ka një çmim:

  • është e nevojshme standardizimi i API për ndërveprimin me logjikën e biznesit (në veçanti, për të siguruar aktivitete përdoruesish në proceset biznesore) dhe shërbimeve të platformës API; kërkohet një qasje më e kujdesshme ndaj ndryshimeve të API, përputhshmërisë direkte dhe të kundërt;
  • është e nevojshme shtimi i bibliotekave për runtime për të siguruar funksionimin e logjikës së biznesit në secilin nga këto mikrosherbime, dhe kjo sjell kërkesa të reja për këto biblioteka: lehtësinë dhe minimumin e varësive transitore;
  • zhvilluesit e logjikës së biznesit duhet të kenë kujdes për versionet e bibliotekave: nëse ndonjë mikroshërbim nuk është prekur për një kohë të gjatë, është për t'u pritur që në të do të ketë një version të vjetruar të bibliotekave. Kjo mund të bëhet një pengesë e papritur për shtimin e një funksionaliteti të ri dhe mund të kërkojë migrimin e logjikës së vjetër të biznesit të këtij shërbimi në versionet e reja të bibliotekave, nëse ka pasur ndryshime të papajtueshme midis versioneve.

Integrimi në stilin BPM

Shtresa e shërbimeve të platformës në një arkitekturë të tillë gjithashtu ekziston, por kjo shtresë nuk formon më një konteiner për realizimin e logjikës domene të biznesit, por thjesht mjedisin e saj, duke ofruar funksione ndihmëse "platformike". Kjo shtresë është e nevojshme jo vetëm për të ruajtur lehtësinë e mikroshërbimeve domene, por gjithashtu për të centralizuar menaxhimin.

Për shembull, aktivitetet e përdoruesve në proceset e biznesit krijojnë detyra. Megjithatë, gjatë punës me detyrat, përdoruesi duhet të shohë detyrat nga të gjitha domainet në një listë të përgjithshme, dhe prandaj duhet të ketë një shërbim platformik për regjistrimin e detyrave, i çliruar nga logjika domene të biznesit. Ruajtja e inkapsulimit të logjikës së biznesit në këtë kontekst është mjaft problematike, dhe kjo është një tjetër kompromis i kësaj arkitekture.

Integrimi i proceseve të biznesit nga perspektiva e zhvilluesit aplikativ.

Siç u tha më lart, zhvilluesi aplikativ duhet të jetë i abstraguar nga karakteristikat teknike dhe inxhinierike të realizimit të ndërveprimit të disa aplikacioneve, në mënyrë që të pritet një produktivitet i mirë në zhvillim.

Të përpiqemi të zgjidhim një detyrë të integrimit mjaft të vështirë, e cila është krijuar në mënyrë të veçantë për këtë artikull. Kjo do të jetë një detyrë "loje" me pjesëmarrjen e tre aplikacioneve, ku secili prej tyre përcakton një emër domeni: "app1", "app2", "app3".

Brenda secilit aplikacion, fillojnë proceset e biznesit, të cilat fillojnë "të luajnë me top" përmes autobusit të integrimit. Si top do të përdoren mesazhet me emrin "Ball".

Rregullat e lojës:

  • lojtari i parë - iniciatori. Ai fton lojtarët e tjerë në lojë, fillon lojën dhe mund ta mbyllë atë në çdo moment;
  • lojtarët e tjerë shpallin pjesëmarrjen e tyre në lojë, "shkruajnë" me njëri-tjetrin dhe me lojtarin e parë;
  • më në fund, duke marrë topin, lojtari zgjodhi një lojtar tjetër pjesëmarrës dhe i kalon të atin. Po mbahen llogaritë e numrit total të kalimeve;
  • çdo lojtar ka "energji", e cila zvogëlohet me çdo kalim topi nga ky lojtar. Pasi të skadojë energjia, lojtarit i hiqet mundësia për të luajtur, duke shpallur largimin e tij;
  • nëse lojtari mbetet vetëm, ai menjëherë shpall largimin;
  • kur të gjithë lojtarët dalin, lojtari i parë shpall përfundimin e lojës. Nëse ai kishte dalë më parë nga loja, ai mbetet për të vëzhguar lojën për ta përfunduar atë.

Për të zgjidhur këtë problem, do të përdor DSL-në tonë për proceset biznesore, e cila lejon të përshkruaj logjikën në Kotlin në mënyrë kompakte, me minimumin e kodit përgjithësues.

Në aplikacionin app1 do të funksionojë procesi biznesor i lojtarit të parë (ai që nis lojën):

klasë LojtariFillestar

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

// Ky kjo eshte klasa e instancës së procesit: inkapsulon gjendjen e saj të brendshme
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
}

// Ky është deklarimi i modelit të procesit: krijohet një herë, përdoret nga të gjitha
// instancat e procesit të klasës përkatëse
val initialPlayerModel = processModel(name = "InitialPlayer",
                                                     version = 1) {

    // Sipas rregullave, lojtari i parë është iniciatori i lojës dhe duhet të jetë i vetmi
    uniqueConstraint = UniqueConstraints.singleton

    // Deklarojmë aktivitetet, nga të cilat përbëhet procesi i biznesit
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "${players.size} lojtarë janë bashkuar. Të fillojmë?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... lojtari ${signal.data} u bashkua ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... lojtari ${signal.data} doli ...")
    }
    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
    }

    // Tani konstruktin grafikun e procesit nga aktivitetet e shpallura
    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)
    }

    // Ngjitim të trajtesave shtesë në aktivitetet për regjistrimin e aktivitetit
    sendNewGameSignal.onExit { println("Të luajmë!") }
    sendStopGameSignal.onExit { println("Ndalo!") }
    sendPlayerOut.onExit { println("$playerName: Unë dola!") }
}

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

Përveç ekzekutimit të logjikës biznesore, kodi i paraqitur ka aftësinë për të dhënë modelin objektor të procesit të biznesit, i cili mund të vizualizohet si një diagram. Vizualizuesin ende nuk e kemi realizuar, prandaj na duhej të shpenzonim pak kohë për të vizatuar (këtu e kam thjeshtuar pak notacionin BPMN në lidhje me përdorimin e porteve, për të përmirësuar konsistencën e diagramit me kodin e paraqitur):

Integrimi në stilin BPM

Aplikacioni app2 do të përfshijë procesin e biznesit të një lojtarit tjetër:

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}")
}

Diagrami:

Integrimi në stilin BPM

Në aplikimin app3 do ta bëjmë lojtarin pak më ndryshe: në vend të zgjedhjes rastësore të lojtarit tjetër, ai do të veprojë sipas algoritmit round-robin:

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<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

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

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

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

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

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

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

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

Në të tjera, sjellja e lojtarit nuk ndryshon nga e mëparshme, kështu që diagrami mbetet i njëjtë.

Tani ne kemi nevojë për një test për ta ekzekutuar gjithë këtë. Do të jap vetëm kodin e testit vetë, në mënyrë që të mos e mbush artikullin me bojlerplak (në të vërtetë, përdora mjedisin e testit të krijuar më parë për të testuar integrimin e proceseve të tjera të biznesit):

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");
    // Tani duhet të presim pak, derisa lojtarët "të njohin" njëri-tjetrin.
    // Të presim përmes sleep është një zgjidhje e dobët, por është më e thjeshtë.
    // Mos e bëni këtë në teste serioze!
    Thread.sleep(1000);
    // Fillojmë lojën, duke mbyllur aktivitetin e përdoruesit
    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;
}

Po fillojmë testin, shikojmë logun:

console output

Çelësi lock://app1/process/InitialPlayer është bllokuar
Le të luajmë!
Çelësi lock://app1/process/InitialPlayer është çbllokuar
Lojtari2: Unë jam këtu!
Lojtari3: Unë jam këtu!
Lojtari4: Unë jam këtu!
Lojtari5: Unë jam këtu!
... bashko lojtarin Lojtari2 ...
... bashko lojtarin Lojtari4 ...
... bashko lojtarin Lojtari3 ...
... bashko lojtarin Lojtari5 ...
Hapi 1: Lojtari1 >>> Lojtari3
Hapi 2: Lojtari3 >>> Lojtari5
Hapi 3: Lojtari5 >>> Lojtari3
Hapi 4: Lojtari3 >>> Lojtari4
Hapi 5: Lojtari4 >>> Lojtari3
Hapi 6: Lojtari3 >>> Lojtari4
Hapi 7: Lojtari4 >>> Lojtari5
Hapi 8: Lojtari5 >>> Lojtari2
Hapi 9: Lojtari2 >>> Lojtari5
Hapi 10: Lojtari5 >>> Lojtari4
Hapi 11: Lojtari4 >>> Lojtari2
Hapi 12: Lojtari2 >>> Lojtari4
Hapi 13: Lojtari4 >>> Lojtari1
Hapi 14: Lojtari1 >>> Lojtari4
Hapi 15: Lojtari4 >>> Lojtari3
Hapi 16: Lojtari3 >>> Lojtari1
Hapi 17: Lojtari1 >>> Lojtari2
Hapi 18: Lojtari2 >>> Lojtari3
Hapi 19: Lojtari3 >>> Lojtari1
Hapi 20: Lojtari1 >>> Lojtari5
Hapi 21: Lojtari5 >>> Lojtari1
Hapi 22: Lojtari1 >>> Lojtari2
Hapi 23: Lojtari2 >>> Lojtari4
Hapi 24: Lojtari4 >>> Lojtari5
Hapi 25: Lojtari5 >>> Lojtari3
Hapi 26: Lojtari3 >>> Lojtari4
Hapi 27: Lojtari4 >>> Lojtari2
Hapi 28: Lojtari2 >>> Lojtari5
Hapi 29: Lojtari5 >>> Lojtari2
Hapi 30: Lojtari2 >>> Lojtari1
Hapi 31: Lojtari1 >>> Lojtari3
Hapi 32: Lojtari3 >>> Lojtari4
Hapi 33: Lojtari4 >>> Lojtari1
Hapi 34: Lojtari1 >>> Lojtari3
Hapi 35: Lojtari3 >>> Lojtari4
Hapi 36: Lojtari4 >>> Lojtari3
Hapi 37: Lojtari3 >>> Lojtari2
Hapi 38: Lojtari2 >>> Lojtari5
Hapi 39: Lojtari5 >>> Lojtari4
Hapi 40: Lojtari4 >>> Lojtari5
Hapi 41: Lojtari5 >>> Lojtari1
Hapi 42: Lojtari1 >>> Lojtari5
Hapi 43: Lojtari5 >>> Lojtari3
Hapi 44: Lojtari3 >>> Lojtari5
Hapi 45: Lojtari5 >>> Lojtari2
Hapi 46: Lojtari2 >>> Lojtari3
Hapi 47: Lojtari3 >>> Lojtari2
Hapi 48: Lojtari2 >>> Lojtari5
Hapi 49: Lojtari5 >>> Lojtari4
Hapi 50: Lojtari4 >>> Lojtari2
Hapi 51: Lojtari2 >>> Lojtari5
Hapi 52: Lojtari5 >>> Lojtari1
Hapi 53: Lojtari1 >>> Lojtari5
Hapi 54: Lojtari5 >>> Lojtari3
Hapi 55: Lojtari3 >>> Lojtari5
Hapi 56: Lojtari5 >>> Lojtari2
Hapi 57: Lojtari2 >>> Lojtari1
Hapi 58: Lojtari1 >>> Lojtari4
Hapi 59: Lojtari4 >>> Lojtari1
Hapi 60: Lojtari1 >>> Lojtari4
Hapi 61: Lojtari4 >>> Lojtari3
Hapi 62: Lojtari3 >>> Lojtari2
Hapi 63: Lojtari2 >>> Lojtari5
Hapi 64: Lojtari5 >>> Lojtari4
Hapi 65: Lojtari4 >>> Lojtari5
Hapi 66: Lojtari5 >>> Lojtari1
Hapi 67: Lojtari1 >>> Lojtari5
Hapi 68: Lojtari5 >>> Lojtari3
Hapi 69: Lojtari3 >>> Lojtari4
Hapi 70: Lojtari4 >>> Lojtari2
Hapi 71: Lojtari2 >>> Lojtari5
Hapi 72: Lojtari5 >>> Lojtari2
Hapi 73: Lojtari2 >>> Lojtari1
Hapi 74: Lojtari1 >>> Lojtari4
Hapi 75: Lojtari4 >>> Lojtari1
Hapi 76: Lojtari1 >>> Lojtari2
Hapi 77: Lojtari2 >>> Lojtari5
Hapi 78: Lojtari5 >>> Lojtari4
Hapi 79: Lojtari4 >>> Lojtari3
Hapi 80: Lojtari3 >>> Lojtari1
Hapi 81: Lojtari1 >>> Lojtari5
Hapi 82: Lojtari5 >>> Lojtari1
Hapi 83: Lojtari1 >>> Lojtari4
Hapi 84: Lojtari4 >>> Lojtari5
Hapi 85: Lojtari5 >>> Lojtari3
Hapi 86: Lojtari3 >>> Lojtari5
Hapi 87: Lojtari5 >>> Lojtari2
Hapi 88: Lojtari2 >>> Lojtari3
Lojtari2: Unë dal!
Hapi 89: Lojtari3 >>> Lojtari4
... lojtar Lojtari2 ka dalë ...
Hapi 90: Lojtari4 >>> Lojtari1
Hapi 91: Lojtari1 >>> Lojtari3
Hapi 92: Lojtari3 >>> Lojtari1
Hapi 93: Lojtari1 >>> Lojtari4
Hapi 94: Lojtari4 >>> Lojtari3
Hapi 95: Lojtari3 >>> Lojtari5
Hapi 96: Lojtari5 >>> Lojtari1
Hapi 97: Lojtari1 >>> Lojtari5
Hapi 98: Lojtari5 >>> Lojtari3
Hapi 99: Lojtari3 >>> Lojtari5
Hapi 100: Lojtari5 >>> Lojtari4
Hapi 101: Lojtari4 >>> Lojtari5
Lojtari4: Unë dal!
... lojtar Lojtari4 ka dalë ...
Hapi 102: Lojtari5 >>> Lojtari1
Hapi 103: Lojtari1 >>> Lojtari3
Hapi 104: Lojtari3 >>> Lojtari1
Hapi 105: Lojtari1 >>> Lojtari3
Hapi 106: Lojtari3 >>> Lojtari5
Hapi 107: Lojtari5 >>> Lojtari3
Hapi 108: Lojtari3 >>> Lojtari1
Hapi 109: Lojtari1 >>> Lojtari3
Hapi 110: Lojtari3 >>> Lojtari5
Hapi 111: Lojtari5 >>> Lojtari1
Hapi 112: Lojtari1 >>> Lojtari3
Hapi 113: Lojtari3 >>> Lojtari5
Hapi 114: Lojtari5 >>> Lojtari3
Hapi 115: Lojtari3 >>> Lojtari1
Hapi 116: Lojtari1 >>> Lojtari3
Hapi 117: Lojtari3 >>> Lojtari5
Hapi 118: Lojtari5 >>> Lojtari1
Hapi 119: Lojtari1 >>> Lojtari3
Hapi 120: Lojtari3 >>> Lojtari5
Hapi 121: Lojtari5 >>> Lojtari3
Lojtari5: Unë dal!
... lojtar Lojtari5 ka dalë ...
Hapi 122: Lojtari3 >>> Lojtari5
Hapi 123: Lojtari5 >>> Lojtari1
Lojtari5: Unë dal!
Hapi 124: Lojtari1 >>> Lojtari3
... lojtar Lojtari5 ka dalë ...
Hapi 125: Lojtari3 >>> Lojtari1
Hapi 126: Lojtari1 >>> Lojtari3
Lojtari1: Unë dal!
... lojtar Lojtari1 ka dalë ...
Hapi 127: Lojtari3 >>> Lojtari3
Lojtari3: Unë dal!
Hapi 128: Lojtari3 >>> Lojtari3
... lojtar Lojtari3 ka dalë ...
Lojtari3: Unë dal!
Ndalo!
Hapi 129: Lojtari3 >>> Lojtari3
Lojtari3: Unë dal!

Nga e gjitha kjo, mund të nxjerrim disa përfundime të rëndësishme:

  • me kusht që të kemi mjetet e nevojshme, zhvilluesit aplikativë mund të krijojnë ndërveprime integruese mes aplikacioneve pa u shkëputur nga logjika e biznesit;
  • kompleksiteti i detyrës integruese, që kërkon kompetenca inxhinierike, mund të fshihet brenda kuadrit, nëse e kemi parashikuar qysh në fillim në arkitekturën e kuadrit. Ndërsa vështirësia e detyrës nuk mund të fshihet, prandaj zgjidhja e një detyre të vështirë në kod do të duket përkatësisht;
  • në zhvillimin e logjikës integruese duhet patjetër të kemi parasysh konsistencën eventuale dhe mungesën e linearizueshmërisë së ndryshimeve të gjendjes së të gjithë pjesëmarrësve në integrim. Kjo e detyron logjikën të komplikohet, për ta bërë atë të pandjeshme ndaj rendit të ngjarjeve të jashtme. Në shembullin tonë, lojtari është i detyruar të marrë pjesë në lojë vetëm pasi të shpallë daljen nga loja: lojtarët e tjerë do të vazhdojnë t'i kalojnë topin, derisa informacioni për daljen e tij të arrijë dhe të përpunojë nga gjithë pjesëmarrësit. Kjo logjikë nuk buron nga rregullat e lojës dhe është një zgjidhje kompromisi brenda arkitekturës së zgjedhur.

Më pas do të flasim për nuancat e ndryshme të zgjidhjes sonë, kompromiset dhe çështje të tjera.

Të gjitha mesazhet – në një radhë

Të gjitha aplikacionet e integruara punojnë me një shufër integrimi, e cila paraqitet si një broker i jashtëm, një radhë BPMQueue – për mesazhet dhe një temë BPMTopic – për sinjale (ngjarje). Kalimi i të gjitha mesazheve përmes një radhe është vetë një kompromis. Në nivelin e logjikës së biznesit tani mund të shtojmë sa më shumë lloje të reja mesazhesh, pa bërë ndryshime në strukturën e sistemit. Kjo është një thjeshtim i rëndësishëm, por sjell disa rreziqe të caktuara, të cilat në kontekstin e detyrave tona tipike na dukeshin jo aq të rëndësishme.

Integrimi në stilin BPM

Megjithatë, ka një nuancë këtu: çdo aplikacion filtrojnë "mesazhet" e veta nga rrjedha që në hyrje, sipas emrit të domenit të tij. Gjithashtu, domeni mund të përmendet edhe në sinjale, nëse nevojitet të kufizohet "fusha e dukshmërisë" e sinjalit në një aplikacion të vetëm. Kjo duhet të rrisë kapacitetin e autobusit, por logjika e biznesit tani duhet të veprojë me emrat e domeneve: për adresimin e mesazheve – është e obligueshme, për sinjalet – e dëshiruara.

Sigurimi i besueshmërisë së autobusit integrues

Besueshmëria përbëhet nga disa pjesë:

  • Brokeri i mesazheve të zgjedhur është një komponent kritik i arkitekturës dhe një pikë e vetme dështimi: ai duhet të jetë mjaft i qëndrueshëm. Duhet të përdoren vetëm zbatime të provuara me kohë, me mbështetje të mirë dhe një komunitet të madh;
  • duhet të sigurohet një disponueshmëri e lartë e brokerit të mesazheve, për çka ai duhet të jetë fizikisht i ndarë nga aplikacionet që integrohen (sigurimi i disponueshmërisë së lartë të aplikacioneve me logjikën e biznesit është ndjeshëm më i komplikuar dhe më i shtrenjtë);
  • Brokeri duhet të sigurojë garanci "të paktën një herë" për dorëzimin. Ky është një kërkesë e obligueshme për funksionimin e besueshëm të autobusit integrues. Nuk ka nevojë për garanci të nivelit "saktësisht një herë": proceset e biznesit zakonisht nuk janë të ndjeshme ndaj rimarrjes së mesazheve ose ngjarjeve, dhe në raste të veçanta, ku kjo është e rëndësishme, është më e lehtë të shtohet një kontroll shtesë në logjikën e biznesit sesa të përdoren vazhdimisht garanci mjaft "të shtrenjta";
  • Dërgimi i mesazheve dhe sinjaleve duhet të përfshihet në një transaksion të përgjithshëm me ndryshimin e gjendjes së proceseve të biznesit dhe të dhënave domene. Opcioni i preferuar do të ishte përdorimi i modelit Transactional Outbox, por kjo do të kërkonte një tabelë shtesë në bazë të të dhënave dhe një retransmetues. Në aplikacionet JEE, kjo mund të thjeshtohet duke përdorur një menaxher lokal JTA, por lidhja me brokerin e zgjedhur duhet të jetë në gjendje të punojë në modalitetin XA;
  • përpunuesit e mesazheve dhe ngjarjeve që hyjnë gjithashtu duhet të punojnë me transaksionin e ndryshimit të gjendjes së procesit të biznesit: nëse një transaksion i tillë anulohet, atëherë prana e mesazhit duhet të anulohet;
  • mesazhet që nuk arritën të dorëzoheshin për shkak të gabimeve duhet të ruhen në një depo të veçantë DLQ (Dead Letter Queue). Ne kemi krijuar një mikroshërbim të veçantë në platformë, i cili ruan mesazhet e tilla në magazinën e tij, i indekson ato sipas atributeve (për grupim dhe kërkim të shpejtë), dhe ofron një API për të parë, dërguar përsëri në adresën e caktuar, ose fshirë mesazhet. Administratorët e sistemit mund të punojnë me këtë shërbim përmes ndërfaqes së tij web;
  • në cilësimet e brokerit duhet të përshtatet numri i përpjekjeve të përsëritura për dërgim dhe vonesat midis dërgesave, për të ulur mundësinë e mesazheve që përfundojnë në DLQ (për të llogaritur parametrat optimalë është praktikisht e pamundur, por mund të veprohet empirikisht dhe të përshtaten gjatë operimit);
  • magazina DLQ duhet të monitorohet vazhdimisht, dhe sistemi i monitorimit duhet të alarmojë administratorët e sistemit, në mënyrë që të reagojnë sa më shpejt të jetë e mundur kur shfaqen mesazhe të papërfunduara. Kjo do të lejojë uljen e "zonës së prekur" shkaktuar nga dështimi ose gabimi i logjikës biznesore;
  • busa integruese duhet të jetë e padurueshme ndaj mungesës së përkohshme të aplikacioneve: abonetë në temë duhet të jenë të qëndrueshme, dhe emri i domenit të aplikacionit duhet të jetë unik, në mënyrë që gjatë mungesës së aplikacionit, mesazhet e tij nga rradha të mos përpiqen të përpunohen nga dikush tjetër.

Sigurimi i sigurisë në rrjedhën e logjikës biznesore

Të njëjtit ekzemplar të procesit të biznesit mund t'i dorëzohen menjëherë disa mesazhe dhe ngjarje, përpunimi i të cilave do të fillojë përparësisht. Në të njëjtën kohë, për zhvilluesin aplikativ, gjithçka duhet të jetë e thjeshtë dhe e sigurt në rrjedhë.

Logjika biznesore e procesit përpunon çdo ngjarje të jashtme që ndikon në këtë proces biznesi, veçmas. Ngjarjet e tilla mund të jenë:

  • fillimi i ekzemplarit të procesit të biznesit;
  • veprimi i përdoruesit, i lidhur me aktivitetin brenda procesit biznesor;
  • ardhja e një mesazhi apo sinjali, në të cilin është i abonuar ekzemplari i procesit të biznesit;
  • aktivizimi i një timer-i, të vendosur nga ekzemplari i procesit të biznesit;
  • ndërhyrja kontrolluese përmes API-së (p.sh., ndërprerja emergjente e procesit).

Çdo ngjarje e tillë mund të ndërlikojë gjendjen e instancës së procesit të biznesit: disa aktivitete mund të përfundojnë dhe disa të tjera mund të fillojnë, mund të ndryshojnë vlerat e pronave të persistuara. Mbyllja e çdo aktiviteti mund të çojë në aktivizimin e një ose më shumë aktiviteteve të ardhshme. Ato, nga ana e tyre, mund të ndalen duke pritur për ngjarje të tjera ose, nëse nuk kanë nevojë për të dhëna të tjera, mund të përfundojnë në të njëjtën transaksion. Para mbylljes së transaksionit, gjendja e re e procesit të biznesit ruhet në DB, ku do të presë që ndodhi ngjarja e ardhshme.

Të dhënat e persistuara të procesit të biznesit, të ruajtura në një DB rrelacionale, janë një pikë shumë e përshtatshme e sinkronizimit të përpunimit, nëse përdoret SELECT FOR UPDATE. Nëse një transaksion ka arritur të marrë gjendjen e procesit të biznesit nga databaza për ta ndryshuar, atëherë asnjë transaksion tjetër paralel nuk do të ketë mundësi të marrë të njëjtën gjendje për ndonjë ndryshim tjetër, dhe pas përfundimit të transaksionit të parë, transaksioni i dytë do të marrë me siguri gjendjen e ndryshuar.

Duke përdorur bllokimet pesimiste në anën e DBMS, ne përmbushim të gjitha kërkesat e nevojshme ACID, dhe gjithashtu ruajmë mundësinë për të shkallëzuar aplikacionin me logjikën e biznesit duke rritur numrin e instancave të aktivizuara.

Megjithatë, bllokimet pesimiste na rrezikojnë me deadlocks, prandaj, SELECT FOR UPDATE ende duhet të kufizohet me një timeout të arsyeshëm në rast të ndodhisë së deadlocks në ndonjë rast të rëndë në logjikën e biznesit.

Një problem tjetër është sinkronizimi i fillimit të procesit të biznesit. Derisa të mos ketë një instancë të procesit të biznesit, nuk ka as gjendjen e tij në databazë, kështu që metoda e përshkruar nuk do të funksionojë. Nëse nevojitet të sigurohet unikësia e instancës së procesit të biznesit në një skop të caktuar, atëherë do të kërkohet një objekt sinkronizimi, i asociuar me klasën e procesit dhe skopin përkatës. Për të zgjidhur këtë problem ne përdorim një mekanizëm tjetër bllokimi që lejon marrjen e një bllokimi të ndonjë burimi të rastësishëm, të caktuar me çelësin në formatin URI, përmes një shërbimi të jashtëm.

Në shembujt tanë, procesi i biznesit InitialPlayer përmban shpalljen

uniqueConstraint = UniqueConstraints.singleton

Prandaj, në log ka mesazhe për marrë e lirimin e bllokimit të çelësit përkatës. Në proceset e tjera të biznesit, mesazhe të tilla nuk ka: uniqueConstraint nuk është caktuar.

Problemet e proceseve të biznesit me gjendje të qëndrueshme.

Herë pas here, pranija e gjendjes së qëndrueshme jo vetëm që ndihmon, por gjithashtu pengon shumë në zhvillim.
Problemet fillojnë kur është e nevojshme të bëhen ndryshime në logjikën e biznesit dhe/ose modelin e procesit të biznesit. Jo çdo ndryshim i tillë është i përputhshëm me gjendjen e vjetër të proceseve të biznesit. Nëse në bazën e të dhënave ka shumë ekzemplarë "të gjallë", atëherë bërja e ndryshimeve të papërputhshme mund të sjellë shumë probleme, me të cilat shpesh jemi përballur gjatë përdorimit të jBPM.

Në varësi të thellësisë së ndryshimeve, mund të veprohet në dy mënyra:

  1. të krijohet një tip i ri procesi biznesi, në mënyrë që të mos bëhen ndryshime të papërputhshme në të vjetrin, dhe ta përdorim atë në vend të të vjetrës për kur nisëm ekzemplarë të rinj. Ekzemplarët e vjetër do të vazhdojnë të funksionojnë "si më parë";
  2. të migrohet gjendja e qëndrueshme e proceseve të biznesit gjatë përditësimit të logjikës së biznesit.

Rruga e parë është më e thjeshtë, por ka kufizime dhe disavantazhe, për shembull:

  • duke ripërsëritur logjikën e biznesit në shumë modele të proceseve të biznesit, duke rritur vëllimin e logjikës së biznesit;
  • shpesh kërkohet një kalim të menjëhershëm në logjikën e re të biznesit (në pjesën e detyrave të integrimit – pothuajse gjithmonë);
  • zhvilluesi nuk e di se në cilin moment mund të fshijë modelet e vjetruara.

Në praktikë ne përdorim të dy qasje, por kemi bërë disa vendime për t'i thjeshtuar vetes jetën:

  • në bazën e të dhënave, gjendja e qëndrueshme e procesit të biznesit ruhet në një format të lehtë për t'u lexuar dhe procesuar: në një varg formati JSON. Kjo lejon që migrimet të kryhen si brenda aplikacionit, ashtu edhe jashtë. Në rastin më ekstrem mund të rregullohet edhe manualisht (sidomos e dobishme në zhvillim gjatë debugging);
  • logjika e integrimit të biznesit nuk përdor emrat e proceseve të biznesit, në mënyrë që në çdo moment të mund të zëvendësohet realizimi i njërit nga proceset e përfshira me një të re, me një emër të ri (p.sh., "InitialPlayerV2"). Lidhja ndodh përmes emrave të mesazheve dhe sinjalëve;
  • modeli i procesit ka një numër versioni, të cilin ne e rrisim nëse bëjmë ndryshime të papajtueshme në këtë model, dhe ky numër ruhet së bashku me gjendjen e instancës së procesit;
  • gjendja persistente e procesit lexohesh nga baza fillimisht në një model objekti të përshtatshëm, me të cilin mund të punojë procedura e migrimit, nëse ka ndryshuar numri i versionit të modelit;
  • procedura e migrimit vendoset pranë logjikës biznesore dhe thirret "lenë" për çdo instancë të procesit biznesor në momentin e rikthimit të saj nga baza;
  • nëse duhet të migrohet gjendja e të gjitha instancave të procesit në mënyrë të shpejtë dhe sinkrone, zbatohen zgjidhje më klasike për migrimin e DB, por atje duhet të punohet me JSON.

A nevojitet një tjetër kornizë për proceset biznesore?

Zgjidhjet e përshkruara në artikull na lejuan të thjeshtojmë dukshëm jetën, të zgjeronim gamën e çështjeve që zgjidhen në nivelin e zhvillimit të aplikacioneve, të bëjmë më atraktive idetë për ndarjen e logjikës biznesore në mikrosherbime. Për këtë kemi bërë shumë punë, kemi krijuar një kornizë shumë "e lehtë" për proceset biznesore, si dhe komponentë shërbimi për zgjidhjen e problemeve të shënuara në kontekstin e një gamë të gjerë detyrash aplikative. Ne kemi dëshirë të ndajmë këto rezultate, të publikojmë zhvillimin e komponentëve të përbashkët në akses të hapur nën një licencë të lirë. Kjo do të kërkojë përpjekje dhe kohë të caktuar. Kuptimi i kërkesës për zgjidhje të tilla mund të bëhej një stimul shtesë për ne. Artikulli i propozuar i kushton shumë pak vëmendje mundësive të vetë kornizës, por disa nga ato janë të dukshme nga shembujt e paraqitur. Nëse megjithatë publikojmë kornizën tonë, do t'i dedikohet një artikull i veçantë. Por për tani do të ishim mirënjohës nëse do të linit një feedback të vogël, duke iu përgjigjur pyetjes:

Vetëm përdoruesit e regjistruar mund të marrin pjesë në anketë. Hyni, ju lutem.

A nevojitet një tjetër kornizë për proceset biznesore?

  • 18,8%po, prej kohësh po kërkojmë diçka të tillë3

  • 12,5%është interesante të mësojmë më shumë për implementimin tuaj, ndoshta do të nevojitet2

  • 6,2%po përdorim një nga kornizat ekzistuese, por po mendojmë për një zëvendësim1

  • 18,8%po përdorim një nga kornizat ekzistuese, të gjitha janë në rregull3

  • 18,8%po ia dalim pa një kornizë3

  • 25,0%po shkruajmë tonën4

16 përdorues votuan. 7 përdorues abstenuan.

Burimi: habr.com

Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS 🔥 Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS - ProHoster